谈谈在iOS中使用的缓存

4,629 阅读12分钟

缓存

NSCache

iOS中系统提供的缓存就是NSCache还有NSURLCache,但NSURLCache的使用则局限于只是针对于网络请求,所以这里指对NSCache展开讨论

常用的淘汰算法:

  • FIFO(First In First Out):先进先出。判断被存储的时间,离目前最远的数据优先被淘汰
  • LRU(Least Recently Used):最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰
  • LFU(Least Frequently Used):最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰

NSCache的释放

NSCache释放取决于countLimitremoveapp进入后台内存警告

countLimit


@interface CacheIOP : NSObject<NSCacheDelegate>

@end

@interface ViewController ()

@property (nonatomic, strong) NSCache *cache;

@property (nonatomic, strong) CacheIOP *cacheIOP;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.cacheIOP = [CacheIOP new];
    self.cache = [NSCache new];
    self.cache.delegate = self.cacheIOP;
    self.cache.countLimit = 5;
    for (int i = 1; i <= 10; i++) {
        [self.cache setObject:[NSString stringWithFormat:@"CacheObject-%d", i] forKey:[NSString stringWithFormat:@"id = %d", i]];
    }
    [self getCache];
}

- (void)getCache
{
    for (int i = 1; i <= 10; i++) {
        NSLog(@"%@", [self.cache objectForKey:[NSString stringWithFormat:@"id = %d", i]]);
    }
}

@implementation CacheIOP

-(void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"objc:%@ will evict by cache:%@", obj, cache);
}

@end

image.png

设置countLimit后添加到NSCache的对象超过了限制,则会将之前的数据进行驱逐

app进入后台

image.png

image.png

可以看到进入后台时会驱逐所有的对象,同时会调用[NSCache removeAllObjects]其底层符号为cache_remove_all

内存警告⚠️

在模拟器上模拟内存警告 image.png

第一次内存警告后

image.png

第二次内存警告后

image.png

第三次内存警告后将全部清除

image.png

GNUstep 解析 NSCache

现在通过GNUstep来分析NSCache

image.png

从这里分析进入方法setObject:forKey:cost:,这里默认是cost为0

@interface _GSCachedObject : NSObject
{
  @public
  id object;
  NSString *key;
  int accessCount;//LFU  Least Frequently Used
  NSUInteger cost;
  BOOL isEvictable;
}

这里对象都会使用NSMaptable来存取被包装为_GSCachedObject的对象,同时accessCount的作用就是来实现LFU的淘汰策略

- (id) objectForKey: (id)key
{
  _GSCachedObject *obj = [_objects objectForKey: key];

  if (nil == obj)
    {
      return nil;
    }
  if (obj->isEvictable)
    {
      // Move the object to the end of the access list.
      [_accesses removeObjectIdenticalTo: obj];
      [_accesses addObject: obj];
    }
  obj->accessCount++;
  _totalAccesses++;
  return obj->object;
}

由上可以看到每当在访问存储在缓存中对象的时候,accessCount会被操作

- (void) setObject: (id)obj forKey: (id)key cost: (NSUInteger)num
{
  _GSCachedObject *oldObject = [_objects objectForKey: key];
  _GSCachedObject *newObject;

  // 如果key存储了别的对象,那么要将之前对象清除
  if (nil != oldObject)
    {
      [self removeObjectForKey: oldObject->key];
    }
    // 根据LRU+LFU的淘汰策略来修剪缓存
  [self _evictObjectsToMakeSpaceForObjectWithCost: num];
  newObject = [_GSCachedObject new];
  // Retained here, released when obj is dealloc'd
  newObject->object = RETAIN(obj);
  newObject->key = RETAIN(key);
  newObject->cost = num;
  if ([obj conformsToProtocol: @protocol(NSDiscardableContent)])
    {
      newObject->isEvictable = YES;
      [_accesses addObject: newObject];
    }
  [_objects setObject: newObject forKey: key];
  RELEASE(newObject);
  _totalCost += num;
}

下述方法是处理驱逐策略的方法。该实现使用相对简单的LRU/LFU混合。来自AppleNSCache文档清楚地说明了策略可能会改变。

- (void)_evictObjectsToMakeSpaceForObjectWithCost: (NSUInteger)cost
{
  NSUInteger spaceNeeded = 0;
  NSUInteger count = [_objects count];

  // 根据总消耗和消耗的阈值来计算需要清理的消耗值
  if (_costLimit > 0 && _totalCost + cost > _costLimit)
    {
      spaceNeeded = _totalCost + cost - _costLimit;
    }

  // Only evict if we need the space.
    // 只有需要清理消耗或者数量达到阈值时才进行修剪
  if (count > 0 && (spaceNeeded > 0 || count >= _countLimit))
    {
      NSMutableArray *evictedKeys = nil;
      // Round up slightly.
      // 根据二八原则计算出平均访问次数
      NSUInteger averageAccesses = ((_totalAccesses / (double)count) * 0.2) + 1;
      // 根据LRU排序过的对象数组
      NSEnumerator *e = [_accesses objectEnumerator];
      _GSCachedObject *obj;

      if (_evictsObjectsWithDiscardedContent)
	{
	  evictedKeys = [[NSMutableArray alloc] init];
	}
      // 通过循环不断删除符合LRU+LFU修剪的对象
      while (nil != (obj = [e nextObject]))
	{
	  // Don't evict frequently accessed objects.
	  if (obj->accessCount < averageAccesses && obj->isEvictable)
	    {
	      [obj->object discardContentIfPossible];
	      if ([obj->object isContentDiscarded])
		{
		  NSUInteger cost = obj->cost;

		  // Evicted objects have no cost.
		  obj->cost = 0;
		  // Don't try evicting this again in future; it's gone already.
		  obj->isEvictable = NO;
		  // Remove this object as well as its contents if required
		  if (_evictsObjectsWithDiscardedContent)
		    {
		      [evictedKeys addObject: obj->key];
		    }
		  _totalCost -= cost;
		  // If we've freed enough space, give up
		  if (cost > spaceNeeded)
		    {
		      break;
		    }
		  spaceNeeded -= cost;
		}
	    }
	}
      // Evict all of the objects whose content we have discarded if required
      if (_evictsObjectsWithDiscardedContent)
	{
	  NSString *key;

	  e = [evictedKeys objectEnumerator];
	  while (nil != (key = [e nextObject]))
	    {
	      [self removeObjectForKey: key];
	    }
	}
    [evictedKeys release];
    }
}

所以在GNUstep中的NSCache修剪内存的策略是LRU+LFU的混合策略

通过SwiftFoundation来分析NSCache

image.png

同样也是根据set方法来找到分析点,这里同上述一样

open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
    
    private var _entries = Dictionary<NSCacheKey, NSCacheEntry<KeyType, ObjectType>>()
    private let _lock = NSLock()
    private var _totalCost = 0
    private var _head: NSCacheEntry<KeyType, ObjectType>?
    
    open var name: String = ""
    open var totalCostLimit: Int = 0 // limits are imprecise/not strict
    open var countLimit: Int = 0 // limits are imprecise/not strict
    open var evictsObjectsWithDiscardedContent: Bool = false
}

首先可以看到NSCache中实际使用字典来存储,但是keyvalue分别是NSCacheKeyNSCacheEntry类型

fileprivate class NSCacheKey: NSObject {
    
    var value: AnyObject
    
    init(_ value: AnyObject) {
        self.value = value
        super.init()
    }
    
    override var hash: Int {
        switch self.value {
        case let nsObject as NSObject:
            return nsObject.hashValue
        case let hashable as AnyHashable:
            return hashable.hashValue
        default: return 0
        }
    }
    
    override func isEqual(_ object: Any?) -> Bool {
        guard let other = (object as? NSCacheKey) else { return false }
        
        if self.value === other.value {
            return true
        } else {
            guard let left = self.value as? NSObject,
                let right = other.value as? NSObject else { return false }
            
            return left.isEqual(right)
        }
    }
}

NSCacheKey作为包装key的类,其中重写了hashisEqual函数

private class NSCacheEntry<KeyType : AnyObject, ObjectType : AnyObject> {
    var key: KeyType
    var value: ObjectType
    var cost: Int
    var prevByCost: NSCacheEntry?
    var nextByCost: NSCacheEntry?
    init(key: KeyType, value: ObjectType, cost: Int) {
        self.key = key
        self.value = value
        self.cost = cost
    }
}

从上处可以看出NSCache使用了双向链表结构

open func setObject(_ obj: ObjectType, forKey key: KeyType, cost g: Int) {
        let g = max(g, 0)
        let keyRef = NSCacheKey(key)
        
        _lock.lock()
        
        let costDiff: Int
        
        if let entry = _entries[keyRef] {// 当前cache中存在此键值对,删除旧值插入新值
            costDiff = g - entry.cost
            entry.cost = g
            
            entry.value = obj
            
            if costDiff != 0 {
                remove(entry)
                insert(entry)
            }
        } else {// cache中没有此键值对,插入对象
            
            let entry = NSCacheEntry(key: key, value: obj, cost: g)
            _entries[keyRef] = entry
            insert(entry)
            
            costDiff = g
        }
        
        _totalCost += costDiff
        
        // totalCostLimit 根据此进行内存裁剪,并且是从头结点开始
        var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0
        while purgeAmount > 0 {
            if let entry = _head {
                delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
                
                _totalCost -= entry.cost
                purgeAmount -= entry.cost
                
                remove(entry) // _head will be changed to next entry in remove(_:)
                _entries[NSCacheKey(entry.key)] = nil
            } else {
                break
            }
        }
        // countLimit 根据此进行内存裁剪,同样也是从头结点开始
        var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0
        while purgeCount > 0 {
            if let entry = _head {
                delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
                
                _totalCost -= entry.cost
                purgeCount -= 1
                
                remove(entry) // _head will be changed to next entry in remove(_:)
                _entries[NSCacheKey(entry.key)] = nil
            } else {
                break
            }
        }
        
        _lock.unlock()
    }
/**
      插入节点时的三种情况:
       1.cache为空直接将插入对象作为头结点
       2.cache不为空,若插入节点比头结点的耗费小,将插入节点设置为头结点
       3.cache不为空,若插入节点比头结点耗费大,找到第一个不比插入节点耗费小的节点进行插入,例如插入节点为5,目前链表为1->3->6->8,则插入后为1->3->5->6->8
     */
    private func insert(_ entry: NSCacheEntry<KeyType, ObjectType>) {
        // cache为空的话当做头结点
        guard var currentElement = _head else {
            // The cache is empty
            entry.prevByCost = nil
            entry.nextByCost = nil
            
            _head = entry
            return
        }
        // 头结点有值,但是插入节点的耗费比头结点耗费小,直接置为头结点
        guard entry.cost > currentElement.cost else {
            // Insert entry at the head
            entry.prevByCost = nil
            entry.nextByCost = currentElement
            currentElement.prevByCost = entry
            
            _head = entry
            return
        }
        // 头结点有值,找到第一个不比插入节点耗费小的节点进行插入
        while let nextByCost = currentElement.nextByCost, nextByCost.cost < entry.cost {
            currentElement = nextByCost
        }
        
        // Insert entry between currentElement and nextElement
        let nextElement = currentElement.nextByCost
        
        currentElement.nextByCost = entry
        entry.prevByCost = currentElement
        
        entry.nextByCost = nextElement
        nextElement?.prevByCost = entry
    }
/**
        移除节点
     */
    private func remove(_ entry: NSCacheEntry<KeyType, ObjectType>) {
        let oldPrev = entry.prevByCost
        let oldNext = entry.nextByCost
        
        oldPrev?.nextByCost = oldNext
        oldNext?.prevByCost = oldPrev
        
        if entry === _head {
            _head = oldNext
        }
    }

从上可以看到,NSCache是优先根据totalCostLimit进行裁剪,其次是根据countLimit,同样文档中写到这两个指标都不是严格精确的,而且这两种裁剪方法可能只会执行其中一个也可能两个都执行,这些都是不能确定的

总结:

NSCache的缺点:

  • 驱逐方式是不确定的
  • 内存修剪是不精确的
  • 数据可能会被自动释放

NSURLCache

根据NSURLCache官方文档可以知道NSURLCache是采用内存、磁盘双缓存策略,磁盘缓存的路径是Library/Caches,并且是以数据库来存储的

image.png

接下来通过打开数据库来看看,其中图片便被存放在这里

image.png

NSURLCache存放在沙盒路径下的磁盘缓存其中存放:

  • request
  • response
  • receive_data

其中控制缓存的重要字段是Cache-Control:

  • max-age:缓存时间
  • public:谁都可以缓存
  • private:只有客户端缓存,中间代理无法缓存
  • no-cache:服务端进行确认
  • no-store:禁止使用缓存

通过NSURLSessionConfiguration设置缓存策略:

  • NSURLRequestUseProtocolCachePolicy:指定现有的缓存数据应该用来满足URL加载请求,不管它存在多久或过期日期。然而,如果没有缓存中与URL加载请求对应的现有数据,URL从服务器加载
  • NSURLRequestReloadIgnoringLocalCacheData:指定URL加载的数据应该从服务器加载
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不仅要忽略本地缓存数据,还要忽略代理的缓存
  • NSURLRequestReloadIgnoringCacheData:NSURLRequestReloadIgnoringLocalCacheData的旧名称
  • NSURLRequestReturnCacheDataElseLoad:有缓存则使用缓存,没有就从服务器请求
  • NSURLRequestReturnCacheDataDontLoad:只使用cache数据,如果不存在缓存,请求失败;用于没有建立网络连接离线模式
  • NSURLRequestReloadRevalidatingCacheData:是有现有的缓存前必须与服务器确认其有效性,否则就要从服务器获取

通过下面这段代码可以看出使用ETag或者lastModified如果服务器发现请求的资源并没有发生改变,那么会返回304意思可以直接使用缓存数据

NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/50x50.jpg"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
    
    if (self.lastModified) {
        [request setValue:self.lastModified forHTTPHeaderField:@"If-Modified-Since"];
    }
    if (self.etag) {
        [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
    }
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"error warning : %@",error);
        } else {
            NSData *tempData = data;
            NSString *responseStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
            self.lastModified = [(NSHTTPURLResponse *)response allHeaderFields][@"Last-Modified"];
            self.etag = [(NSHTTPURLResponse *)response allHeaderFields][@"Etag"];
            NSLog(@"response:%@", response);
        }
    }] resume];

image.png

SDWebImage中的NSURLCache

定位到SDWebImageDownloaderOperationstart方法中

if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
                self.response = cachedResponse.response;
            }
        }

这里可以看到如果设置了忽略缓存的选项的话,同样需要检查如果有NSURLCache存在的话是需要将缓存数据读取出来

image.png

通过作者的文档标注来看,框架内对于NSURLCache的使用地方就在于SDWebImageDownloaderUseNSURLCacheSDWebImageDownloaderIgnoreCachedResponse,那么接下来的分析主要是要集中于这两个option

image.png

如果设置了使用NSURLCache的话请求中的缓存策略使用默认的,如果没有设置则是使用NSURLRequestReloadIgnoringLocalCacheData,主要就是为了防止重复缓存

image.png

这是在将要缓存数据的时候,如果没有设置使用NSURLCache则将缓存直接置空,那么就缓存不到数据了

image.png

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error方法中,这里是网络请求已经回调了,此时发现设置了忽略缓存并且缓存的图片和网络回来的是一样的,也就是与之前演示中304的效果一样,那么给到外面的回调是给一个错误,但这个并不是真的错误,其实就是标识304,如下

image.png

同样在- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler方法中也有

image.png

如果标记为304但是又没有缓存数据,这里就没有地方可以取数据了

总结:

默认的缓存策略:

  • 客户端发起一个请求

  • 检查本地的缓存:

             1. 如果没有过期则直接使用缓存数据
             2. 如果过期了,对比服务器资源,服务器返回`304`直接使用缓存,服务器返回`200 `就使用服务器返回的数据            
                         
    

YYCache

YYCache采取的缓存淘汰算法:LRU(Least Recently Used)最近最少使用

缓存淘汰策略的维度:

  • count
  • cost(20KB)
  • age(距离上一次访问的时间)

YYCache采取的缓存是内存缓存磁盘缓存的双缓存策略,另外对于磁盘缓存来讲是采用文件缓存和数据库缓存结合的方法

磁盘缓存中的区分点:

  • 数据大小大于临界值采用文件缓存
  • 数据大小小于临界值采用数据库缓存
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}

从这里可以看出是内存缓存和磁盘缓存双缓存的结构

内存缓存

YYMemoryCache就是管理内存缓存的类

YYMemoryCacheNSCache不同的地方在于以下几点:

  • 它使用LRU(最近最少使用)来删除对象,NSCache驱逐的方法是不确定的
  • 它使用costcountage来控制,NSCache的限制是不精确的
  • 它可以配置为当收到内存警告或应用程序进入后台时自动驱逐对象
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

YYMemoryCache内部使用双向链表结构,每次使用setObject:forKey:的方法最终会将对象存放于双向链表,并且会调整至表头节点,这就刚好符合LRU的淘汰策略,同样裁剪策略会针对于cost(耗费)以及count(数量)进行精确的裁剪,就是从链表的尾结点开始裁剪

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {// 如果之前存储过这个key,重新赋值后置为链表的头结点
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } else {//如果之前没有存储过这个key,重新创建一个节点进行赋值后置为头结点
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];
    }
    // 存储的总花费比限制的高时进行异步裁剪
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    // 另一个维度在数量上如果超过限制同样进行裁剪
    if (_lru->_totalCount > _countLimit) {
        _YYLinkedMapNode *node = [_lru removeTailNode];
        if (_lru->_releaseAsynchronously) {
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    pthread_mutex_unlock(&_lock);
}
- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        //pthread_mutex_trylock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCost > costLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            usleep(10 * 1000); //10 ms
        }
    }
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        // 可以在非主线程中释放对象,holder是个局部变量,如果不这样做只会在函数完成时在主线程内析构
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

同样,在YYMemoryCache对象存在的情况下会定时对内存缓存的对象从cost(耗费)count(数量)age(最后一次访问时间)进行检查和裁剪

- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        [self _trimInBackground];
        [self _trimRecursively];
    });
}

- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}
- (void)_trimToAge:(NSTimeInterval)ageLimit {
    BOOL finish = NO;
    NSTimeInterval now = CACurrentMediaTime();
    pthread_mutex_lock(&_lock);
    if (ageLimit <= 0) {
        [_lru removeAll];
        finish = YES;
    } else if (!_lru->_tail || (now - _lru->_tail->_time) <= ageLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) {
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            } else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        } else {
            usleep(10 * 1000); //10 ms
        }
    }
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            [holder count]; // release in queue
        });
    }
}

image.png

另外可以注意到这里使用的关键字是__unsafe_unretained,为什么不使用__weak?其实这里就是为了性能考虑,如果使用__weak会对weak表进行一定的操作,那么这里就会产生性能消耗

磁盘缓存

YYCache的磁盘缓存中YYDiskCache还需要将存入的对象归档后通过YYKVStorage的方法来存入数据库,存入数据库时也需要判断存入对象的大小,如果低于20KB则直接存入数据库的inline_data,如果大于20KB则使用写入文件的方式,同时数据库中filename就会存放写入文件的路径,这里就通过判断是否大于20KB来实现磁盘缓存中是使用文件写入的方式还是直接将对象存入数据库

存放的文件结构是以下这样:

File:
 /path/
      /manifest.sqlite
      /manifest.sqlite-shm
      /manifest.sqlite-wal
      /data/
           /e10adc3949ba59abbe56e057f20f883e
           /e10adc3949ba59abbe56e057f20f883e
      /trash/
            /unused_file_or_folder
 
 SQL:
 create table if not exists manifest (
    key                 text,
    filename            text,
    size                integer,
    inline_data         blob,
    modification_time   integer,
    last_access_time    integer,
    extended_data       blob,
    primary key(key)

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    
    NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
    NSData *value = nil;
    if (_customArchiveBlock) {//如果有自定义的归档方法
        value = _customArchiveBlock(object);
    } else {//使用默认的归档方法
        @try {
            value = [NSKeyedArchiver archivedDataWithRootObject:object];
        }
        @catch (NSException *exception) {
            // nothing to do...
        }
    }
    if (!value) return;
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) {
            //如果对象大于20kb,生成一个MD5的文件名
            filename = [self _filenameForKey:key];
        }
    }
    
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    
    if (filename.length) {
        //写入文件
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        // 存入数据库,这里要看是否存在filename,如果存在则inline_data为空,使用文件存取,否则对象存入inline_data
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

数据库和存放的文件如下图所示:

image.png

image.png