SDWebImage源码阅读笔记(一)
在做iOS开发中加载图片是经常性工作,一种是使用UIImage加载本地图片,使用[UIImage imageNamed:@""],[UIImage imageWithContentsOfFile:@""]等方法,各有侧重优劣,不是本篇重点不必赘述。另一种实时从网络加载,其中一种方法是从服务端获取图片的二进制数据,客户端将其转化为NSData *类型,再通过UIImage加载,这种方式适合小批量的图片加载,安全性好实用简便,另一种方法就是服务端先提供一个图片的URL,客户端再通过URL加载图片,这个适合大批量获取图片的场景,在iOS工程中也是广泛实用的场景,也是本篇讨论的重点。
如果不使用第三方框架,最简单的方法便是先调用[NSData dataWithContentsOfURL:url],再使用[UIImage imageWithData:data]加载图片,通常这个过程可以使用GCD等异步加载方式防止阻塞UI主线程。但通常情况下为了使用方便和提高性能,通常要使用一些封装的框架,这其中就有大名鼎鼎的SDWebImage,这也是本篇文章要介绍的。
首先,还是来大概浏览一下其结构:
大概可以看出大概分为loader下载器,cache管理,图片加载等部分,下面沿袭之前风格,还是通过一个在工程中的简单调用来分析其工作原理。
下面就从一个UIImageView加载一个图片的URL开始:
以上就是一个一个在UITableView的cell中一个普通调用,可以看出这个框架主要使用了类别来实现。
下面进入API查看:
发现其API在UIImageView+WebCache的类别中:
最终调用方法中参数:url为图片网络地址,placeholder为占位图,options为下载处理选择项默认为SDWebImageRetryFailed(也就是说加载失败这个URL就会被拉入黑名单不会重复加载),operationKey为网络操作标识符,setImageBlock、progressBlock和completedBlock这三个block会在不同时间调用,这些在后续分析中都会着重解说。
再继续点进去就到了UIView+WebCache这个类别:
这一步多了一个context参数,这个会死一个字典类型,主要管理一些dispatch_group_t,后面会分析。
再继续就开始脱去层层外衣见识真相了:
下面开始逐行代码分析:
NSString *validOperationKey = operationKey ?:NSStringFromClass([selfclass]);如果上边的参数operationKey为空的话,就创建这个key值,但是用来做什么呢?向下看:
[self sd_cancelImageLoadOperationWithKey:validOperationKey];从字面意思上看是要通过这个key值来撤销一些操作。可以进去详细看:
由上可以看出,UIView+WebCacheOperation这个类别维护了一个NSMapTable *类型的属性,NSMapTable类似于字典吧,这个字典就是以operationKey为key值,遵守协议<SDWebImageOperation>的对象为value值,这行代码就是通过key找到这个operation,将其撤销,并从字典中删除,书中代言operation就是用来做图片依次IO的操作,后边还会详细介绍。
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);这一步是将url保存为这个UIView+WebCache的一个关联类,类似于属性。
dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];这个是用来做一个任务队列管理。
这一步是根据options值决定是否立即显示占位图,
这一步就知道setImageBlock这个参数是用来做什么的,
这里可以看出这个block其实目的是用来将image给对应的控件一般是UIImageView赋值的,如果为空就构造一个finalSetImageBlock,最终在这一步
调用这个block给相应图片显示控件赋值。
继续向下走:
如果url存在,会通过url去IO图片,如果不存在也会有个错误处理。
现在开始具体分析url存在下的图片加载和缓存机制,这个是重点:
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
这两行从字面就可以看出是重置加载进度。
到这一步,SDWebImageManager浮出水面,SDWebImageManager也是一个很重要的部分,相当于这个框架的一个管理类。
进去可以看到,这个类的全局单例对象负责管理缓存、加载、失败处理和运行operation(上文有提到过)。
这一步可看出是对progressBlock的处理,从字面可看出是对加载进度的一个处理,如果工程中需要对加载进度进行处理,可实现这个block。
这一步就是具体的operation操作。也是即将需要大量笔墨分析的部分。
展开这部分代码:
可看出在completed的block中是operation结束后的处理,也就是说在调用这个block时,image已经完成加载和缓存了,是最后一步,既然是最后一步,暂且不谈押后处理。
下面先重点分析一下operation的构造方法:
可以看到operation是遵守<SDWebImageOperation>协议的SDWebImageCombinedOperation类的对象,这个方法的调用者便是SDWebImageManager类的单例对象。
这部分是对url的一个处理以及和operation的创建。
manager维护了一个failedURLs的数组,从字面可看出failedURLs是一个加载失败的url的数组,这里会判断,如果url为空,或者符合花括号中的条件,就会走这个方法[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];展开这个方法:
可以看出这个方法就是处理上文提到过的构造方法中最后一个block参数的,在这里调,说明整个operation也就结束了,当然是以加载失败告终。
这一步将operation加入到manager维护的runningOperations数组中,LOCK锁是用信号量dispatch_semaphore_t实现的。然后通过url得到key值,在
中,可以看出我们可以对url加自定义的过滤器来获取key值。
这一步是缓存一个SDImageCacheOptions的缓存加载查询策略类型的cacheOptions。
紧接着是构建一个缓存的operation,
可以看出SDWebImageCombinedOperation实际上是一个对operation的管理类,cacheOperation才是具体图片IO的operation。
从构造方法中,可看出done后边的block应该是最后一步执行的block,暂且不论,先进去看cacheOperation的具体构造方法。
这个构造方法是由manager维护的SDImageCache *类型的imageCache来完成的。imageCache是框架的缓存管理类。
先对key做判断处理,若为空直接执行doneBlock。
首先从内存缓存中获取,如果内存中存在且满足花括号中的条件直接返回。
接着,从硬盘缓存去获取。由于磁盘IO比较缓慢,所以提供了一个异步任务队列,可以选择异步磁盘获取,可见作者思虑之周详。
展开queryDiskBlock:
先从磁盘中获取到diskData数据,若内存中已经获取到image便赋值diskImage = image;否则再将diskData解码后的数据赋值给diskImage,然后调用[self.memCache setObject:diskImage forKey:key cost:cost];将其存入内存缓存,最后再执行doneBlock。
由上边方法可看出,从磁盘中获取数据时解码是一个很大的耗时耗性能操作,恰好SDWebImage能很巧妙的解决这个问题,其解决方法就是先将其绘制成bitmap数据到画布上,这些在进行网络获取时还会用到,之后再详细讲解,这也是SDWebImage很经典的一部分。
现在就回来再看doneBlock中部分:
由于block会强持有对象,所以这里要对operation做weak处理。
如果operation被撤销,则会被从runningOperations数组中安全移除。
根据加载策略,是否从缓存中取得图片等条件确定是否从网络下载。
如果需要不需要从网络下载,也会回调执行最后的completed的block,并从数组中安全移除operation。
如果需要从网络加载,走下面的选择支:
如果从缓存中加载到图片,切加载策略为从网络刷新,则先返回执行completedBlock,但事没完,还要继续从网络加载刷新,这个适合可能在后台同一个url换另一张图片的情形。
设置一个网络下载策略。
接着构造了一个downloadToken作为属性赋给了operation。可能我们会好奇,downloadToken是啥,点进去看:
可以看得出,这个token的作用是作为一个下载的独立标示,这样一来图片加载处理的operation可以通过持有的这个token来进行撤销下载等操作。
从上边这整块代码来看,可以发现是又manager维护的imageDownloader下载器负责下载图片的,并返回这个token。
在这个方法中传入了url、downloaderOptions下载策略、progressBlock、completedBlock等参数,progressBlock就是前文API过程调用的block,completedBlock从字面上看应该也是结束后调用的block,暂且不论,首先来分析这个下载器的工作:
可以看出SDWebImageDownloader其实是一个下载管理类,imageDownloader就是这个管理类的一个单例对象,负责在运行期间管理图片下载,维护者下载operation的URLOperations字典,downloadQueue下载任务队列等。
逐步展开来看:
如果url为空,直接返回空,并接受操作。
先根据以url为key值从self.URLOperations中查找具体的下载operation。如果不存在或已结束或已撤销就创建一个新的并加入self.URLOperations字典和self.downloadQueue队列。如果存在,但并不在执行中,可根据operation下载策略来调整下载operation的优先级。整个过程都是在线程安全中进行的。
紧接着构造SDWebImageDownloadToken *token返回赋给图片加载处理的operation的downloadToken。
下面继续深入分析具体的下载operation:
可以看到下载operation其实是NSOperation的一个实例,遵守<SDWebImageDownloaderOperationInterface>协议。
展开分析:
设置超时时间,网络请求安全策略,构造网络请求request。
根据request构造相应的operation,并根据下载策略options做优先级处理等。
进入operation的构造方法:
可以看出operation的类其实是框架自定义的继承于NSOperation的类。其任务主体主要是通过重写的start函数实现的:
首先也是用@synchronized 锁来锁住相关代码,这部分代码就是初始化session、dataTask部分。
展开这个锁:
如果任务已经被撤销,重置。
设置应用退入后台的处理。
初始化session,不过这里的session一般使用的是初始化方法传进来的session,一般是由imageDownloader维护的一个框架全局的session。
根据下载策略option设置网络请求缓存策略。
通过session和request构造请求任务dataTask,并将任务operation的executing状态设置为执行。
根据下载策略option设置dataTask的优先级,并运行dataTask。
回头发现imageDownloader的初始化函数中
session的delegate给了imageDownloader,但同样SDWebImageDownloader也继承了<NSURLSessionTaskDelegate, NSURLSessionDataDelegate>协议,并实现了具体协议方法。
而在协议方法中:
又将回调方法在下载dataOperation中执行,所以再来到dataOperation中实现的代理方法:
首先在线程安全下降dataTask置空,紧接着就开始处理返回的图片的二进制数据:
除去错误处理等,核心代码是这块:
这块主要就是在self.coderQueue异步队列中完成图片解码,下面逐步分析:
在这里SDWebImageCodersManager这个解码管理类就粉墨登场了,同样也是实现了一个全局的单例实例来做解码工作。
首先看一下SDWebImageCodersManager的初始化方法:
初始化中除了实现安全锁外,更重要的初始化这个数组_coders很重要,这个数组目前只有一个元素SDWebImageImageIOCoder*类型的实例。
然后进入第一个解码方法:
根据初始化方法会走到SDWebImageImageIOCoder*类型的实例方法:
第一行代码是普通的解码方法,第二行代码是框架通过NSData的类别实现的一个查看图片类型的方法不再详述。
然后获取图片的缓存key值,并矫正图片方向。
然而就此结束了吗?向下走:
这里有又了一个解码方法,这点可能就会引起大家的困惑,为什么要这样呢?原来[[UIImage alloc] initWithData:data]这个方法并没有实际解码,只有将image第一次显示的时候才会解码,并长久滞留内存,这并不是一个很好的处理方法。这一点也就是SDWebImage在那个时代很精华的一部分,通过调用
这个方法将图片绘制到CG画布上,控件直接加载画布上的image,这在当时是一个巨大的创新,下面就具体分析这个方法:
如果下载策略不包含大图片等比例缩小的话将走到这个方法:
展开分析:
做个判断,如果image为空或是动画直接返回。
紧接着就是CG的天地了:
主要思路竟是通过创建一个bitmap context,将图片绘制到画布上,才从画布上获取image。
就此解码工作完成,也会产生长久滞留内存的图片信息数据。顺便提到上文在从硬盘中获取缓存时卖的关子,从硬盘IO获取到图片的二进制数据也走的是这条线。
就此image加载也就完成了,先回到下载operation的回调方法中,还在那个图片解码的队列coderQueue的代码块中:
进去这个方法:
可以看出是通过下载operation维护的callbackBlock数组找到回调的completedBlock执行回调,到这里同学们是否有迷路的,能否找到回家的路呢?
回到这个下载operation的构造方法:
可以看出就是在
这个方法中将过程progressBlock和完成completedBlock传入下载operation中的。
到此为止,整个网络下载过程也就分析完了,回到SDWebImageManager勒种图片加载operaion的构造方法中,
现在就到这completedBlock的执行部分。
正常的话就会走到这个选择支:
先做缓存,由manger持有的缓存管理imageCache来执行:
展开分析:
如果image或key不存在,直接返回。
将解码后的image放入内存缓存,下次从内存中加载就不需要解码。
异步磁盘IO,将图片压缩后二进制数据存入硬盘。
结束缓存后:
终于到了激动人心的时候了,就像一个走了很多路的孩子终于要回家了,终于回调框架API中的completionBlock了。
展开分析,除去过程处理和重绘处理,主干代码走到这里:
赋值要处理的image和data,再走到之前提到到的
方法,给相应控件赋值,就此一个调用结束,可以长吁一口气了。
说了这么多,感觉是时候总结一下了,
从网路上借张图:
按照本篇的分析,整个路径是这样的:首先调用加载图片的UIImageVIew*类型的imageView的分类UIView+WebCache的API,在API方法中,通过id<SDWebImageOperation>的对象,也就是SDWebImageManager*类型的manager,通过manager构造加载图片的id <SDWebImageOperation> operation,也就是SDWebImageCombinedOperation *类型的operation,并由分类UIView+WebCache持有的sd_operationDictionary字典来管理。然后通过id <SDWebImageOperation> operation来构造并持有cacheOperation来获取缓存,同时如果需要从网络下载,便构造持有网络下载的dataOperation来完成下载以及图片解码。这就是大致流程,当然在这个过程中缓存管理,图片解码等都有很多可圈可点的地方,再次不再赘述,可以单独开篇讲解。