代码目录 屏幕快照 2018-03-08 10.25.03

YYCache

对外暴露的对象,核心函数

  • 初始对象 核心参数是一个磁盘的缓存路径,作者也提供了横向代理,通过一个唯一标识的名称,默认创建一个文件路径。(学习点:尽可能的提供更加方便的函数,增强框架体验)

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

  • (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; } ```

  • 通过Key判断是否缓存了对象
  • 作者提供了2个种方式,直接回调结果和异步回调结果(学习点:尽量考虑到使用者可能遇到的开发情况)
 Returns a boolean value that indicates whether a given key is in cache.
 This method may blocks the calling thread until file read finished.
 
 @param key A string identifying the value. If nil, just return NO.
 @return Whether the key is in cache.
 */
- (BOOL)containsObjectForKey:(NSString *)key;

/**
 Returns a boolean value with the block that indicates whether a given key is in cache.
 This method returns immediately and invoke the passed block in background queue
 when the operation finished.
 
 @param key   A string identifying the value. If nil, just return NO.
 @param block A block which will be invoked in background queue when finished.
 */
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
  • 通过Key值获取到缓存对象(学习点:在需要的情况下,尽量用泛型指明你需要的参数,或者返回的结果)
 Returns the value associated with a given key.
 This method may blocks the calling thread until file read finished.
 
 @param key A string identifying the value. If nil, just return nil.
 @return The value associated with key, or nil if no value is associated with key.
 */
- (nullable id<NSCoding>)objectForKey:(NSString *)key;

/**
 Returns the value associated with a given key.
 This method returns immediately and invoke the passed block in background queue
 when the operation finished.
 
 @param key A string identifying the value. If nil, just return nil.
 @param block A block which will be invoked in background queue when finished.
 */
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
  • 缓存对象(学习点:对象是否可以nil,尽量表明,swift调用的话,你懂的)
 Sets the value of the specified key in the cache.
 This method may blocks the calling thread until file write finished.
 
 @param object The object to be stored in the cache. If nil, it calls `removeObjectForKey:`.
 @param key    The key with which to associate the value. If nil, this method has no effect.
 */
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;

/**
 Sets the value of the specified key in the cache.
 This method returns immediately and invoke the passed block in background queue
 when the operation finished.
 
 @param object The object to be stored in the cache. If nil, it calls `removeObjectForKey:`.
 @param block  A block which will be invoked in background queue when finished.
 */
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
  • 删除对象或者清空缓存
 Removes the value of the specified key in the cache.
 This method may blocks the calling thread until file delete finished.
 
 @param key The key identifying the value to be removed. If nil, this method has no effect.
 */
- (void)removeObjectForKey:(NSString *)key;

/**
 Removes the value of the specified key in the cache.
 This method returns immediately and invoke the passed block in background queue
 when the operation finished.
 
 @param key The key identifying the value to be removed. If nil, this method has no effect.
 @param block  A block which will be invoked in background queue when finished.
 */
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;

/**
 Empties the cache.
 This method may blocks the calling thread until file delete finished.
 */
- (void)removeAllObjects;

/**
 Empties the cache.
 This method returns immediately and invoke the passed block in background queue
 when the operation finished.
 
 @param block  A block which will be invoked in background queue when finished.
 */
- (void)removeAllObjectsWithBlock:(void(^)(void))block;

/**
 Empties the cache with block.
 This method returns immediately and executes the clear operation with block in background.
 
 @warning You should not send message to this instance in these blocks.
 @param progress This block will be invoked during removing, pass nil to ignore.
 @param end      This block will be invoked at the end, pass nil to ignore.
 */
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;

YYMemoryCache

YYMemoryCache is a fast in-memory cache that stores key-value pairs. In contrast to NSDictionary, keys are retained and not copied. The API and performance is similar to NSCache, all methods are thread-safe.

YYMemoryCache objects differ from NSCache in a few ways:

  • It uses LRU (least-recently-used) to remove objects; NSCache’s eviction method is non-deterministic.
  • It can be controlled by cost, count and age; NSCache’s limits are imprecise.
  • It can be configured to automatically evict objects when receive memory warning or app enter background.

The time of Access Methods in YYMemoryCache is typically in constant time (O(1)).

YYCache的内存管理类,函数API设计都类似系统类NSCache,并且线程安全。 通过LRU算法管理内存对象,在收到内存警告或者进入后台,可以配置是否自动清理对象。

参数

  • @property NSUInteger countLimit; 数量限制
  • @property NSUInteger costLimit;可以容纳的最大值,如果是NSUIntegerMax则不限制
  • @property NSTimeInterval ageLimit;最大对象缓存过期时间,如果是DBL_MAX则不限制
  • @property NSTimeInterval autoTrimInterval;自动检测的时间点,默认5.0
  • @property BOOL shouldRemoveAllObjectsOnMemoryWarning; @property BOOL shouldRemoveAllObjectsWhenEnteringBackground;配置是否收到内存警告和进入后台,清除内存数据
  • @property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); @property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);内存警告和进入后台回调块
  • @property BOOL releaseOnMainThread; @property BOOL releaseAsynchronously;配置是否在主线程析构,还是异步析构。默认是异步

    函数

    在分析函数前,先看下YYMemoryCache的私有的内部类_YYLinkedMap_YYLinkedMapNode

    _YYLinkedMapNode

    屏幕快照 2018-03-08 14.26.04

  • prev和next:前一个和后一个节点,不增加引用系数
  • key和value:键值对
  • cost和time:用于LRU计算

    _YYLinkedMap

    屏幕快照 2018-03-08 14.37.23 这个Map我有个想法,能不能不暴露CFMutableDictionaryRef,而提供更加多的功能接口让YYMemoryCache使用,我发现在YYMemoryCache大量使用CFMutableDictionaryRef的API,如果后续我要换个其他缓存方式,改动是不是会有点大呢?

    参数
  • CFMutableDictionaryRef:存储的字典
  • _head 和 _tail:头尾节点
  • _totalCost 和 _totalCount:LRU、MRU算法用
  • _releaseOnMainThread 和 _releaseAsynchronously:哪个线程析构
    函数

    * - (void)insertNodeAtHead:(_YYLinkedMapNode *)node; 将一个节点插入到头部

```- (void)insertNodeAtHead:(_YYLinkedMapNode *)node { CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); 【1】 _totalCost += node->_cost;
_totalCount++; if (_head) { 【2】 node->_next = _head; _head->_prev = node; _head = node; } else { _head = _tail = node; } }

1.把节点的key 和 value 传入到字典里面,这里把`id`类型转换到 `__bridge const void *`,键、值的ARC管理,由`_YYLinkedMapNode`来决定
2.判断当前Map是否有头节点,如果有`把当前头节点`赋值到`新的头节点的Next`

* `- (void)bringNodeToHead:(_YYLinkedMapNode *)node` :把一个节点移动到头,内部实现也主要是对节点的pre和next进行一些切换
* `- (void)removeNode:(_YYLinkedMapNode *)node; 、- (_YYLinkedMapNode *)removeTailNode;、- (void)removeAll;`:节点的删除和Map的清空,再清空的时候,析构`CFMutableDictionaryRef`,会按照配置决定是否在主线程还是其他线程

### YYMemoryCache函数
* 构造器:
    1.初始化`pthread_mutex_t`,先前版本作者用的是`OSSpinLock`,具体为什么选择`pthread_mutex_t`,作者也给出了答案[https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/](不再安全的 OSSpinLock)
    2.`_YYLinkedMap`初始化,用于管理键值对
    3.创建了一个新的串行队列(com.ibireme.cache.memory),看源码的时候,这个队列只在2个地方使用。1.私有内部函数:`- (void)_trimInBackground`  2.`- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost` 发现cost不够的时候,异步到队列去执行清除工作。为什么要这样,查看`- (void)_trimToCost:(NSUInteger)costLimit`
    4.其他基本参数的赋值
    
* `- (void)_trimToCost:(NSUInteger)costLimit`:清除部分对象达到目标要求,在清除内存对象有一定的耗时,所以执行这个函数的时候,放到了异步队列中。
  
``` - (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) {
        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 【1】
        }
    }
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{  【2】
            [holder count]; // release in queue
        });
    }
}

学习点:1.降低CPU负担 2.放到异步线程去释放https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/ 作者在他的博客中也提到了这个点

  • - (id)objectForKey:(id)key:通过Key获取内存对象,内部还处理了一些逻辑,比如把节点移动到头部
  • - (void)removeObjectForKey:(id)key- (void)removeAllObjects:内存对象的删除和清空
  • - (void)trimToCount:(NSUInteger)count- (void)trimToCost:(NSUInteger)cost- (void)trimToAge:(NSTimeInterval)age其他私有函数,一些按照规定大小和数量的清除策略。

YYDiskCache

  • #define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER) #define Unlock() dispatch_semaphore_signal(self->_lock) 磁盘锁作者选择的是信号量

    引用作者原话,在选锁的见解,现在版本只是把自旋锁OSSpinLock改成了pthread_mutex,学习点:技术的专研精神,很多同学对于锁可能就知道一个NSLock,甚至面试的时候,有些同学都没用过锁。

    ```OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。

    dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。 ```

  • @property (nullable, copy) NSData *(^customArchiveBlock)(id object); @property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); 自定义的归档块,如果是自定义,对象可以不实现NSCoding
  • 如果没有实现自定义归档方法,存储对象必须实现NSCoding,通过系统NSKeyedArchiver转换为NSData
  • 属性的话大部分都有内存缓存差不多,用于算法策略。
  • 核心函数也是增/删等
  • 有2个类方法 + (nullable NSData *)getExtendedDataFromObject:(id)object; + (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object; 可以将扩展信息缓存起来
  • 定时循环清除数据 - (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]; }); } 感觉和当时写Android的时候用Handler循环很像…
  • 数据清除策略和内存一样LRU

```- (void)trimToCount:(NSUInteger)count;

  • (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;
  • (void)trimToCost:(NSUInteger)cost;
  • (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;
  • (void)trimToAge:(NSTimeInterval)age;
  • (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block; ```

YYKVStorage

YYKVStorage 内部类YYKVStorageItem

屏幕快照 2018-03-08 16.46.23

  • 其他参数都了解,fileName这个参数,因为YYKVStorage再存在磁盘的时候有2种方式:YYKVStorageTypeFileYYKVStorageTypeSQLite,如果对象数据大于20KB文件读取速度会大于数据库,如果用户没有指定,默认是混合模式。如果数据存储的方式是数据库,则这个属性为nil

存储过程

```- (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;
    }
    if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) { -----------------【1】
        [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];
} } ``` 【1】对于扩展数据,还是存储到数据库的。我的疑问:为什么不用NSKeyedArchiver,将对象数据和扩展数据混合存入文件,难道有隐患?

```- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { NSString *sql = @”insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);”; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO;

int timestamp = (int)time(NULL);
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 3, (int)value.length);
if (fileName.length == 0) {  -------------【1】
    sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
    sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
sqlite3_bind_int(stmt, 5, timestamp);
sqlite3_bind_int(stmt, 6, timestamp);
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);

int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
    if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
    return NO;
}
return YES; } ``` 【1】如果有文件存在,则不会把对象数据写入数据库

读取过程类似和其他函数也是策略相关功能,查看源码一看便知