import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'dart:async'; /// 메모리 관리를 위한 헬퍼 클래스 class MemoryManager { static final MemoryManager _instance = MemoryManager._internal(); factory MemoryManager() => _instance; MemoryManager._internal(); // 캐시 관리 final Map _cache = {}; final int _maxCacheSize = 100; final Duration _defaultTTL = const Duration(minutes: 5); // 이미지 캐시 관리 static const int maxImageCacheSize = 50 * 1024 * 1024; // 50MB static const int maxImageCacheCount = 100; // 위젯 참조 추적 final Map> _widgetReferences = {}; /// 캐시에 데이터 저장 void cacheData({ required String key, required T data, Duration? ttl, }) { _cleanupExpiredCache(); if (_cache.length >= _maxCacheSize) { _evictOldestEntry(); } _cache[key] = _CacheEntry( data: data, timestamp: DateTime.now(), ttl: ttl ?? _defaultTTL, ); } /// 캐시에서 데이터 가져오기 T? getCachedData(String key) { final entry = _cache[key]; if (entry == null) return null; if (entry.isExpired) { _cache.remove(key); return null; } entry.lastAccess = DateTime.now(); return entry.data as T?; } /// 캐시 비우기 void clearCache() { _cache.clear(); if (kDebugMode) { print('🧹 메모리 캐시가 비워졌습니다.'); } } /// 특정 패턴의 캐시 제거 void clearCacheByPattern(String pattern) { final keysToRemove = _cache.keys .where((key) => key.contains(pattern)) .toList(); for (final key in keysToRemove) { _cache.remove(key); } } /// 만료된 캐시 정리 void _cleanupExpiredCache() { final expiredKeys = _cache.entries .where((entry) => entry.value.isExpired) .map((entry) => entry.key) .toList(); for (final key in expiredKeys) { _cache.remove(key); } } /// 가장 오래된 캐시 항목 제거 void _evictOldestEntry() { if (_cache.isEmpty) return; var oldestKey = _cache.keys.first; var oldestTime = _cache[oldestKey]!.lastAccess; for (final entry in _cache.entries) { if (entry.value.lastAccess.isBefore(oldestTime)) { oldestKey = entry.key; oldestTime = entry.value.lastAccess; } } _cache.remove(oldestKey); } /// 이미지 캐시 최적화 static void optimizeImageCache() { PaintingBinding.instance.imageCache.maximumSize = maxImageCacheCount; PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize; } /// 이미지 캐시 상태 확인 static ImageCacheStatus getImageCacheStatus() { final cache = PaintingBinding.instance.imageCache; return ImageCacheStatus( currentSize: cache.currentSize, maximumSize: cache.maximumSize, currentSizeBytes: cache.currentSizeBytes, maximumSizeBytes: cache.maximumSizeBytes, ); } /// 이미지 캐시 비우기 static void clearImageCache() { PaintingBinding.instance.imageCache.clear(); PaintingBinding.instance.imageCache.clearLiveImages(); if (kDebugMode) { print('🖼️ 이미지 캐시가 비워졌습니다.'); } } /// 위젯 참조 추적 void trackWidget(String key, State widget) { _widgetReferences[key] = WeakReference(widget); } /// 위젯 참조 제거 void untrackWidget(String key) { _widgetReferences.remove(key); } /// 살아있는 위젯 수 확인 int getAliveWidgetCount() { return _widgetReferences.values .where((ref) => ref.target != null) .length; } /// 메모리 압박 시 대응 void handleMemoryPressure() { // 캐시 50% 제거 final keysToRemove = _cache.keys.take(_cache.length ~/ 2).toList(); for (final key in keysToRemove) { _cache.remove(key); } // 이미지 캐시 축소 final imageCache = PaintingBinding.instance.imageCache; imageCache.maximumSize = maxImageCacheCount ~/ 2; imageCache.maximumSizeBytes = maxImageCacheSize ~/ 2; if (kDebugMode) { print('⚠️ 메모리 압박 대응: 캐시 크기 감소'); } } /// 자동 메모리 정리 시작 Timer? _cleanupTimer; void startAutoCleanup({Duration interval = const Duration(minutes: 1)}) { _cleanupTimer?.cancel(); _cleanupTimer = Timer.periodic(interval, (_) { _cleanupExpiredCache(); // 죽은 위젯 참조 제거 final deadKeys = _widgetReferences.entries .where((entry) => entry.value.target == null) .map((entry) => entry.key) .toList(); for (final key in deadKeys) { _widgetReferences.remove(key); } }); } /// 자동 메모리 정리 중지 void stopAutoCleanup() { _cleanupTimer?.cancel(); _cleanupTimer = null; } /// 메모리 사용량 리포트 Map getMemoryReport() { return { 'cacheSize': _cache.length, 'maxCacheSize': _maxCacheSize, 'aliveWidgets': getAliveWidgetCount(), 'totalWidgetReferences': _widgetReferences.length, 'imageCacheStatus': getImageCacheStatus().toJson(), }; } } /// 캐시 항목 클래스 class _CacheEntry { final dynamic data; final DateTime timestamp; final Duration ttl; DateTime lastAccess; _CacheEntry({ required this.data, required this.timestamp, required this.ttl, }) : lastAccess = timestamp; bool get isExpired => DateTime.now().difference(timestamp) > ttl; } /// 이미지 캐시 상태 클래스 class ImageCacheStatus { final int currentSize; final int maximumSize; final int currentSizeBytes; final int maximumSizeBytes; ImageCacheStatus({ required this.currentSize, required this.maximumSize, required this.currentSizeBytes, required this.maximumSizeBytes, }); double get sizeUsagePercentage => (currentSize / maximumSize) * 100; double get bytesUsagePercentage => (currentSizeBytes / maximumSizeBytes) * 100; Map toJson() => { 'currentSize': currentSize, 'maximumSize': maximumSize, 'currentSizeBytes': currentSizeBytes, 'maximumSizeBytes': maximumSizeBytes, 'sizeUsagePercentage': sizeUsagePercentage.toStringAsFixed(2), 'bytesUsagePercentage': bytesUsagePercentage.toStringAsFixed(2), }; } /// 메모리 효율적인 리스트 뷰 class MemoryEfficientListView extends StatefulWidget { final List items; final Widget Function(BuildContext, T) itemBuilder; final int cacheExtent; final ScrollPhysics? physics; const MemoryEfficientListView({ super.key, required this.items, required this.itemBuilder, this.cacheExtent = 250, this.physics, }); @override State> createState() => _MemoryEfficientListViewState(); } class _MemoryEfficientListViewState extends State> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => false; @override Widget build(BuildContext context) { super.build(context); return ListView.builder( itemCount: widget.items.length, cacheExtent: widget.cacheExtent.toDouble(), physics: widget.physics ?? const BouncingScrollPhysics(), itemBuilder: (context, index) { return widget.itemBuilder(context, widget.items[index]); }, ); } }