iOS - YYCache探究

275 阅读16分钟

YYCache

  • YYCache.h

简介

' YYCache '是一个线程安全的键值缓存。它使用“YYMemoryCache”将对象存储在一个小而快的内存缓存中,并使用“YYDiskCache”将对象持久化到大而慢的磁盘缓存中。更多信息请参见“YYMemoryCache”和“YYDiskCache”

@interface YYCache : NSObject

缓存名字、内存缓存、磁盘缓存

/** 缓存的名称,只读。 */
@property (copy, readonly) NSString *name;

/** 底层内存缓存。更多信息请参见“YYMemoryCache”。*/
@property (strong, readonly) YYMemoryCache *memoryCache;

/** 底层磁盘缓存。有关更多信息,请参见“YYDiskCache”。*/
@property (strong, readonly) YYDiskCache *diskCache;

4个构造方法。两个name、两个path

/**
用指定的名称创建一个新实例。
具有相同名称的多个实例将使缓存不稳定。

@param name缓存名称。它将在创建一个名称为name的字典
在该应用程序的缓存字典的磁盘缓存。一旦初始化,就不应该这样做
读写此目录。
@result一个新的缓存对象,如果出现错误则为nil。
 */
- (nullable instancetype)initWithName:(NSString *)name;

/**
使用指定的路径创建一个新实例。
具有相同名称的多个实例将使缓存不稳定。

@param path缓存要写入的目录的全路径。
一旦初始化,就不应该读写这个目录。
@result一个新的缓存对象,如果出现错误则为nil。
 */
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;

/**
方便的初始化
用指定的名称创建一个新实例。
具有相同名称的多个实例将使缓存不稳定。

@param name缓存名称。它将创建一个名称为in的字典
该应用程序的缓存字典的磁盘缓存。一旦初始化,就不应该这样做
读写此目录。
@result一个新的缓存对象,如果出现错误则为nil。
 */
+ (nullable instancetype)cacheWithName:(NSString *)name;

/**
方便的初始化
使用指定的路径创建一个新实例。
具有相同名称的多个实例将使缓存不稳定。

@param path缓存要写入的目录的全路径。
一旦初始化,就不应该读写这个目录。
@result一个新的缓存对象,如果出现错误则为nil。
 */
+ (nullable instancetype)cacheWithPath:(NSString *)path;

> UNAVAILABLE_ATTRIBUTE 个人理解为不可使用的构造方法

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;

判断是否有Key这个键值、一个带有Block的判断可以用来做操作

#pragma mark - Access Methods

/**
返回一个布尔值,该值指示给定键是否在缓存中。
此方法可能阻塞调用线程,直到文件读取完成。

@param key标识值的字符串。如果为nil,就返回NO。
@return键是否在缓存中。
 */
- (BOOL)containsObjectForKey:(NSString *)key;

/**
返回一个布尔值,该值带有指示给定键是否在缓存中的块。
该方法立即返回并在后台队列中调用传递的block
当操作结束。

@param key标识值的字符串。如果为nil,就返回NO。
当block结束时,将在后台队列中被调用。
 */
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;

根据键值取值、带有Block的键值取值

/**
返回与给定键关联的值。
此方法可能阻塞调用线程,直到文件读取完成。

@param key标识值的字符串。如果为nil,就返回nil。
@return与key关联的值,如果没有值与key关联,则为nil。
 */
- (nullable id<NSCoding>)objectForKey:(NSString *)key;

/**
返回与给定键关联的值。
该方法立即返回并在后台队列中调用传递的block
当操作结束。

@param key标识值的字符串。如果为nil,就返回nil。
当block结束时,将在后台队列中被调用。
 */
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;

键值设值、带有block的键值设值

/**
设置缓存中指定键的值。
此方法可能阻塞调用线程,直到文件写入完成。

@param object存储在缓存中的对象。如果为nil,它调用' removeObjectForKey: '。
@param key与值关联的键。如果为空,则此方法无效。
 */
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;

/**
设置缓存中指定键的值。
该方法立即返回并在后台队列中调用传递的block
当操作结束。

@param object存储在缓存中的对象。如果为nil,它调用' removeObjectForKey: '。
当block结束时,将在后台队列中被调用。
 */
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;

根据键值删除值、带有block的键值删除值

/**
删除缓存中指定键的值。
此方法可能阻塞调用线程,直到文件删除完成。

@param key标识要删除的值的键。如果为空,则此方法无效。
 */
- (void)removeObjectForKey:(NSString *)key;

/**
删除缓存中指定键的值。
该方法立即返回并在后台队列中调用传递的block
当操作结束。

@param key标识要删除的值的键。如果为空,则此方法无效。
当block结束时,将在后台队列中被调用。
 */
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;

清空缓存、带有block的清空、带有进度的清空、

/**
清空缓存。
此方法可能阻塞调用线程,直到文件删除完成。
 */
- (void)removeAllObjects;

/**
清空缓存。
该方法立即返回并在后台队列中调用传递的block
当操作结束。

当block结束时,将在后台队列中被调用。
 */
- (void)removeAllObjectsWithBlock:(void(^)(void))block;

/**
用block清空缓存。
该方法立即返回并在后台执行block清除操作。

你不应该在这些块中发送消息给这个实例。
这个block将在移除过程中被调用,传递nil给忽略。
@param end这个block将在最后被调用,传递nil给ignore。
 */
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;

@end

YYCache主要提供了一系列操作缓存的方法,主要分为初始化、查找、存、取、删除几种。下面分别简单分析一下:

1.初始化

//提示 使用其他两种方法去创建
- (instancetype) init {
    NSLog(@"Use \"initWithName\" or \"initWithPath\" to create YYCache instance.");
    return [self initWithPath:@""];
}

//文件名name拼接在沙盒cache目录下面,作为YYCache创建缓存的路径,然后调用initWithPath:方法:
- (instancetype)initWithName:(NSString *)name {
    if (name.length == 0) return nil;
    NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *path = [cacheFolder stringByAppendingPathComponent:name];
    return [self initWithPath:path];
}
//该方法根据路径path创建硬盘缓存对象diskCache,然后根据目录名称创建内存缓存对象memoryCache。
- (instancetype)initWithPath:(NSString *)path {
    if (path.length == 0) return nil;
    YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
    if (!diskCache) return nil;
    NSString *name = [path lastPathComponent];
    YYMemoryCache *memoryCache = [YYMemoryCache new];
    memoryCache.name = name;
    
    self = [super init];
    _name = name;
    _diskCache = diskCache;
    _memoryCache = memoryCache;
    return self;
}

2.查找

containsObjectForKey:方法判断内存或者硬盘中是否存在key对应的数据,代码如下:
可能存在数据只在内存或者硬盘中出现的情况,所以逻辑上是或的关系。

- (BOOL)containsObjectForKey:(NSString *)key {
    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {
    if (!block) return;
    
    if ([_memoryCache containsObjectForKey:key]) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, YES);
        });
    } else  {
        [_diskCache containsObjectForKey:key withBlock:block];
    }
}

3.取

objectForKey:方法首先从内存中取数据,如果取不到,再从硬盘中取数据,并且把取到的数据存进内存中。 由于从硬盘中取需要I/O操作,所以内存中发现有数据,直接返回的逻辑在性能上会提升。

- (id<NSCoding>)objectForKey:(NSString *)key {
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (!object) {
        object = [_diskCache objectForKey:key];
        if (object) {
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id<NSCoding> object))block {
    if (!block) return;
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (object) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            block(key, object);
        });
    } else {
        [_diskCache objectForKey:key withBlock:^(NSString *key, id<NSCoding> object) {
            if (object && ![_memoryCache objectForKey:key]) {
                [_memoryCache setObject:object forKey:key];
            }
            block(key, object);
        }];
    }
}

4.存

setObject: forKey:方法分别调用_diskCache和_memoryCache对象进行硬盘和内存的存储。

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void (^)(void))block {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key withBlock:block];
}

5.删除

removeObjectForKey:方法分别调用_memoryCache和_diskCache的删除方法分别从内存和硬盘中删除数据。

- (void)removeObjectForKey:(NSString *)key {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key withBlock:block];
}

- (void)removeAllObjects {
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjects];
}

- (void)removeAllObjectsWithBlock:(void(^)(void))block {
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjectsWithBlock:block];
}

- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
                                 endBlock:(void(^)(BOOL error))end {
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjectsWithProgressBlock:progress endBlock:end];
    
}

总结

YYCache类实际都是调用YYMemoryCache和YYDiskCache类的方法对创建的内存缓存和磁盘缓存做操作。

YYMemoryCache

YYMemoryCache是一个存储键值对的快速内存缓存。与NSDictionary相比,键被保留而不被复制。API和性能类似于“NSCache”,所有的方法都是线程安全的。

  • YYMemoryCache对象在以下几个方面与NSCache不同:

  • 它使用LRU (least-recent -used)来删除对象;NSCache驱逐的方法 是不确定的。

  • 可按成本、数量、年龄控制;NSCache的限制并不精确。

  • 可以配置为在接收内存时自动逐出对象,警告或应用程序进入后台。

  • YYMemoryCache中的“访问方法”的时间通常为常量时间(O(1))。 时间复杂度为O(1)
@interface YYMemoryCache : NSObject

成员变量

#pragma mark - Attribute

@property (nullable, copy) NSString *name; // 缓存的名称。默认是空。

@property (readonly) NSUInteger totalCount; //缓存中的对象数量(只读)

@property (readonly) NSUInteger totalCost; //缓存中对象的总成本(只读)。

#pragma mark - Limit

@property NSUInteger countLimit; //缓存应该容纳的最大对象数。默认值为NSUIntegerMax,即不限制,这不是一个严格的限制—如果缓存超过了限制,则缓存可以在后台线程中被逐出

@property NSUInteger costLimit; //缓存在开始清除对象之前所能持有的最大总开销。默认值为NSUIntegerMax,即不限制。这不是一个严格的限制—如果缓存超过了限制,则缓存可以在后台线程中被逐出。

@property NSTimeInterval ageLimit; //缓存中对象的最大过期时间。默认值为DBL_MAX,即不限制。这并不是一个严格的限制—如果一个对象超过了这个限制,那么这个对象就可能超过这个限制稍后在后台线程中被逐出。

-------------------------------------------------------
/**
自动修剪检查时间间隔以秒为单位。默认是5.0。

缓存持有一个内部定时器来检查缓存是否到达
它的极限,如果达到极限,它就开始驱逐物体。
 */
@property NSTimeInterval autoTrimInterval;

/**
如果是,当应用程序收到内存警告时,缓存将删除所有对象。
默认值为“YES”。
 */
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;

/**
如果“是”,缓存将删除所有对象时,应用程序进入背景。
默认值为“YES”。 app退到后台就清理缓存
 */
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
----------------------------------------------------------
/**
当应用程序收到内存警告时执行的块。
默认值为nil。
 */
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

/**
当应用程序进入后台时要执行的块。
默认值为nil。
 */
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
----------------------------------------------------------
/**
如果' YES ',键值对将在主线程上被释放,否则为
后台线程。默认是否定的。

如果key-value对象包含,可以将该值设置为YES
在主线程中应该被释放的实例(如UIView/CALayer)。
 */
@property BOOL releaseOnMainThread;

/**
如果' YES ',键值对将被异步释放,以避免阻塞
访问方法,否则将在访问方法中释放
(如removeObjectForKey:)。默认是肯定的。
 */
@property BOOL releaseAsynchronously;

Access Methods 方法列表

- (BOOL)containsObjectForKey:(id)key; //返回一个布尔值,该值指示给定键是否在缓存中,key标识值的对象。如果是nil,就返回' NO ',键是否在缓存中.

- (nullable id)objectForKey:(id)key; //返回与给定键关联的值。key标识值的对象。如果为nil,就返回nil

- (void)setObject:(nullable id)object forKey:(id)key; //设置缓存中指定键的值,存储在缓存中的对象。如果为nil,它调用' removeObjectForKey: key与值关联的键。如果为空,则此方法无效,与NSMutableDictionary对象不同,缓存不复制键
放入其中的对象。

- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost; //设置缓存中指定键的值,并关联键值与指定的成本配对。object存储在缓存中的对象。如果为nil,它调用' removeObjectForKey 'key与值关联的键。如果为空,则此方法无效。cost键值对关联的开销。与NSMutableDictionary对象不同,缓存不复制键放入其中的对象。

- (void)removeObjectForKey:(id)key; //删除缓存中指定键的值。key标识要删除的值的键。如果为空,则此方法无效

- (void)removeAllObjects; //立即清空缓存。

Trim

- (void)trimToCount:(NSUInteger)count; // 用 LRU 算法删除对象,直到 totalCount <= count

- (void)trimToCost:(NSUInteger)cost; // 用 LRU 算法删除对象,直到 totalCost <= cost

- (void)trimToAge:(NSTimeInterval)age; // 用 LRU 算法删除对象,直到所有到期对象全部被删除

YYMemoryCache内部维护了一个_YYLinkMap对象,_YYLinkMap对象负责实现缓存和LRU的功能。下面是代码注释:

image.png

YYLinkedMapNode

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; //前向前一个节点的指针
    __unsafe_unretained _YYLinkedMapNode *_next; //指向下一个节点的指针
    id _key; //缓存数据key
    id _value; //缓存数据value
    NSUInteger _cost; //节点占用大小
    NSTimeInterval _time; //节点操作时间戳
}
@end

YYLinkedMap

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; //哈希字典,存放缓存数据
    NSUInteger _totalCost; //缓存总大小
    NSUInteger _totalCount; //缓存节点总个数
    _YYLinkedMapNode *_head; //头结点
    _YYLinkedMapNode *_tail; //尾结点
    BOOL _releaseOnMainThread; //在主线程释放
    BOOL _releaseAsynchronously;//在异步线程释放
}
@end
  • _dic是一个哈希字典,负责放缓存数据
  • _head和_tail分别是双向链表中指向头节点和尾节点的指针
  • 链表中的节点单元是_YYLinkedMapNode对象,该对象封装了缓存数据的信息

_YYLinkedMap对象的主要方法

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node; //在头插入一个节点并更新总链表。Node和node.key不应为nil。

- (void)bringNodeToHead:(_YYLinkedMapNode *)node; //将内部节点带到标头。节点应该已经在dic中。

- (void)removeNode:(_YYLinkedMapNode *)node; //删除一个内部节点并更新总成本。节点应该已经在dic中。

- (_YYLinkedMapNode *)removeTailNode; //删除尾节点(如果存在)。

- (void)removeAll; //在后台队列中删除所有节点。
  • 1.init
- (instancetype)init {
    self = [super init];
    _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); //初始化一个可变字典
    _releaseOnMainThread = NO; //不在主线程中释放
    _releaseAsynchronously = YES; //异步子线程释放
    return self;
}
  • CFMutableDictionaryRef的使用和我们常用的NSMutableDictionary是非常相似的,唯一要注意的一点就是由于CFMutableDictionaryRef使用的C语言接口,因此我们需要将对象类型进行桥接,转换为C类型。 参考链接 这个暂时先了解一下 就理解为一个能提高性能的可变字典吧相比于Foundation框架来说这样做可以提高框架的运行速度。
CFMutableDictionaryRef myDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSString *key = @"someKey";
NSNumber *value = [NSNumber numberWithInt: 1];
//set
CFDictionarySetValue(myDict, (__bridge void *)key, (__bridge void *)value);
//get
id dictValue = (__bridge id)CFDictionaryGetValue(myDict, (__bridge void *)key);
//remove
CFDictionaryRemoveValue(myDict, (__bridge void *)key);
  • 2.dealloc
- (void)dealloc {
    CFRelease(_dic); //清理字典
}
  • 3.insertNodeAtHead:
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); //存入字典中
    _totalCost += node->_cost; //更新总大小
    _totalCount++; //更新总数
    if (_head) { //节点置于链表头部
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
        _head = _tail = node;
    }
}
  • 4.bringNodeToHead:方法

该方法将节点移动至链表的头部,因为调用该方法的场景是节点已经存在于字典中,所以不需要新加入字典中。

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return;
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}
  • 5.removeNode方法和removeTailNode方法

removeNode方法将数据节点从字典和链表中删除,同时更新总大小_totalCost和节点总个数_totalCount。removeTailNode方法将链表中的尾部节点删除,同时从字典中删除节点。

  • 6.removeAll方法

该方法删除链表中所有节点,同时从字典中删除所有节点。但是发现并没有释放掉这个字典而是释放了一个复制了字典的临时变量,同时将字典置空

YYMemoryCache

成员变量
pthread_mutex_t _lock;
_YYLinkedMap *_lru;
dispatch_queue_t _queue;
  • _lock;互斥锁
  • _lru; Map 管理节点 用来做数据缓存,实现lru算法来管理缓存。
  • _queue; 队列

- public

  • 1.init 初始化
- (instancetype)init {
    self = super.init;
    pthread_mutex_init(&_lock, NULL); //初始化互斥锁
    _lru = [_YYLinkedMap new]; //初始化这个管理者
    _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL); //自定义一个后台串行队列
    
    _countLimit = NSUIntegerMax; //最大数不限制
    _costLimit = NSUIntegerMax; //最大数不限制
    _ageLimit = DBL_MAX; //double最大数不限制
    _autoTrimInterval = 5.0; //时间5秒
    _shouldRemoveAllObjectsOnMemoryWarning = YES; //内存警告后移除所有
    _shouldRemoveAllObjectsWhenEnteringBackground = YES; //退到后台移除所有
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; //添加两个通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
     
    [self _trimRecursively]; //进行边界检测
    return self;
}
  • 2.dealloc 主要做移除通知 清理缓存 清除锁
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
    [_lru removeAll];
    pthread_mutex_destroy(&_lock);
}
  • 3.几个Set和Get方法 都是先枷锁完成后解锁 保证线程安全
- (NSUInteger)totalCount
- (NSUInteger)totalCost
- (BOOL)releaseOnMainThread
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread
- (BOOL)releaseAsynchronously
- (void)setReleaseAsynchronously:(BOOL)releaseAsynchronously
  • 4.判断字典中是否有这个Key-Value 的值
- (BOOL)containsObjectForKey:(id)key {
    if (!key) return NO;
    pthread_mutex_lock(&_lock);
    BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
    pthread_mutex_unlock(&_lock);
    return contains;
}
  • 5.取值 该方法从字典中获取缓存数据,如果key对应的数据存在,则更新访问时间,根据最近最少使用原则,将本次操作的节点移动至链表的头部。如果不存在,则直接返回nil。
- (id)objectForKey:(id)key {
    if (!key) return nil; //key不为空否则返回
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //取出这个节点
    if (node) {
        node->_time = CACurrentMediaTime(); //更新这个node的time属性为当前的时间
        [_lru bringNodeToHead:node]; //将这个节点移动到头节点
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil; //返回node的Value值 否则为 nil
}

  • 6.存储数据
    • 首先判断key和object是否为空,object如果为空,删除缓存中key对应的数据。
    • 然后从字典中查找key对应的缓存数据,分为两种情况,如果访问到节点,说明缓存数据存在,则根据最近最少使用原则,将本次操作的节点移动至链表的头部,同时更新节点的访问时间。
    • 如果访问不到节点,说明是第一次添加key和数据,需要创建一个新的节点,把节点存入字典中,并且加入链表头部。cost是指定的,默认是0。

调用setObject: forKey:方法存储缓存数据,代码如下:

- (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对应的缓存数据
         //更新totalCost 
         _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; //设置节点的key
        node->_value = object; //设置节点中存放的缓存数据
        [_lru insertNodeAtHead:node]; //将新的节点加入链表头部
    }
    //当我们没有对这个_costLimit做设置则是最大值不走这个方法
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
    //当我们没有对这个_countLimit做设置则是最大值不走这个方法
    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在子线程中释放,减少主线程开销
                [node class]; //hold and release in queue 等待并释放
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
            //异步发送一个消息,以保证node在子线程中释放,减少主线程开销
                [node class]; //hold and release in queue
            });
        }
    }

    pthread_mutex_unlock(&_lock); //解锁
}
  • 7.移除值
- (void)removeObjectForKey:(id)key {
    if (!key) return;
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        [_lru removeNode:node];
        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)removeAllObjects {
    pthread_mutex_lock(&_lock);
    [_lru removeAll];
    pthread_mutex_unlock(&_lock);
}

Trim 方法 LRU算法 缓存的管理

- (void)_trimRecursively
- (void)_trimInBackground
- (void)_trimToCost:(NSUInteger)costLimit
- (void)_trimToCount:(NSUInteger)countLimit
- (void)_trimToAge:(NSTimeInterval)ageLimit
1.边界检测

YYCache通过LRU算法处理缓存数据是否超出容量的情况。首先在初始化时,调用_trimRecursively方法,通过dispatch_after方法默认每隔5秒重新调用。下面是代码注释: 可以理解为创建了内存缓存后每隔五秒就会检测一下缓存数据有没有超出边界

- (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];
    });
}

- _trimInBackground分别调用_trimToCost、_trimToCount和_trimToAge方法检测。
- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}
2._trimToCost
  • _trimToCost方法判断链表中所有节点占用大小之和totalCost是否大于costLimit,cost就是我们在上面的加入 - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost 的时候设置的cost 当我们调用这个方法 - (void)setObject:(id)object forKey:(id)key 则是0,当我们有自定义cost会在我们加入的时候更新总数量并记录的,删除也是
  • 如果超过,则从链表末尾开始删除节点,直到totalCost小于等于costLimit为止。代码注释如下:
- (void)_trimToCost:(NSUInteger)costLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll]; //总量0移除全部缓存
        finish = YES; //yes则不会走到下面while循环
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        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 以微秒为单位挂起线程,在短时间间隔挂起线程
            // 对比 sleep 用 usleep 能更好的利用 CPU 时间
            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
        });
    }
}
3._trimToCount
- (void)_trimToCount:(NSUInteger)countLimit {
    BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (countLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCount <= countLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCount > countLimit) {
                _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
        });
    }
}