细读SDWebImage 源码- SDImageCache 篇
前言:本文章非快餐搬运,需要花点耐心阅读,因为是结合自己理解从Manager 开始一个方法一个方法去阅读解析,这个过程是痛苦的、但是读完之后是喜悦的,也希望指出个人理解不准确的地方,大家共同进步。
文章设定:较少篇幅的代码片段直接复制过来解析说明,篇幅较长的就上截图了,这样也方便各位观看。
本篇文章采用属性定位代码片段方法来探究SDImageCacheConfig 的一些配置属性具体作用,而不仅仅通过头文件的注释去理解
进入SDImageCache单列里面看:
![](https://img.haomeiwen.com/i3366589/67eb60bada8b55fd.png)
缓存单列可以看到 默认以 default为名称建立初始化路径
进入 makeDiskCachePath 看看:
![](https://img.haomeiwen.com/i3366589/9ddcd923d0f3fa0d.png)
可以看出 sd 是以 缓存路径,用户域名为空间 来开辟缓存路径(这个可以自己点进去看枚举解释,这里不过多说明了),只是这个路径下系统会在内存告急的情况下自动清除磁盘缓存。所以SD 做了一个弱引用表优化读取效率(后面会解释)。
我们回到 initWithNamespace这方法,继续向下阅读 initWithNamespace:
![](https://img.haomeiwen.com/i3366589/453c26321ad5da65.png)
这方法里面做的事情较多主要分为 初始化缓存配置、内存缓存、硬盘缓存,已经注册 2个通知 deleteOldFiles,backgroundDeleteOkdFiles
进入 SDImageCacheConfig里面看看。
看完SDImageCacheConfig 在进入 SDMemoryCache里面看看:
SDMemoryCache 是SDImageCache 的隐藏的类,外面看不到。,先看看其初始化的方法里面干了啥
![](https://img.haomeiwen.com/i3366589/34734a8defa175f8.png)
弱引用表就是针对cache数据在缓存一次,但是它仅仅是引用而不持有,所以不影响引用计数,当图片缓存被系统清除的时候,但是图片其实还是被某个UIImageView或者其他的实例持有的可以再次恢复,避免此时从磁盘里面读图片(I/O口耗性能)。
SDMemoryCache 就是继承 NSCache,里面重写了NSCache 的一些存/取对象的方法,主要是为了增加weakCache一层,随便找个例子看看:
// `setObject:forKey:` just call this with 0 cost. Override this is enough
- (void)setObject:(id)objforKey:(id)keycost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if(key && obj) {
// Store weak cache
LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];
UNLOCK(self.weakCacheLock);
}
}
可以看出 [super setObject:obj forKey:key cost:g ]; 调用父类缓存对象,下面接着判断是否有弱内存缓存标志,有的话就直接在二次缓存一下。 其他比如取数据接口基本类似。
特别注意一点就是
- (void)didReceiveMemoryWarning:(NSNotification*)notification {
// Only remove cache, but keep weak cache
[super removeAllObjects];
}
内存告急的情况下 只删除 NSCache 数据,保持 weakCache 里面的数据
SDMemoryCache 相对简单一点,现在我们回到SDImageCache 初始化方法里面继续往下看:
// Init the disk cache
if(directory !=nil) {
_diskCachePath= [directorystringByAppendingPathComponent:fullNamespace];
}else{
NSString*path = [self makeDiskCachePath:ns];
_diskCachePath= path;
}
dispatch_sync(_ioQueue, ^{
self.fileManager= [NSFileManager new];
});
有本地路径就进行磁盘缓存,没啥好讲的,继续往下看:
#if SD_UIKIT
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
两个通知,分别是 app 生命周期结束 和 进入后台,两个通知的方法最终走的都是同一个
![](https://img.haomeiwen.com/i3366589/0f00b590ddb752a0.png)
可以看752 行和639 行所调用的方法,唯一区别是 752 利用block 做了一些归还系统进入后台任务的操作而已
![](https://img.haomeiwen.com/i3366589/8bd2e0fd59d738f4.png)
我们进入deleteOldFilesWithCompletionBlock 里面看看,这个方法在SDImageCacheConfig 介绍属性的时候也介绍过,这里在详细的过一遍:开始上图
![](https://img.haomeiwen.com/i3366589/3a704cf113c26a25.png)
代码片段1主要是做了文件磁盘缓存以修改还是以最近操作的时间算:
NSURLContentAccessDateKey 最近操作时间
NSURLContentModificationDateKey 最后修改时间
resourceKeys 是所需要遍历的文件的类型(是路径,按照什么时间类型的文件,以字节为单位分配文件),使用NSFileManager 的枚举方法获取 NSDirectoryEnumerator *fileEnumerator
![](https://img.haomeiwen.com/i3366589/d4ddc197ed4c7d75.png)
这个遍历干了两件事:1.移除过期的文件(expiration date) 2. 存储新的文件并记录大小
resourceValues 是以NSURLResourceKey 为key,id 为value字典(id类型根据 key的含义存储对应的数据)
688 行 就是根据时间对把文件添加到删除的数组里面,循环结束删除。 cacheContentDateKey 是时间属性的key所以取出来的value 是NSDate
695 行 NSURLTotalFileAllocatedSizeKey 是大小属性的key,所以取出来文件大小。
![](https://img.haomeiwen.com/i3366589/3f39a3d084af0880.png)
700 行就是遍历删除文件操作
706 行开始 就是通过设定的最大缓存大小和当前文件实际大小对比
708 先定义要清掉的大小 为设定最大缓存大小的1/2
711 利用系统的排序方法按照缓存时间进行排序
717 通过遍历目前缓存文件,cacheFiles 就是fileURL 和resourceValues的映射,看一参考上一张图679 行,
一直到遍历结束 可知,清到当前实际缓存的文件大小小雨于 desiredCacheSize 为止
729 直接以block 返回到调用处
至此 SDImageCache的初始化方法全都剖析完成。肯定有一些不正确的地方,希望广大网友留言指正。
再挑几个比较重要的接口方法剖析一下:比如缓存数据,读取数据这些
![](https://img.haomeiwen.com/i3366589/36c2dd2cbe4b67cf.png)
直接去.m 看实现了:
![](https://img.haomeiwen.com/i3366589/e0a309f1e212f2a9.png)
287 异常处理
294 针对的是内存缓存,SDImageCacheConfig里面已经说过了,感兴趣的进去看看。
299 toDisk,我们提取核心思想,缓存磁盘 SD单独开了异步线程去做,并且检查了图片是否含有通道,我们去SDCGImageRefContainsAlpha看看:
![](https://img.haomeiwen.com/i3366589/8b1c0abfed4e5b01.png)
意图很明显,kCGImageAlphaNone、kCGImageAlphaNoneSkipFirst、kCGImageAlphaNoneSkipLast 都是不包含通道的。
回到函数调用处继续往下看
307-309 区分出来是什么格式图片
311 我们进入编码方法实现里看看:
![](https://img.haomeiwen.com/i3366589/b270eacce1d6b631.png)
self.coders 看下头文件解释:
/**
All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority
*/
@property (nonatomic, copy, readwrite, nullable) NSArray<id<SDWebImageCoder>> *coders;
是一个遵循SDWebimageCoder 协议的对象队列,后进先出,最后加入进来的具有最高执行的优先级。
142 行我们进入具体编码实现看看:
目前SD里面有SDWebImageGIFCoder和 SDWebImageImageIOCoder 两个类遵循实现了图片编码操作,一个个的来看看
先进入SDWebImageImageIOCoder:
![](https://img.haomeiwen.com/i3366589/65628563c947497c.png)
380 - 393 异常判断和格式图片格式判断,没啥好说的,
395 利用传进来的参数 format 获取 系统的CFStringRef,sd_UTTypeFromSDImageFormat 方法里面就是枚举获取。也不用多介绍了
398 创建目标图片CGImageDestinationRef
/* Create an image destination writing to `data'. The parameter `type'
* specifies the type identifier of the resulting image file. Constants for
* `type' are found in the LaunchServices framework header UTCoreTypes.h. The
* parameter `count' specifies number of images (not including thumbnails)
* that the image file will contain. The `options' dictionary is reserved
* for future use; currently, you should pass NULL for this parameter. */
IMAGEIO_EXTERN CGImageDestinationRef _iio_Nullable CGImageDestinationCreateWithData(CFMutableDataRef _iio_Nonnull data, CFStringRef _iio_Nonnull type, size_t count, CFDictionaryRef _iio_Nullableoptions) IMAGEIO_AVAILABLE_STARTING(10.4, 4.0);
通过注释可以知道,创建一个指定的图片“容器”对象 用来存放CGImage
406 -407 就是把图片的UIImageOrientation 转换成kCGImagePropertyOrientation的方向
411 把image加入到CGImageDestinationRef 中
![](https://img.haomeiwen.com/i3366589/b0c91d9dfd319124.png)
414 - 421 没啥好介绍的,释放C的指针以及返回上层。
下面我们进入SDWebImageGIFCoder 这个编码类来看看,总体和 SDWebImageImageIOCoder类似,只不过加了 针对frame 的处理,还是挑重点看:
![](https://img.haomeiwen.com/i3366589/228c5b313f63b4fa.png)
148 提取gif图片的帧,转换成SDWebImageFrame 帧对象,这个对象有两个属性,
/**
The image of current frame. You should not set an animated image.
*/
@property (nonatomic, strong, readonly, nonnull) UIImage *image;
/**
The duration of current frame to be displayed. The number is seconds but not milliseconds. You should not set this to zero.
*/
@property (nonatomic, readonly, assign) NSTimeInterval duration;
进入到framesFromAnimatedImage里面看看具体怎样把GIF 转换成 SDWebImageFrame
![](https://img.haomeiwen.com/i3366589/a63afadfad3ff224.png)
87 - 103 初始化 frames 并且求动画平均值。
![](https://img.haomeiwen.com/i3366589/885ebebd8593f4cc.png)
106 - 126 :抽取图片每一个frame,转换成 SDWebImageFrame, index这个变量好像是多余的,就是index++ 其他地方没用到。不知道SD作者想干啥
![](https://img.haomeiwen.com/i3366589/9110cf2c0232e11c.png)
条件定义的else 应该是针对的macOS ,进不了对应的类去查看(不知掉为啥),看代码逻辑和前面的一样,遍历处理每一帧然后转换成SDWebImageFrame
我们继续回到encodedDataWithImage里面往下看:
![](https://img.haomeiwen.com/i3366589/cd64f132ae1ee3ef.png)
提取核心内容160 - 170 :
image.sd_imageLoopCount 在解码的时候就已经赋值了(暂不追究怎么来的),用的是关联属性去做的:
![](https://img.haomeiwen.com/i3366589/50f250122b755a70.png)
162 - 163 : 为imageDestination 这个“容器”设置一下 图片帧数量的映射,
165 - 170: 循环添加gif图片到imageDestination里面(和SDWebImageImageIOCoder里面类似)
解码的具体实现基本看完了,我门继续回到入口处(别晕了):
![](https://img.haomeiwen.com/i3366589/332ebaa2454e20ba.png)
317 :解码完成之存一下,后面就是block完成返回了。
![](https://img.haomeiwen.com/i3366589/77d89b33790def69.png)
开放了直接存入磁盘的接口,里面调用的就是 storeImageDataToDisk,没啥可说的了
![](https://img.haomeiwen.com/i3366589/cc098fba37ab7b74.png)
查找缓存:二重查找
![](https://img.haomeiwen.com/i3366589/6cd14734c9908b9d.png)
430 从内存SDCacheImage里面查,但是 没有从弱hash表里面查(个人有点疑问题)
436 从磁盘查
SDImageCache 比较重要的接口细究到此结束,欢迎大家留言相互交流。