这篇文章通过SD的源码,梳理下SD框架下图片加载的流程。
SD的大致流程相信各位已经很熟悉了。(需要加载图片时,首先查看本地有没有存,如果没有就去下载,然后缓存到本地,最后再显示出来。)我们这里根据官方提供的流程图的步骤详细分下主要流程。
一、流程:
展开之前,我们看一下官方给出的流程图:由图可见,SD把加载图片一共分为了10个步骤。接下来我们根据步骤进行详细的解读。
二、流程分解:
1-2-3:无论我们调用UI层的哪个方法,首先都会进入UI层的总方法sd_internalSetImageWithURL
由该方法做一些简单的配置,如初始化SDWebImageContext
(就是个NSDictionary,默认配置为空)、初始化imageProgress
(用于显示下载进度)后,进入管理类SDWebImageManager
的loadImageWithURL
方法,这个方法依然是做一些辅助工作后进入SDWebImageManager
的callCacheProcessForOperation
方法,看名字就知道,缓存相关的程序,由此开始。
4-5:callCacheProcessForOperation
里直接调用了SDImageCache
的queryImageForKey
方法。SDImageCache
作为缓存类,负责缓存相关的工作的重要类,日常代码中甚至可能需要碰到直接操作该类的情况。流程由queryImageForKey
进入queryCacheOperationForKey
过程中也是做了一些配置。然后进入查询阶段的核心逻辑。
--①-- 首先SDImageCache
会先查询内存中有没有图片。查询途径为通过SDMemoryCache
类调用objectForKey
方法。
SDMemoryCache
是NSCache
的子类,该类最大的特点是直接把数据缓存进内存中,且无论是查询还是存储速度都特别的快。但是该类最大的缺点是内存的释放由系统控制,程序员无法手动控制。
objectForKey
的源码如下:
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(_weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(_weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
简单来说如果SDMemoryCache
里没有查到结果,就再去weakCache
里查,如果查到了就再往SDMemoryCache
里存一份,用以保证下次查询的速度。
关于这个weakCache
类,其实是NSMapTable
类的实例。往简单了说,你可以把NSMapTable
当成NSDictionary
。NSDictionary
只能以字符串作为key
而NSMapTable
能以对象作为key
。(NSMapTable
的详细介绍可以看这里。)
在这里,就已经用到了两层存储,既SDMemoryCache
与NSMapTable
。SDMemoryCache
的优点是快但不稳定,NSMapTable
的优点是稳定,但不是那么的快。两者相辅相成,最大程度保证数据的持久性与敏捷性。如果到这里还未查到数据,则再进入从磁盘查数据的流程。
--②-- 磁盘查找源码先贴一下
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
这里只贴了4行核心代码。第一行建个自动释放池。第二行磁盘查找,第三行nsdata转UIImage,第四行往内存里存一份。我们对这四个核心步骤挨个分析:
第一行:这里之所以搞个自动释放池,不少人认为是因为这里创建了较多的变量需要及时释放。但是仔细看下代码,创建的变量并不算多,这个理由非常牵强。真实的原因是这里的变量数据较大,才需要及时释放,而不是因为变量太多。通过上面的代码你就能看到,这里最少有两个大数据变量,就是图片数据对应的diskData与diskData。如果不对这两个大数据变量及时用autoreleasepool释放,如果当前同时下载的图片很多,就会导致这里堆积大量数据占用用内,导出内存不足甚至闪退。同样的操作在下载的过程中也出现过。
第二行:就是从磁盘里查数据了,这里稍稍需要注意的是SD在磁盘上,默认是把数据存在了NSCachesDirectory上的,源码如下。
+ (nullable NSString *)userCacheDirectory {
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return paths.firstObject;
}
第三行:之所以有个复杂的转换过程,是因为这个这个转换是根据Options与context进行定制的。我们知道SD支持显示缩略图与自定义图片大小的功能,就是用这个方法进行定制的。
第四行:是往内存里存数据。SD会根据是否需要把从磁盘查到的数据往内存里存一份。从而进一步保证了查询了速度。顺序是先往NSCache
(既上文提到的SDMemoryCache
)存,再往NSMapTable
里存。
6-7:如果本地都没有,就进入了网络下载的环节。此时会进入callDownloadProcessForOperation
方法。这个方法里经过一系列配置后,进入requestImageWithURL
方法而后进入downloadImageWithURL
方法(ps:看这命名,多磨的严谨)。在这个方法里会把下载任务封装进NSOpration
的子类SDWebImageDownloaderOperation
中,通过队列进行下载。这个队列(downloadQueue
)的最大并发数默认为6个,并通过一个监听(addObserver
)实时监听并发数的改变并跟着改变。
加入队列后就进入了NSOpration
的子类SDWebImageDownloaderOperation
中了。(关于自定义NSOpration
与NSOperationQueue
后边会专门写一个文章进行介绍)
在SDWebImageDownloaderOperation
中,使用了NSURLSession
的dataTaskWithRequest
方法进行下载。到这大家就都知道请求的结果肯定是通过代理回调回去了。
额外插一句,感觉这里一个比较有意思的操作。作者把progressBlock
(过程回调)与completedBlock
(结果回调)都放进了各自的固定key
的字典里(SDCallbacksDictionary
),然后又把字典放进了叫callbackBlocks
的数组里。需要用时根据key
从数组里取不同的字典对应的回调。代码如下
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
@synchronized (self) {
[self.callbackBlocks addObject:callbacks];
}
return callbacks;
}
个人感觉可能没有必要这样做,就两个block直接让类持有不就好了嘛。不知道这里用到了什么设计思路或者设计模式,了解的还请不吝赐教。
7-8-9-10:下载完成后,进入存储阶段callStoreCacheProcessForOperation
顺序是先往内存里存然后再存磁盘。最后把UIImage回调给UI层。
三、结语
流程分享完毕后,这里其实可以看出来,SD为了保证数据的速度与持久性,其实把每个图片都存了3次。两次在内存(NSCache
与NSMapTable
)一次在磁盘(NSCachesDirectory
)。这个虽然比较稳妥,但是也确实占用了不少空间。