SDWebImage的原理

这篇文章通过SD的源码,梳理下SD框架下图片加载的流程。

SD的大致流程相信各位已经很熟悉了。(需要加载图片时,首先查看本地有没有存,如果没有就去下载,然后缓存到本地,最后再显示出来。)我们这里根据官方提供的流程图的步骤详细分下主要流程。

一、流程:

展开之前,我们看一下官方给出的流程图:
SDWebImageSequenceDiagram.png

由图可见,SD把加载图片一共分为了10个步骤。接下来我们根据步骤进行详细的解读。

二、流程分解:

1-2-3:无论我们调用UI层的哪个方法,首先都会进入UI层的总方法sd_internalSetImageWithURL由该方法做一些简单的配置,如初始化SDWebImageContext(就是个NSDictionary,默认配置为空)、初始化imageProgress(用于显示下载进度)后,进入管理类SDWebImageManagerloadImageWithURL方法,这个方法依然是做一些辅助工作后进入SDWebImageManagercallCacheProcessForOperation方法,看名字就知道,缓存相关的程序,由此开始。
4-5callCacheProcessForOperation里直接调用了SDImageCachequeryImageForKey方法。SDImageCache作为缓存类,负责缓存相关的工作的重要类,日常代码中甚至可能需要碰到直接操作该类的情况。流程由queryImageForKey进入queryCacheOperationForKey过程中也是做了一些配置。然后进入查询阶段的核心逻辑。
--①-- 首先SDImageCache会先查询内存中有没有图片。查询途径为通过SDMemoryCache类调用objectForKey方法。
SDMemoryCacheNSCache的子类,该类最大的特点是直接把数据缓存进内存中,且无论是查询还是存储速度都特别的快。但是该类最大的缺点是内存的释放由系统控制,程序员无法手动控制。
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当成NSDictionaryNSDictionary只能以字符串作为keyNSMapTable能以对象作为key。(NSMapTable详细介绍可以看这里。)
在这里,就已经用到了两层存储,既SDMemoryCacheNSMapTableSDMemoryCache的优点是快但不稳定,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中了。(关于自定义NSOprationNSOperationQueue后边会专门写一个文章进行介绍)
SDWebImageDownloaderOperation中,使用了NSURLSessiondataTaskWithRequest方法进行下载。到这大家就都知道请求的结果肯定是通过代理回调回去了。

额外插一句,感觉这里一个比较有意思的操作。作者把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次。两次在内存(NSCacheNSMapTable)一次在磁盘(NSCachesDirectory)。这个虽然比较稳妥,但是也确实占用了不少空间。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容