解码YYCache

☞源码: YYCache

☞ 作者:YYCache 设计思路

☞ 阅读代码的注释:github.com/wenghengcon…

YYCache

YYCache 是由内存缓存 YYMemoryCache 与磁盘缓存 YYDiskCache 组成的Cache矩阵,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。

在 YYCache 中使用接口访问缓存对象时,会先去尝试

  • 从内存缓存 YYMemoryCache 中访问
    • 命中:之前访问过,且仍然在内存中,任何击中对象。
    • 未命中-> YYDiskCache:之前未使用过该key缓存的对象,或者由于各种限制驱逐了该缓存对象。
      • 命中:任何击中对象,更新到内存缓存。
      • 未命中:缓存到磁盘中。
@interface YYCache : NSObject

@property (copy, readonly) NSString *name;
@property (strong, readonly) YYMemoryCache *memoryCache;
@property (strong, readonly) YYDiskCache *diskCache;

- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;

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

- (BOOL)containsObjectForKey:(NSString *)key;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;

- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
.....
    
@end
复制代码

缓存策略

LRU,即LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

YYCache中,有两种实现:

内存缓存中,采用双链表的实现:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当需要清除缓存时,将链表尾部的数据丢弃。

在磁盘缓存中,采用了SQLite的实现:

  1. 新数据插入到缓存表;
  2. 每当缓存命中(即缓存数据被访问),则更新该缓存对应的表中数据行对应的访问时间
  3. 当需要清除缓存时,根据访问时间将最旧的条目删除;

内存缓存

YYMemoryCache

YYMemoryCache 是一个高速的内存缓存,用于存储键值对。它与 NSDictionary 相反,Key 被保留并且不复制。API 和性能类似于 NSCache,所有方法都是线程安全的。

它是通过双链表实现LRU算法、**CFMutableDictionaryRef**** 实现快速存取、****pthread_mutex_lock**来保证线程安全。

与 NSCache 的不同之处在于:

YYMemoryCache NSCache
驱逐 LRU(least-recently-used) 算法来驱逐对象 非确定性
控制 age、cost、count 不精确的
异常 内存警告或者 App 进入后台时自动驱逐对象

API

优雅简洁的API:

@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;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit;
@property NSTimeInterval autoTrimInterval;

@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;

#pragma mark - Access Methods
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;


#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;

@end

@implementation YYMemoryCache {
    pthread_mutex_t _lock;  //互斥锁,保证线程安全,对于所有的属性和方法
    _YYLinkedMap *_lru;     //_YYLinkedMap 处理层类。处理链表操作,操作缓存对象
    dispatch_queue_t _queue;    //串行队列,用于用于 YYMemoryCache 的 trim 操作
}
复制代码

LRU缓存策略

LRU缓存策略的实现,依靠双链表_YYLinkedMap,_YYLinkedMapNode是链表节点。策略是:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当需要清除缓存时,将链表尾部的数据丢弃。
@interface _YYLinkedMapNode : NSObject {
    @package
    //__unsafe_unretained:和__weak 一样,唯一的区别便是,对象即使被销毁,指针也不会自动置空, 此时指针指向的是一个无用的野地址。如果使用此指针,程序会抛出 BAD_ACCESS 的异常。
    // __unsafe_unretained 在于性能优化,节点被 _YYLinkedMap 的 _dic 强引用
    __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;
}
复制代码

链表的操作

 (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        //设置nil,就移除对应的Node
        [self removeObjectForKey:key];
        return;
    }
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
        _lru->_totalCost -= node->_cost;    //移除掉老的cost
        _lru->_totalCost += cost;           //增加新的cost
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];        //将访问过的,放在表头
    } else {
        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,等待在该队列上释放
                [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)removeObjectForKey:(id)key {
    if (!key) return;
    pthread_mutex_lock(&_lock);
    
    //将要移除的node
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    
    if (node) {
        //移除node
        [_lru removeNode:node];
        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 class]; //异步释放
            });
        }
    }
    pthread_mutex_unlock(&_lock);
}
.....
复制代码

驱逐缓存对象

驱逐对象有三个维度:cost、count、age,分别代表每个对象的消耗、对象的总个数、对象的过期时间

调用

#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
复制代码

以Count为例:

- (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) {
                // 从双线链表的尾节点(LRU)开始清理缓存,释放资源
                _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]; // 异步释放
        });
    }
}
复制代码

定时

- (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];
    });
}
复制代码

上边的代码中,每隔_autoTrimInterval时间就会在后台尝试处理数据,然后再次调用自身,这样就实现了一个类似定时器的功能。

异步释放

关于上面的异步释放缓存对象的代码,需要着重说明:

dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
    [holder count]; // release in queue
});
复制代码

这个技巧 ibireme 在他的另一篇文章 iOS 保持界面流畅的技巧 中有提及:

Note: 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

而上面代码中的 YYMemoryCacheGetReleaseQueue 这个队列为:

// 静态内联 dispatch_queue_t
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
复制代码

在源码中可以看到 YYMemoryCacheGetReleaseQueue 是一个低优先级 DISPATCH_QUEUE_PRIORITY_LOW队列,可以让 iOS 在系统相对空闲时再来异步释放缓存对象。

  1. 让block来retain一下对象?

应该是holder在执行完这个方法后就出了作用域了,reference会减1,但是此时holder不会被dealloc,因为block 中retain了node,使得holder的reference count为1,当执完block后,holder的reference count又-1,此时node就会在block对应的queue上release了。
2. 为什么需要在指定queue销毁对象?
放到其他线程去销毁对象,减轻当前线程压力,一般都是减轻main thread的压力

快速访问

YYMemoryCache之所以能做到高速命中缓存,采用的还是NSDictionary的方式,但是其采用的是CFMutableDictionaryRef而不是NSMutableDictionary,有两个考虑:

  • CFMutableDictionaryRef性能更优;
  • NSMutableDictionary 中的键是被拷贝的并且需要是不变的。如果在一个键在被用于在字典中放入一个值后被改变的话,那么这个值就会变得无法获取了。一个有趣的细节是,在  中键是被 copy 的,但是在使用一个 toll-free 桥接的 CFMutableDictionaryRef 时却只会被 retain。

CFMutableDictionaryRef的用法

//create
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, 
                                 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//setter
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//getter
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
//contains
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
//remove
CFDictionaryRemoveValue(\_dic, (__bridge const void *)(node->_key));
复制代码

线程安全

在内存缓存中,用pthread_mutex_t来保证线程安全,具体可参考文档

在读写缓存时,先加锁,再解锁:

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { 
    pthread_mutex_lock(&_lock); 
    //写缓存数据 
    pthread_mutex_unlock(&_lock); 
} 
- (id)objectForKey:(id)key { 
    pthread_mutex_lock(&_lock); 
    //读缓存数据 
    pthread_mutex_unlock(&_lock); 
}
复制代码

但是在几种驱逐缓存对象时,却采用pthread_mutex_trylock来进行加锁,两者的区别在于:

pthread_mutex_trylock     behaves      identically      to  pthread_mutex_lock,  except  that  it  does  not block the calling thread if the mutex is already locked  by  another thread (or by the calling thread in the case of a “fast”  mutex). Instead, pthread_mutex_trylock returns immediately with the error code EBUSY.
From www.skrenta.com/rt/man/pthr…

// 加锁(阻塞操作)
pthread_mutex_lock(pthread_mutex_t *mutex);
// 试图加锁(不阻塞操作)
// 当互斥锁空闲时将占有该锁;否则立即返回
// 但是与pthread_mutex_lock不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
pthread_mutex_trylock(pthread_mutex_t *mutex);

为什么要这样做?

因为读写缓存,一定要保证线程安全,必须阻塞,但是删减缓存对象时,为了提高性能,将这种低优先级定时的操作进行尝试加锁,以避免因线程加锁带来的损耗。

磁盘缓存

YYDiskCache

YYDiskCache 是一个线程安全的磁盘缓存,用于存储由 SQLite 和文件系统支持的键值对。

SQLite数据库表来实现LRU策略,dispatch_semaphore保证线程安全,通过SQLite的高性能和SQL语句缓存_dbStmtCache(是CFMutableDictionaryRef类),并且通过阈值来决定存储域文件系统还是SQLite,来保证快速存取。

其中对于数据大小的磁盘缓存选择:

  • sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
  • file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。

初始化方法

//.h
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;

//.m
- (instancetype)init {
    @throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
    return [self initWithPath:@"" inlineThreshold:0];
}
复制代码

LRU缓存策略

LRU的实现:

  1. 新数据插入到缓存表;
  2. 每当缓存命中(即缓存数据被访问),则更新该缓存对应的表中数据行对应的访问时间
  3. 当需要清除缓存时,根据访问时间将最旧的条目删除;

YYKVStorage

YYKVStorage 是基于 sqlite 和文件系统的键值存储。通常情况下,我们不应该直接使用这个类。  这个类的实例是 线程安全的,你需要确保 只有一个线程可以同时访问该实例。如果你真的需要在多线程中处理大量的数据,应该分割数据 到多个KVStorage 实例(分片)。

/**
 存储类型,指示“YYKVStorageItem.value”存储在哪里。
 
 @discussion
  通常,将数据写入 sqlite 比外部文件更快,但是
  读取性能取决于数据大小。在我的测试(环境 iPhone 6 64G),
  当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。
 */
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统
    YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite
    YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储
};

@interface YYKVStorage : NSObject

#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path;        ///< The path of this storage.
@property (nonatomic, readonly) YYKVStorageType type;  ///< The type of this storage.
@property (nonatomic) BOOL errorLogsEnabled;           ///< Set `YES` to enable error logs for debug.

#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;


#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
- (BOOL)saveItemWithKey:(NSString *)key
                  value:(NSData *)value
               filename:(nullable NSString *)filename
           extendedData:(nullable NSData *)extendedData;

#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
- (BOOL)removeItemsLargerThanSize:(int)size;
.....
- (BOOL)removeAllItems;
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                               endBlock:(nullable void(^)(BOOL error))end;


#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
- (nullable NSData *)getItemValueForKey:(NSString *)key;
.....
#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;

@end
复制代码

YYKVStorageItem

这个类对应就是存储在SQLite表中的数据:

说明 YYKVStorageItem SQLite
key key key
value,以二进制数据保存 value inline_data
文件系统存储对应的文件名 filename filename
大小 size size
修改时间(在这里为修改时间) modTime modification_time
访问时间 accessTime last_access_time
扩展数据 extendedData extended_data

存储到文件还是SQLite

YYDiskCache对于是存储到文件系统,还是SQLite,有一个阈值20K,是作者测试出得到的一个阈值。

  • 小于该阈值,采用SQLite访问速度更快;
  • 大于该阈值,采用文件系统访问速度更快;

该阈值在初始化时指定:

- (instancetype)initWithPath:(NSString *)path {
    return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

/**
 threshold
    0:使用文件存储
    NSUIntegerMax 则采用sqlite
 */
 - (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
        YYKVStorageType type;
        if (threshold == 0) {
            type = YYKVStorageTypeFile;
        } else if (threshold == NSUIntegerMax) {
            type = YYKVStorageTypeSQLite;
        } else {
            type = YYKVStorageTypeMixed;
        }
        .....     
 }
复制代码

所以,假如不指定阈值或者介于0与NSUIntegerMax,YYKVStorage存储类型均为YYKVStorageTypeMixed。即采用根据阈值来进行混合存储。

但我们通过源码发现,就算YYKVStorage存储类型为YYKVStorageTypeFile,还是会在SQLite存储对应的条目,因为我们需要实现LRU驱逐缓存对象。

YYKVStorage存储类型均为YYKVStorageTypeSQLite,此时将不再在文件系统存储对应的对象。

YYKVStorageTypeSQLite * SQLite * SQLite * SQLite
YYKVStorageTypeFile _ 文件系统
_ SQLite * SQLite _ 文件系统
_ SQLite
YYKVStorageTypeMixed _ 文件系统
_ SQLite * SQLite _ 文件系统
_ SQLite

/**
 存储数据

 @param key key
 @param value 值
 @param filename 存储的文件名
 @param extendedData 扩展数据
 */
- (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;
    }
    //有文件名,直接以默认文件及sqlite两种方式存储
    if (filename.length) {
        //文件存储失败直接返回
        if (![self _fileWriteWithName:filename data:value]) {
            return NO;
        }
        //sqlite存储失败,删除本地文件存储,然后返回
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        //文件名为空时,默认按sqlite方式存储,并且检查当前如果不是仅仅以sqlite存储(文件存储)时,需要删除该key对应的文件名
        if (_type != YYKVStorageTypeSQLite) {
            //如果当前存储方式,不是sqlite,那么需要找出文件名,并且如果找到文件名,删除文件之后存储到sqlite
            NSString *filename = [self _dbGetFilenameWithKey:key];
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}
复制代码

- (BOOL)removeItemsToFitCount:(int)maxCount {
    if (maxCount == INT_MAX) return YES;
    if (maxCount <= 0) return [self removeAllItems];
    
    int total = [self _dbGetTotalItemCount];
    if (total < 0) return NO;
    if (total <= maxCount) return YES;
    
    NSArray *items = nil;
    BOOL suc = NO;
    do {
        int perCount = 16;
        items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
        for (YYKVStorageItem *item in items) {
            if (total > maxCount) {
                if (item.filename) {
                    [self _fileDeleteWithName:item.filename];
                }
                suc = [self _dbDeleteItemWithKey:item.key];
                total--;
            } else {
                break;
            }
            if (!suc) break;
        }
    } while (total > maxCount && items.count > 0 && suc);
    if (suc) [self _dbCheckpoint];
    return suc;
}
复制代码

/**
 该方法能获取到key对应的所有数据
 包括value(来源于sqlite,当存在文件时,从文件读取)
 */
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    //excludeInlineData 获取是否包含inline data,就是key对应的value数据
    //NO,即包含value数据
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
    if (item) {
        //更新访问时间,用于删除时判断是否最近访问的数据
        [self _dbUpdateAccessTimeWithKey:key];
        
        if (item.filename) {
            //如果也有文件存储了,那么从文件系统读取value
            item.value = [self _fileReadWithName:item.filename];
            
            if (!item.value) {
                //若是item的数据为空,直接删除sqlite的数据
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}
复制代码

NSMapTable

NSMapTable 是类似于字典的集合,但具有更广泛的可用内存语义。NSMapTable 是 iOS6 之后引入的类,它基于 NSDictionary 建模,但是具有以下差异:

  • 键/值可以选择 “weakly” 持有,以便于在回收其中一个对象时删除对应条目。
  • 它可以包含任意指针(其内容不被约束为对象)。
  • 您可以将 NSMapTable 实例配置为对任意指针进行操作,而不仅仅是对象。

Note: 配置映射表时,请注意,只有 NSMapTableOptions 中列出的选项才能保证其余的 API 能够正常工作,包括复制,归档和快速枚举。 虽然其他 NSPointerFunctions 选项用于某些配置,例如持有任意指针,但并不是所有选项的组合都有效。使用某些组合,NSMapTableOptions 可能无法正常工作,甚至可能无法正确初始化。

更多信息详见 NSMapTable 官方文档

每一个YYDiskCache实例存储于一个全局的NSMapTabel类的_globalInstances实例中,以达到快速访问:

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    self = [super init];
    if (!self) return nil;
    
    //假如,已经在该path有cache对象,直接返回
    YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
    if (globalCache) return globalCache;
    
    //否则,创建一个cache对象,
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;

    _YYDiskCacheSetGlobal(self);
    
    return self;
}

/// weak reference for all instances
// 存储所有弱引用的Cache对象,并通过key来获取
static NSMapTable *_globalInstances;
static dispatch_semaphore_t _globalInstancesLock;

static void _YYDiskCacheInitGlobal() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _globalInstancesLock = dispatch_semaphore_create(1);
        //maptable 就是一个 dictionary
        // 区别是:dictionary 的key 是 copy语义,value是 retain语义
        // maptable key、value可以自定义
        _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    });
}

static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
    if (path.length == 0) return nil;
    _YYDiskCacheInitGlobal();
    // 信号量存取
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    id cache = [_globalInstances objectForKey:path];
    dispatch_semaphore_signal(_globalInstancesLock);
    return cache;
}

static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
    if (cache.path.length == 0) return;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    [_globalInstances setObject:cache forKey:cache.path];
    dispatch_semaphore_signal(_globalInstancesLock);
}
复制代码

线程安全

YYKVStore本身不支持线程安全,所以不推荐直接使用。

但是YYDiskCache支持线程安全,其通过信号量dispatch_semaphore来实现互斥锁。

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

磁盘缓存中两处使用了信号量的互斥锁:

一处上面说的用_globalInstances中获取存储的YYDiskCache的实例对象,需要进行互斥读写:

dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
// or [_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
复制代码

另外一处,是在_kv所有对item进行操作(包括读、写、删、查),并且 定义的两个宏来方便使用:

#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
复制代码

架构

分层

YYCache有优秀的分层设计。

性能

其实性能这个东西是隐而不见的,又是到处可见的(笑)。它从我们最开始设计一个缓存架构时就被带入,一直到我们具体的实现细节中慢慢成形,最后成为了我们设计出来的缓存优秀与否的决定性因素。

上文中剖析了太多 YYCache 中对于性能提升的实现细节:

  • 异步释放缓存对象
  • 锁的选择
  • 使用 NSMapTable 单例管理的 YYDiskCache
  • YYKVStorage 中的 _dbStmtCache
  • 使用 CoreFoundation 来换取微乎其微的性能提升

锁的选择

  • 磁盘缓存采用dispatch_semaphore、内存缓存采用pthread_mutex

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

  • 不采用OSSpinLock

OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
不再安全的 OSSpinLock
自从 OSSpinLock 被公布不安全,第三方库纷纷用pthread_mutex 替换 OSSpinLock。
ibireme 在确认 OSSpinLock 不再安全之后为了寻找替代方案做的简单性能测试,对比了一下几种能够替代 OSSpinLock 锁的性能。在 不再安全的 OSSpinLock 文末的评论中,我找到了作者使用 pthread_mutex 的原因。

ibireme: 苹果员工说 libobjc 里 spinlock 是用了一些私有方法 (mach_thread_switch),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。在我的一些测试中,OSSpinLockdispatch_semaphore 都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore 代替 OSSpinLock 是否正确。能够肯定的是,用 pthread_mutex 是安全的。

参考

  1. Cache replacement policies
  2. 从 YYCache 源码 Get 到如何设计一个优秀的缓存
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享