上一篇说了整体的 Manager 所做的事情。现在来有针对性的看看缓存策略是怎样的
SDImageCache
说到缓存,一般都会想到两级缓存:内存缓存和磁盘缓存。SDWebImage 也不例外,我们先开看一下初始化方法:
初始化方法
SDImageCache
是一个单例对象。它的初始化方法中会创建磁盘缓存对象 _diskCache
,和内存缓存对象 _memoryCache
:
1 | + (nonnull instancetype)sharedImageCache { |
可以看到,两个缓存的类型都是通过 config
获取的。config
默认的类型为 SDMemoryCache
和 SDDiskCache
。
SDMemoryCache 初始化
SDMemoryCache 初始化方法如下:
1 | - (void)commonInit { |
主要设置了缓存的最大值和最大数量。另外 weakCache
是一个令人注意的地方。它是一个 NSMapTable,它的作用会在后续使用的时候揭晓。
另外,totalCostLimit
和 countLimit
是 NSCache 的两个属性。SDMemoryCache
是 NSCache 的子类。
SDDiskCache 初始化
SDDiskCache
的初始化方法就简单了很多。就是获取一个 NSFileManager
1 | - (void)commonInit { |
入口方法
前篇说到,Manager 会通过 SDImageCache
实例,创建一个缓存的 operation:
1 | /// 进行缓存相关操作,把返回的 Operation 设置给 CacheOperation |
现在来到具体方法中:
1 | - (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { |
这里对于外部传来的 options 做了一些处理。然后传入内部的处理方法。
内部方法
内部方法分为两部分,从 Memory 中查找缓存以及从 Disk 中查找缓存。方法很长:
1 | - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { |
内存查找 image
内存查找方法直接从前面创建的 SDMemoryCache
对象 memoryCache
对象中获取:
1 | - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { |
SDMemoryCache
本身继承于 NSCache
,它有一套系统的缓存方式,会自动在某个时刻进行内存的释放。不过 SDWebImage 重写了 objectForKey
方法:
1 | - (id)objectForKey:(id)key { |
这里就用到了上面 SDMemoryCache
初始化的 NSMapTable
了。那么它的作用是什么呢?由于 NSCache 是系统维护回收状态的缓存,可能在任何时候回收。那么就可能出现 image 还存在,但是已经被 NSCache 回收的情况,容易产生因为内存紧张频繁释放内存导致频繁从磁盘加载图片的情况。
因此,在 NSCache 本身的缓存回收机制下,再设置一个弱引用的字典。它并不影响缓存的引用计数。但是如果 image 没有被回收,那么也不会像 NSCache 一样可能被回收。
类似的,在把 image 丢到 SDMemoryCache
中的时候需要设置到 NSMapTable
中:
1 | - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g { |
Disk 中查找 image
1 | - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { |
SDDiskCache
中的查找方法。这是一个比较简单明确的方法:
1 | - (NSData *)dataForKey:(NSString *)key { |
Disk 中的 NSData 转 UIImage
从 Disk 中拿到的是 NSData 对象。我们需要把它转为 UIImage:
1 | - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context { |
具体解码相关键后续文章。
返回 NSOperation
我们可以看到内部方法中通过 NSOperation *operation = [NSOperation new];
创建了一个 NSOperation 实例,然后就直接返回了。没有对这个 NSOperation 做任何其他的处理。唯一使用到的地方就是在 Disk 查询的时候判断是否已经取消。那么为什么要这样做呢?
因为获取缓存的方法中要根据 config 的不同,对缓存进行同步,或者异步的读取。即要进行 dispatch_sync
和 dispatch_async
的切换。这用一个 NSOperation 不方便完成。但是对于外部来说,外部希望有一个和 download 统一的获取缓存的 NSOperation 实例用于取消。因此,就在内部的 GCD 中加入了 NSOperation 的 cancel,来动态取消执行。
保存相关方法
按理说,保存应该放在下载模块,不过 SDWebImage 中将保存放在了 Cache 模块中。因此,也一起说完。
外部调用方法
下面就是 Manager 中会调用的缓存方法:
1 |
|
代码有点长,不过注释很清楚。这里的逻辑主要用于处理是否需要缓存原图以及是否需要对原图进行 transform 上。
内部存储方法
内部存储方法在 SDImageCache
中:
1 | - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { |
Manager 会把下载下来的 image 或者 data 传入。然后根据缓存策略进行内存或者磁盘缓存:
1 | - (void)storeImage:(nullable UIImage *)image |
写入磁盘的时候一直使用的是一个串行队列 ,这样防止了多线程情况下可能会产生的 crash:
1 | _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL); |
总结
总的来说,就是通过 SDImageCache
去管理内存缓存 SDMemoryCache
和磁盘缓存 SDDiskCache
缓存部分到此为止。