Skip to content

Memory Cache & In-Memory Caching ​

In-memory caching is one of the most critical performance optimization techniques in mobile applications, providing fast data access while considering device resource constraints.

Quick Decision ​

SituationUse Memory CacheWatch Out
Small frequently read dataYesLRU/LFU may be enough
Large bitmaps/listsLimitedMemory pressure must be monitored
Sensitive dataAvoid or keep short-livedDump/log risk
Offline persistenceNo, disk is neededLost after app kill

Production Checklist ​

  • Problem: Which latency issue does memory cache solve?
  • Solution: Are max size, eviction, memory warning handling, key, and lifecycle clear?
  • Trade-off: More cache is faster; more memory increases crash and OS kill risk.
  • Failure mode: Memory leaks, stale objects, unbounded maps, and bitmap OOM should be handled.
  • Measurement: Track hit ratio, memory footprint, eviction count, GC pressure, and OOM/crash rate.
  • Security/cost: Tokens/PII should not be kept in memory unnecessarily; low-RAM devices need separate testing.

Memory Cache Fundamentals ​

Mobile Memory Constraints ​

Mobile devices have unique memory constraints that require special attention:

  • Limited RAM: 2-8GB on average devices
  • Memory Pressure: Operating system can terminate applications
  • Battery Impact: Excessive memory usage drains battery
  • Shared Resources: Memory shared with other applications

Cache Size Calculation ​

java
// Android - Optimal Cache Size Calculation
public class MemoryCacheManager {
    private static final float MEMORY_CACHE_PERCENTAGE = 0.125f; // 12.5% of available memory
    
    public static int calculateOptimalCacheSize() {
        final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        final int memoryClass = am.getMemoryClass();
        
        // Convert to bytes and calculate cache size
        final int memoryClassBytes = memoryClass * 1024 * 1024;
        return Math.round(memoryClassBytes * MEMORY_CACHE_PERCENTAGE);
    }
}

LRU Cache Implementation ​

Android LruCache ​

java
// Android - Advanced LruCache Implementation
public class AdvancedLruCache<K, V> extends LruCache<K, V> {
    private final Map<K, Long> accessTimes = new ConcurrentHashMap<>();
    private final Map<K, Integer> accessCounts = new ConcurrentHashMap<>();
    private final EvictionCallback<K, V> evictionCallback;
    
    public AdvancedLruCache(int maxSize, EvictionCallback<K, V> callback) {
        super(maxSize);
        this.evictionCallback = callback;
    }
    
    @Override
    public V get(K key) {
        V value = super.get(key);
        if (value != null) {
            recordAccess(key);
        }
        return value;
    }
    
    @Override
    public V put(K key, V value) {
        recordAccess(key);
        return super.put(key, value);
    }
    
    @Override
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
        super.entryRemoved(evicted, key, oldValue, newValue);
        
        if (evicted && evictionCallback != null) {
            evictionCallback.onEntryEvicted(key, oldValue);
        }
        
        // Cleanup tracking maps
        accessTimes.remove(key);
        accessCounts.remove(key);
    }
    
    private void recordAccess(K key) {
        long currentTime = System.currentTimeMillis();
        accessTimes.put(key, currentTime);
        accessCounts.merge(key, 1, Integer::sum);
    }
    
    public Map<K, AccessPattern> getAccessPatterns() {
        Map<K, AccessPattern> patterns = new HashMap<>();
        
        for (K key : accessTimes.keySet()) {
            Long lastAccess = accessTimes.get(key);
            Integer count = accessCounts.get(key);
            
            if (lastAccess != null && count != null) {
                patterns.put(key, new AccessPattern(lastAccess, count));
            }
        }
        
        return patterns;
    }
    
    public interface EvictionCallback<K, V> {
        void onEntryEvicted(K key, V value);
    }
    
    public static class AccessPattern {
        public final long lastAccessTime;
        public final int accessCount;
        
        public AccessPattern(long lastAccessTime, int accessCount) {
            this.lastAccessTime = lastAccessTime;
            this.accessCount = accessCount;
        }
        
        public boolean isFrequentlyAccessed() {
            return accessCount > 10; // Threshold can be adjusted
        }
        
        public boolean isRecentlyAccessed() {
            return System.currentTimeMillis() - lastAccessTime < 300000; // 5 minutes
        }
    }
}

iOS NSCache Integration ​

swift
// iOS - Smart NSCache with Memory Management
class SmartMemoryCache<Key: AnyObject, Value: AnyObject> where Key: NSCopying {
    private let cache = NSCache<Key, CacheWrapper<Value>>()
    private let accessQueue = DispatchQueue(label: "cache.access.queue", attributes: .concurrent)
    private var accessPatterns: [Key: AccessPattern] = [:]
    
    var totalCostLimit: Int {
        get { return cache.totalCostLimit }
        set { cache.totalCostLimit = newValue }
    }
    
    var countLimit: Int {
        get { return cache.countLimit }
        set { cache.countLimit = newValue }
    }
    
    init() {
        setupMemoryWarningObserver()
        configureCacheDefaults()
    }
    
    private func configureCacheDefaults() {
        // Set default limits based on device memory
        let physicalMemory = ProcessInfo.processInfo.physicalMemory
        let memoryCacheLimit = Int(Double(physicalMemory) * 0.1) // 10% of physical memory
        
        cache.totalCostLimit = memoryCacheLimit
        cache.countLimit = 100 // Default object count limit
        cache.delegate = self
    }
    
    func object(forKey key: Key) -> Value? {
        return accessQueue.sync { [weak self] in
            guard let wrapper = self?.cache.object(forKey: key) else {
                return nil
            }
            
            // Record access pattern
            self?.recordAccess(forKey: key)
            
            return wrapper.value
        }
    }
    
    func setObject(_ object: Value, forKey key: Key, cost: Int = 0) {
        accessQueue.async(flags: .barrier) { [weak self] in
            let wrapper = CacheWrapper(value: object, cost: cost)
            self?.cache.setObject(wrapper, forKey: key, cost: cost)
            self?.recordAccess(forKey: key)
        }
    }
    
    func removeObject(forKey key: Key) {
        accessQueue.async(flags: .barrier) { [weak self] in
            self?.cache.removeObject(forKey: key)
            self?.accessPatterns.removeValue(forKey: key)
        }
    }
    
    func removeAllObjects() {
        accessQueue.async(flags: .barrier) { [weak self] in
            self?.cache.removeAllObjects()
            self?.accessPatterns.removeAll()
        }
    }
    
    private func recordAccess(forKey key: Key) {
        let currentTime = Date()
        
        if let pattern = accessPatterns[key] {
            pattern.recordAccess(at: currentTime)
        } else {
            accessPatterns[key] = AccessPattern(firstAccess: currentTime)
        }
    }
    
    private func setupMemoryWarningObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
    
    @objc private func handleMemoryWarning() {
        // Implement intelligent cleanup based on access patterns
        performIntelligentCleanup()
    }
    
    private func performIntelligentCleanup() {
        accessQueue.async(flags: .barrier) { [weak self] in
            guard let self = self else { return }
            
            let currentTime = Date()
            let keysToRemove: [Key] = self.accessPatterns.compactMap { (key, pattern) in
                // Remove items not accessed in the last 10 minutes
                if currentTime.timeIntervalSince(pattern.lastAccess) > 600 {
                    return key
                }
                return nil
            }
            
            for key in keysToRemove {
                self.cache.removeObject(forKey: key)
                self.accessPatterns.removeValue(forKey: key)
            }
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

extension SmartMemoryCache: NSCacheDelegate {
    func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: AnyObject) {
        // Handle eviction if needed
        print("Cache will evict object: \(obj)")
    }
}

private class CacheWrapper<T> {
    let value: T
    let cost: Int
    let creationTime = Date()
    
    init(value: T, cost: Int) {
        self.value = value
        self.cost = cost
    }
}

private class AccessPattern {
    private(set) var lastAccess: Date
    private(set) var accessCount: Int = 1
    private(set) var totalAccessTime: TimeInterval = 0
    
    init(firstAccess: Date) {
        self.lastAccess = firstAccess
    }
    
    func recordAccess(at time: Date) {
        totalAccessTime += time.timeIntervalSince(lastAccess)
        lastAccess = time
        accessCount += 1
    }
    
    var averageAccessInterval: TimeInterval {
        return accessCount > 1 ? totalAccessTime / Double(accessCount - 1) : 0
    }
}

Advanced LRU Strategies ​

Size-based Eviction ​

dart
// Flutter - Advanced LRU with Size-based Eviction
class SizeAwareLRUCache<K, V> {
  final int maxSize;
  final int maxMemorySize;
  final Map<K, CacheEntry<V>> _cache = LinkedHashMap();
  final SizeCalculator<V> _sizeCalculator;
  int _currentMemoryUsage = 0;
  
  SizeAwareLRUCache({
    required this.maxSize,
    required this.maxMemorySize,
    required SizeCalculator<V> sizeCalculator,
  }) : _sizeCalculator = sizeCalculator;
  
  V? get(K key) {
    final entry = _cache.remove(key);
    if (entry == null) return null;
    
    // Move to end (most recently used)
    _cache[key] = entry;
    entry.recordAccess();
    
    return entry.value;
  }
  
  void put(K key, V value) {
    final size = _sizeCalculator.calculateSize(value);
    
    // Remove existing entry if present
    final existingEntry = _cache.remove(key);
    if (existingEntry != null) {
      _currentMemoryUsage -= existingEntry.size;
    }
    
    // Ensure there's space for the new entry
    _ensureSpace(size);
    
    // Add new entry
    final entry = CacheEntry(value: value, size: size);
    _cache[key] = entry;
    _currentMemoryUsage += size;
  }
  
  void _ensureSpace(int requiredSize) {
    // Remove entries until there's enough space
    while ((_cache.length >= maxSize || 
            _currentMemoryUsage + requiredSize > maxMemorySize) &&
           _cache.isNotEmpty) {
      _removeLRU();
    }
  }
  
  void _removeLRU() {
    if (_cache.isEmpty) return;
    
    final entry = _cache.entries.first;
    _cache.remove(entry.key);
    _currentMemoryUsage -= entry.value.size;
  }
  
  void clear() {
    _cache.clear();
    _currentMemoryUsage = 0;
  }
  
  CacheStats getStats() {
    final entries = _cache.values.toList();
    final totalAccesses = entries.fold(0, (sum, entry) => sum + entry.accessCount);
    final averageSize = entries.isEmpty ? 0 : _currentMemoryUsage ~/ entries.length;
    
    return CacheStats(
      entryCount: _cache.length,
      memoryUsage: _currentMemoryUsage,
      maxMemoryUsage: maxMemorySize,
      totalAccesses: totalAccesses,
      averageEntrySize: averageSize,
    );
  }
}

class CacheEntry<V> {
  final V value;
  final int size;
  final DateTime creationTime = DateTime.now();
  DateTime lastAccessTime = DateTime.now();
  int accessCount = 0;
  
  CacheEntry({required this.value, required this.size});
  
  void recordAccess() {
    lastAccessTime = DateTime.now();
    accessCount++;
  }
  
  Duration get age => DateTime.now().difference(creationTime);
  Duration get timeSinceLastAccess => DateTime.now().difference(lastAccessTime);
}

abstract class SizeCalculator<T> {
  int calculateSize(T object);
}

class StringSizeCalculator implements SizeCalculator<String> {
  @override
  int calculateSize(String object) {
    return object.length * 2; // Approximate size in bytes (UTF-16)
  }
}

class ImageSizeCalculator implements SizeCalculator<ui.Image> {
  @override
  int calculateSize(ui.Image object) {
    return object.width * object.height * 4; // RGBA bytes
  }
}

class CacheStats {
  final int entryCount;
  final int memoryUsage;
  final int maxMemoryUsage;
  final int totalAccesses;
  final int averageEntrySize;
  
  CacheStats({
    required this.entryCount,
    required this.memoryUsage,
    required this.maxMemoryUsage,
    required this.totalAccesses,
    required this.averageEntrySize,
  });
  
  double get memoryUsagePercentage => 
      maxMemoryUsage > 0 ? (memoryUsage / maxMemoryUsage) * 100 : 0;
  
  double get averageAccessesPerEntry => 
      entryCount > 0 ? totalAccesses / entryCount : 0;
}

Image Caching Solutions ​

Android Image Cache with Glide ​

java
// Android - Custom Glide Image Cache
public class SmartImageCache {
    private final GlideRequests glideRequests;
    private final Map<String, ImageMetrics> imageMetrics = new ConcurrentHashMap<>();
    
    public SmartImageCache(Context context) {
        this.glideRequests = GlideApp.with(context);
    }
    
    public void loadImage(String url, ImageView imageView, ImageLoadCallback callback) {
        recordImageRequest(url);
        
        glideRequests
            .load(url)
            .transform(new SmartTransformation())
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(@Nullable GlideException e, Object model, 
                                          Target<Drawable> target, boolean isFirstResource) {
                    recordImageFailure(url, e);
                    callback.onError(e);
                    return false;
                }
                
                @Override
                public boolean onResourceReady(Drawable resource, Object model, 
                                             Target<Drawable> target, DataSource dataSource, 
                                             boolean isFirstResource) {
                    recordImageSuccess(url, dataSource);
                    callback.onSuccess(resource);
                    return false;
                }
            })
            .into(imageView);
    }
    
    private void recordImageRequest(String url) {
        imageMetrics.computeIfAbsent(url, k -> new ImageMetrics(url)).recordRequest();
    }
    
    private void recordImageSuccess(String url, DataSource dataSource) {
        ImageMetrics metrics = imageMetrics.get(url);
        if (metrics != null) {
            metrics.recordSuccess(dataSource);
        }
    }
    
    private void recordImageFailure(String url, Exception error) {
        ImageMetrics metrics = imageMetrics.get(url);
        if (metrics != null) {
            metrics.recordFailure(error);
        }
    }
    
    public ImageCacheStats getCacheStats() {
        int totalRequests = 0;
        int cacheHits = 0;
        int failures = 0;
        
        for (ImageMetrics metrics : imageMetrics.values()) {
            totalRequests += metrics.getTotalRequests();
            cacheHits += metrics.getCacheHits();
            failures += metrics.getFailures();
        }
        
        return new ImageCacheStats(totalRequests, cacheHits, failures);
    }
    
    private static class SmartTransformation extends BitmapTransformation {
        @Override
        protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, 
                                 int outWidth, int outHeight) {
            // Intelligent image resizing based on view size and device density
            float scale = Math.min(
                (float) outWidth / toTransform.getWidth(),
                (float) outHeight / toTransform.getHeight()
            );
            
            if (scale < 1.0f) {
                Matrix matrix = new Matrix();
                matrix.setScale(scale, scale);
                
                return Bitmap.createBitmap(toTransform, 0, 0, 
                    toTransform.getWidth(), toTransform.getHeight(), matrix, true);
            }
            
            return toTransform;
        }
        
        @Override
        public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
            messageDigest.update("smart_transformation".getBytes());
        }
    }
    
    public interface ImageLoadCallback {
        void onSuccess(Drawable drawable);
        void onError(Exception error);
    }
}

class ImageMetrics {
    private final String url;
    private int totalRequests = 0;
    private int cacheHits = 0;
    private int failures = 0;
    private long totalLoadTime = 0;
    
    public ImageMetrics(String url) {
        this.url = url;
    }
    
    public void recordRequest() {
        totalRequests++;
    }
    
    public void recordSuccess(DataSource dataSource) {
        if (dataSource == DataSource.MEMORY_CACHE || dataSource == DataSource.DISK_CACHE) {
            cacheHits++;
        }
    }
    
    public void recordFailure(Exception error) {
        failures++;
    }
    
    // Getters
    public int getTotalRequests() { return totalRequests; }
    public int getCacheHits() { return cacheHits; }
    public int getFailures() { return failures; }
}

class ImageCacheStats {
    private final int totalRequests;
    private final int cacheHits;
    private final int failures;
    
    public ImageCacheStats(int totalRequests, int cacheHits, int failures) {
        this.totalRequests = totalRequests;
        this.cacheHits = cacheHits;
        this.failures = failures;
    }
    
    public double getCacheHitRatio() {
        return totalRequests > 0 ? (double) cacheHits / totalRequests : 0;
    }
    
    public double getFailureRatio() {
        return totalRequests > 0 ? (double) failures / totalRequests : 0;
    }
}

iOS Image Cache with Kingfisher ​

swift
// iOS - Advanced Kingfisher Image Cache
class SmartImageCacheManager {
    private let imageCache: ImageCache
    private let downloader: ImageDownloader
    private var imageMetrics: [String: ImageMetrics] = [:]
    private let metricsQueue = DispatchQueue(label: "image.metrics.queue")
    
    init() {
        // Configure cache
        imageCache = ImageCache(name: "SmartImageCache")
        imageCache.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024 // 100MB
        imageCache.diskStorage.config.sizeLimit = 500 * 1024 * 1024 // 500MB
        
        // Configure downloader
        downloader = ImageDownloader(name: "SmartImageDownloader")
        downloader.downloadTimeout = 30.0
        
        setupCacheEventHandlers()
    }
    
    func loadImage(from url: URL, 
                   into imageView: UIImageView,
                   completion: @escaping (Result<RetrieveImageResult, KingfisherError>) -> Void) {
        
        recordImageRequest(for: url.absoluteString)
        
        let options: KingfisherOptionsInfo = [
            .targetCache(imageCache),
            .downloader(downloader),
            .processor(SmartImageProcessor()),
            .cacheSerializer(SmartCacheSerializer()),
            .backgroundDecode
        ]
        
        imageView.kf.setImage(
            with: url,
            options: options
        ) { [weak self] result in
            self?.handleImageResult(result, for: url.absoluteString)
            completion(result)
        }
    }
    
    private func setupCacheEventHandlers() {
        // Monitor cache events
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
    
    @objc private func handleMemoryWarning() {
        // Clear memory cache on memory pressure
        imageCache.clearMemoryCache()
        
        // Optionally, clear old disk cache entries
        imageCache.cleanExpiredDiskCache()
    }
    
    private func recordImageRequest(for url: String) {
        metricsQueue.async { [weak self] in
            if self?.imageMetrics[url] == nil {
                self?.imageMetrics[url] = ImageMetrics(url: url)
            }
            self?.imageMetrics[url]?.recordRequest()
        }
    }
    
    private func handleImageResult(_ result: Result<RetrieveImageResult, KingfisherError>, 
                                   for url: String) {
        metricsQueue.async { [weak self] in
            guard let metrics = self?.imageMetrics[url] else { return }
            
            switch result {
            case .success(let imageResult):
                metrics.recordSuccess(cacheType: imageResult.cacheType)
            case .failure(let error):
                metrics.recordFailure(error: error)
            }
        }
    }
    
    func getCacheStatistics() -> ImageCacheStatistics {
        return metricsQueue.sync {
            let totalRequests = imageMetrics.values.reduce(0) { $0 + $1.totalRequests }
            let cacheHits = imageMetrics.values.reduce(0) { $0 + $1.cacheHits }
            let failures = imageMetrics.values.reduce(0) { $0 + $1.failures }
            
            let memorySize = imageCache.memoryStorage.totalCost
            let diskSize = try? imageCache.diskStorage.totalSize() ?? 0
            
            return ImageCacheStatistics(
                totalRequests: totalRequests,
                cacheHits: cacheHits,
                failures: failures,
                memoryCacheSize: memorySize,
                diskCacheSize: diskSize ?? 0
            )
        }
    }
    
    func clearCache() {
        imageCache.clearCache()
        metricsQueue.async { [weak self] in
            self?.imageMetrics.removeAll()
        }
    }
}

struct SmartImageProcessor: ImageProcessor {
    let identifier = "SmartImageProcessor"
    
    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        switch item {
        case .image(let image):
            return processImage(image)
        case .data(let data):
            guard let image = KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) else {
                return nil
            }
            return processImage(image)
        }
    }
    
    private func processImage(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
        // Intelligent image processing based on device capabilities
        let screenScale = UIScreen.main.scale
        let maxDimension = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * screenScale
        
        let imageSize = image.size
        let maxImageDimension = max(imageSize.width, imageSize.height)
        
        if maxImageDimension > maxDimension {
            let scale = maxDimension / maxImageDimension
            let newSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
            
            return image.kf.resize(to: newSize, for: .aspectFit)
        }
        
        return image
    }
}

struct SmartCacheSerializer: CacheSerializer {
    func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
        // Smart compression based on image characteristics
        if let originalData = original, originalData.count < 50 * 1024 { // < 50KB
            return originalData // Keep small images as-is
        }
        
        // Use appropriate compression for larger images
        return image.kf.jpegRepresentation(compressionQuality: 0.8)
    }
    
    func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
    }
}

class ImageMetrics {
    let url: String
    private(set) var totalRequests = 0
    private(set) var cacheHits = 0
    private(set) var failures = 0
    private(set) var lastRequestTime = Date()
    
    init(url: String) {
        self.url = url
    }
    
    func recordRequest() {
        totalRequests += 1
        lastRequestTime = Date()
    }
    
    func recordSuccess(cacheType: CacheType) {
        if cacheType == .memory || cacheType == .disk {
            cacheHits += 1
        }
    }
    
    func recordFailure(error: KingfisherError) {
        failures += 1
    }
}

struct ImageCacheStatistics {
    let totalRequests: Int
    let cacheHits: Int
    let failures: Int
    let memoryCacheSize: Int
    let diskCacheSize: UInt
    
    var cacheHitRatio: Double {
        return totalRequests > 0 ? Double(cacheHits) / Double(totalRequests) : 0
    }
    
    var failureRatio: Double {
        return totalRequests > 0 ? Double(failures) / Double(totalRequests) : 0
    }
}

Performance Monitoring ​

Cache Analytics ​

dart
// Flutter - Comprehensive Cache Analytics
class CacheAnalyticsManager {
  static final Map<String, CacheMetrics> _cacheMetrics = {};
  static final List<CacheEvent> _events = [];
  static Timer? _reportingTimer;
  
  static void initialize() {
    _reportingTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
      _generatePerformanceReport();
    });
  }
  
  static void recordCacheHit(String cacheType, String key, int sizeBytes) {
    final metrics = _getCacheMetrics(cacheType);
    metrics.recordHit(sizeBytes);
    
    _addEvent(CacheEvent(
      type: CacheEventType.hit,
      cacheType: cacheType,
      key: key,
      sizeBytes: sizeBytes,
      timestamp: DateTime.now(),
    ));
  }
  
  static void recordCacheMiss(String cacheType, String key) {
    final metrics = _getCacheMetrics(cacheType);
    metrics.recordMiss();
    
    _addEvent(CacheEvent(
      type: CacheEventType.miss,
      cacheType: cacheType,
      key: key,
      timestamp: DateTime.now(),
    ));
  }
  
  static void recordCacheEviction(String cacheType, String key, int sizeBytes, String reason) {
    final metrics = _getCacheMetrics(cacheType);
    metrics.recordEviction(sizeBytes);
    
    _addEvent(CacheEvent(
      type: CacheEventType.eviction,
      cacheType: cacheType,
      key: key,
      sizeBytes: sizeBytes,
      timestamp: DateTime.now(),
      metadata: {'reason': reason},
    ));
  }
  
  static CacheMetrics _getCacheMetrics(String cacheType) {
    return _cacheMetrics.putIfAbsent(cacheType, () => CacheMetrics(cacheType));
  }
  
  static void _addEvent(CacheEvent event) {
    _events.add(event);
    
    // Keep only recent events (last 1000)
    if (_events.length > 1000) {
      _events.removeAt(0);
    }
  }
  
  static Map<String, dynamic> _generatePerformanceReport() {
    final report = {
      'timestamp': DateTime.now().toIso8601String(),
      'cache_types': _cacheMetrics.map((type, metrics) => MapEntry(type, {
        'hit_ratio': metrics.hitRatio,
        'total_requests': metrics.totalRequests,
        'total_size_mb': metrics.totalSizeBytes / (1024 * 1024),
        'average_object_size_kb': metrics.averageObjectSize / 1024,
        'eviction_rate': metrics.evictionRate,
      })),
      'recent_events': _events.takeLast(100).map((e) => e.toMap()).toList(),
      'performance_summary': {
        'overall_hit_ratio': _calculateOverallHitRatio(),
        'memory_efficiency': _calculateMemoryEfficiency(),
        'cache_health_score': _calculateHealthScore(),
      },
    };
    
    print('Cache Performance Report: $report');
    return report;
  }
  
  static double _calculateOverallHitRatio() {
    int totalHits = 0;
    int totalRequests = 0;
    
    for (final metrics in _cacheMetrics.values) {
      totalHits += metrics.hits;
      totalRequests += metrics.totalRequests;
    }
    
    return totalRequests > 0 ? totalHits / totalRequests : 0;
  }
  
  static double _calculateMemoryEfficiency() {
    // Calculate bytes saved through caching
    int totalCachedBytes = 0;
    int totalHits = 0;
    
    for (final metrics in _cacheMetrics.values) {
      totalCachedBytes += metrics.totalSizeBytes;
      totalHits += metrics.hits;
    }
    
    return totalHits > 0 ? totalCachedBytes / totalHits : 0;
  }
  
  static double _calculateHealthScore() {
    final hitRatio = _calculateOverallHitRatio();
    final evictionRate = _calculateOverallEvictionRate();
    
    // Health score: high hit ratio and low eviction rate = good
    return (hitRatio * 0.7) + ((1 - evictionRate) * 0.3);
  }
  
  static double _calculateOverallEvictionRate() {
    int totalEvictions = 0;
    int totalRequests = 0;
    
    for (final metrics in _cacheMetrics.values) {
      totalEvictions += metrics.evictions;
      totalRequests += metrics.totalRequests;
    }
    
    return totalRequests > 0 ? totalEvictions / totalRequests : 0;
  }
  
  static void dispose() {
    _reportingTimer?.cancel();
    _cacheMetrics.clear();
    _events.clear();
  }
}

class CacheMetrics {
  final String cacheType;
  int hits = 0;
  int misses = 0;
  int evictions = 0;
  int totalSizeBytes = 0;
  final List<int> objectSizes = [];
  
  CacheMetrics(this.cacheType);
  
  void recordHit(int sizeBytes) {
    hits++;
    if (!objectSizes.contains(sizeBytes)) {
      objectSizes.add(sizeBytes);
      totalSizeBytes += sizeBytes;
    }
  }
  
  void recordMiss() {
    misses++;
  }
  
  void recordEviction(int sizeBytes) {
    evictions++;
    totalSizeBytes -= sizeBytes;
    objectSizes.remove(sizeBytes);
  }
  
  int get totalRequests => hits + misses;
  double get hitRatio => totalRequests > 0 ? hits / totalRequests : 0;
  double get evictionRate => totalRequests > 0 ? evictions / totalRequests : 0;
  double get averageObjectSize => objectSizes.isNotEmpty 
      ? objectSizes.reduce((a, b) => a + b) / objectSizes.length 
      : 0;
}

class CacheEvent {
  final CacheEventType type;
  final String cacheType;
  final String key;
  final int? sizeBytes;
  final DateTime timestamp;
  final Map<String, dynamic>? metadata;
  
  CacheEvent({
    required this.type,
    required this.cacheType,
    required this.key,
    this.sizeBytes,
    required this.timestamp,
    this.metadata,
  });
  
  Map<String, dynamic> toMap() {
    return {
      'type': type.toString(),
      'cache_type': cacheType,
      'key': key,
      'size_bytes': sizeBytes,
      'timestamp': timestamp.toIso8601String(),
      'metadata': metadata,
    };
  }
}

enum CacheEventType {
  hit,
  miss,
  eviction,
  cleanup,
}

Best Practices ​

Memory Management Integration ​

  1. Garbage Collection Awareness: Monitor GC events and adapt cache size
  2. Memory Pressure Handling: Implement proactive cleanup on memory warnings
  3. Lifecycle Integration: Clear caches appropriately during app lifecycle events
  4. Thread Safety: Use concurrent data structures and proper synchronization
  5. Metrics Collection: Track hit ratios, memory usage, and performance metrics

Platform-Specific Optimizations ​

  • Android: Use LruCache, monitor memory class, handle configuration changes
  • iOS: Leverage NSCache automatic management, respond to memory warnings
  • Flutter: Implement custom solutions with platform channel optimizations
  • React Native: Combine native caching with JavaScript bridge efficiency

This comprehensive memory caching strategy ensures optimal performance while maintaining memory efficiency across all mobile platforms.

Created by Eren Demir.