上图按模块整理了sd各模块的类
上图则表示了各模块和类在整理项目中的层级关系。
根据上面的模块图,我这里先按照模块对源码进行学习和分析。
下载模块
因为之前看了af,sd的下载模块应该也是基于NSUrlSession封装的,所以先看下载模块。
SDWebImageDownloaderOperation
SDWebImageDownloaderOperation是继承自系统的NSOperation,然后自己实现了一套异步操作的相关操作。这里不展开讲自定义NSOpration需要做哪些事情,网上应该有很多相关demo和文章。
SDWebImageDownloaderOperation的内部通过NSUrlSession创建task进行图片的下载,每个request的httpmethod默认为GET方式,这也方便拿到下载过程中的图片数据。
SDWebImageDownloaderOperation里面使用NSURLCache对URL的resopne进行缓存,代码如下:
|
|
URLCache相关内容可以参考这一篇文章
SDWebImageDownloaderOperation同时支持了后台下载,代码如下:
|
|
SDWebImageDownloaderOperation将每个task的progress回调和Complete回调存放在一个 callbackBlocks 的数组中,方便在收到NSURLSessionDelegate的代理时调用相对应的block,将数据传出去。
|
|
在收到didReceiveData回调后,如果当前options为SDWebImageDownloaderProgressiveDownload的话,还需要将收到的这部分图片数据进行解码后显示,显然这不是完整的数据,不过这部分数据也是可以正常显示的。
|
|
在收到didCompleteWithError回调后,会异步解码收到的image数据,gif和webp除外,这两会走另外的解码接口。
|
|
sd做https的证书验证方法很简单,如果自定义的证书不为空,则返回自定义的证书,为空的话,直接将服务器的证书返回并标识验证成功,所以实际上它这里只是简单的返回证书,并没有对证书做一个校验。
|
|
SDWebImageDownloader
SDWebImageDownloader是SDWebImageDownloaderOperation的管理类,和af不同的是,SDWebImageDownloader创建了一个NSOperationQueue,管理每个nsoperation。当nsoperation被标记为finish或者cancel后,会自动从Queue中移除。
看一下SDWebImageDownloader是如何管理operation的,比较重要的就是下面这一段代码了
|
|
sd将每一个operation存在URLOperations里面,key是url,value是operation,当需要创建一个下载任务时,先从这个URLOperations的字典中查找是否已经有对应的下载任务,如果没有的话,将createCallback()这个block的返回值赋值给一个新的operation对象(并在opration的完成块中将它从URLOperations字典中移除),给opration添加相应的progressBlock和completeBlock,后将它存入URLOperations中,并创建opration对应的SDWebImageDownloadToken对象,再将该token对象返回。
上面这段描述中有2个地方需要注意下:
第一个是createCallback()这个是个啥?看看下面这段代码就会明白:
|
|
createCallback其实就是创建一个新的downloadoperation,然后将这个downloadoperation返回。
第二个问题是SDWebImageDownloadToken这个类是用来干嘛的,也是看一下下面这个方法应该就清楚了:
|
|
token.url为下载的文件url,operation为当前下载这个文件的任务,downloadOperationCancelToken实际上是一个字典,里面存了这个任务的progressBlock和completedBlock,保存这个主要是为了取消的时候把operation中保存的这两block给移除掉。
总结一下就是,这个SDWebImageDownloadToken对象其实就是用在cancel的时候移除之前保存在相关字典中的数据的。在SDWebImageManager中也是这么用的。
缓存模块
看完下载模块,再来看一下缓存模块。大家都知道sd里面的缓存用到了NSCache,那它到底是咋用的呢,我们具体看一下吧。
SDImageCacheConfig
先来看一下这个config类中都定义了哪些变量
|
|
SDImageCache
NSCache相关内容:
|
|
可以看到sd自定义了一个继承自NSCache的类,并申明了一个NSMapTable类型的属性weakCache,直接用NSCache不就可以存储变量到内存了吗,这里为啥还要加一个NSMapTable呢。我们先看一下这个NSMapTable的初始化(NSMapTable相关内容参考这里)
|
|
可以看到,NSMapTable中存在值都是weak的,如果value被释放,则存的值将变为空,然后我们看一下自定义的SDMemoryCache类的get和set方法。
|
|
对内存中图片的存取都是做了2次操作,一次是存到NSCache中,一次是存到NSMapTable中,大家都知道NSCache有一个特性就是当系统内存不足的时候会首先回收NSCache的内存,看这段get的代码,这里作者应该是考虑如果NSCache中内存被回收了可以从NSMapTable中继续找到该图片数据,不用去读磁盘数据或者重新下载。
然后我们来看一下存储到disk的过程
|
|
如果当前的图片data不为空则直接存在磁盘,如果为空而且图片对象不为空,则需要先对图片进行编码处理转为data在存到磁盘中,这是因为data是直接从服务器拿到的数据,是编码过的,而image则是已经解码了的,是原始数据,数据量会比较大,所以需要先编码在存储。
|
|
存储过程很简单,就是一个文件写入就OK了。写入前先判断一下文件夹是否存在,不存在的话先创建一个。
然后我们看一下比较重要的一个方法,图片的查询
|
|
先从nscache中查找,如果查找成功且options为SDImageCacheQueryDataWhenInMemory,则返回nscache中的结果
如果nscache查找失败,则查找disk中的data,并解码该data同时将解码后的image塞到nscache中,然后返回。
sd还提供了一个删除缓存文件的方法,在该方法中,一共使用了2种删除策略,第一种是按过期时间删除
|
|
如果按过期时间删除后的文件大小还是大于最大缓存空间(maxCacheSize)的话,还有一种删除策略,就是删除现有文件大小的一半的文件。
|
|
图片编解码模块
图片的编解码模块也是sd中比较重要也比较难吃透的模块,涉及到一些图片相关的姿势,需要慢慢学习理解。
SDWebImageCoder
先来看一下这个协议类,这个类中定义了一些编解码的协议和几个静态函数。
静态函数:
|
|
SDWebImageImageIOCoder
这个是图片编解码的核心类,相关方法和协议都在这个类中实现。看头文件的注释,sd支持PNG,JPEG,TIFF和HEIC(需要判断设备是否支持)这几种图片格式的编解码。gif和webp有单独的编解码类。
因为sd是支持图片边下载边显示的,而这个显示操作需要先将接受到的部分图片解码后才可以显示,这里看一下这个解码部分图片是如何实现的
|
|
先创建一个imagesource,然后更新imagesource的data,注意这里的data是目前接收到的图片的所有数据,不止是新增的部分,这里在下载的图片的时候sd已经处理好了,保存了之前下载的那部分数据。然后获取图片宽高属性和方向,最后生成图片后返回。
接着看正常图片的解码过程
|
|
首先需要判断一下图片是否需要被缩放,因为在下载完以后的图片也是根据图片后缀是否带@2x@3x这种标识进行过缩放的,并不是图片的实际尺寸。
如果不需要缩放,直接调用下面的解码方法解码即可:
|
|
这段代码很经典,很多图片解码相关的库都会用到,如果你想自己写一个异步的图片解码函数,也可以直接拿去用。这里用的参数及相关解释网上有很多文章分析过,比如这篇就解释的挺清楚。
如果是需要缩放的图片,则走下面这个方法解码
|
|
这里先判断图片的总像素是否比sd设置的最大像素值大,如果总像素超出了设置的最大像素值,则需要先缩放图片再解码,不然解码需要的内存空间太大,可能造成内存暴涨等问题。。。
|
|
sd这里应该也是用了帧内压缩算法去压缩图片的尺寸,通过计算像素差值的方式将多余的像素剔除,然后sd使用了一个tile的东东去存储每次计算出来的数据,并将这些数据写入到设备上下文中。计算完成后再从上下文中取出一张位图。这个压缩解码函数的详细注释可以参考这一篇文章.
然后就是编码部分,什么时候需要编码呢?就是在存入disk的时候,因为之前用到的image可能是解码之后的图片,直接存入的话会占用比较大的内存空间,所以sd这里都是先将image编码后再存的。
编码的函数和解码相比还是比较简单的:
|
|
先创建一个CGImageDestinationRef,并绑定一个imagedata,然后获取图片的相关属性,然后将图片数据和相关属性一起add到CGImageDestinationRef中,获取之前绑定的imagedata并返回,整个编码过程结束。
SDWebImageGIFCoder
这个类主要做gif的编解码。直接看解码函数:
|
|
sd这里自定义了一个SDWebImageFrame类用来存放每一帧图片的数据,然后将所有帧存放在一个数组中,然后通过下面这个方法将帧数组转为一个image对象,这个方法等下在分析helper类的时候再一起分析一下。
|
|
上面还用到一个方法是如何获取每一帧的时长方法:
|
|
默认每一帧的时长是0.1,如果获取propertydic失败直接返回默认值,否则的话从propertydic取对应的value。
然后看一下gif的编码过程
|
|
编码的时候也是需要先获取到SDWebImageFrame数组,然后和普通图片编码过程一样,也是创建一个CGImageDestinationRef,绑定imagedata,然后遍历SDWebImageFrame数组将每一帧写入CGImageDestinationRef中,然后获取imagedata并返回。
SDWebImageWebPCoder
使用webp相关的编解码方法,需要pod中添加一个依赖仓库
|
|
sd为webp提供了2种解码方法,分别是部分数据的解码和完整数据的解码。
先看完整的数据解码函数
|
|
这里也是分了2种类型去解码,一种是静态图片,直接使用下面这个方法或者静态图片
|
|
如果是动图的话,和GIF的解码一样,需要获取一个SDWebImageFrame的数组,先使用下面这个函数获取每一帧的图片数据
|
|
这个函数里面先调用静态获取图片的方法生成一张图片,然后判断是否需要混合当前上下文中的内容,如果不需要就清空之前的内容,如果需要的话将刚生成的图片再写入之前的上下文中混合成一张新的图。
然后使用之前gif中使用过的下面这个函数将帧数组转为一个image
|
|
部分数据的解码函数如下:
|
|
流程都和之前的其他格式部分解码函数差不多吧,只是有写api的小差异。
解码的部分差不多就这些,接着看一下编码的部分是如何实现的,直接上代码
|
|
和gif的编码流程相似,先获取到这张图的所有帧,(静态图片只有一帧,直接调用处理每一帧的编码函数进行处理),然后循环处理每一帧的数据,然后将处理完的每一帧数据存放到WebPMuxFrameInfo这个结构体,再将每个结构体add到WebPMux这个类里面,循环结束从WebPMux这个类中取出数据并返回。
下面这个函数是具体处理每一帧的数据的编码。
|
|
和之前的其他图片的编码不一样,它这里没有使用CGImageDestinationRef这个结构处理编码,而是使用的webp库提供的编码方法。先创建vImageConverterRef这个对象,然后设置编码前的格式和编码后的格式,然后初始化vImageConverterRef对象,然后初始化vImage_Buffer,最后调用vImageConvert_AnyToAny方法进行格式转换,最后调用WebPEncodeRGBA生成最终需要返回的数据。
整个过程和使用AudioUnit进行音频格式转换很相似。
SDWebImageCodersManager
这个类就是对上面这几个编解码类的使用的一个封装,看初始化函数默认只使用了SDWebImageImageIOCoder这个类作为当前编解码类。
|
|
当然,你可以通过下面这个方法手动的添加和移除你需要的编解码类,比如gif的Coder
|
|
然后它提供了直接对数据进行编解码的方法
|
|
通过判断传入的数据是否支持编解码,如果支持就调用对应的编解码函数,如果不支持直接返回空。
SDWebImageCoderHelper
最后来看一下这个Helper,该类提供了几个通用的方法,主要有:
|
|
看一下具体实现部分:
|
|
帧数组转图片,如果是iOS平台,主要调用UIImage的animatedImageWithImages:duration方法创建,mac平台上我们看到了熟悉的身影CGImageDestinationRef,这个之前在做编码的时候用到过。
然后是图片转帧数组
|
|
也是分了iOS和mac2个平台,iOS上直接去UIImage的images属性遍历一遍,mac上使用了NSBitmapImageRep这个对象获取图片的所有帧数据,然后在循环中从这个对象中取出所有的帧数据。
下面这2个是朝向转换的函数。
|
|
sd中是使用下面的代码拿到在图片数据中朝向的值的
|
|
以上所有编解码相关的类就分析完毕了。这里我们在设计模块的时候可应该参考sd里面的做法,首先通过协议的方式放开需要的接口,各模块按根据需求决定是否实现对应协议,虽然都是编解码,但是针对不同的格式创建不同的类去实现,然后在manager中通过判断不同的格式调用不同的模块去做具体的事情,减少逻辑的耦合。
SDWebImageManager
看完了下载,缓存和编解码这三个主要的模块,在来看看它们的manager,这个其实就比较简单了,就是对上面几个模块的相关方法进行了一些封装。
先看一下这个私有类
|
|
在manager中定义了相关属性
|
|
这里,manager将每一个download返回的SDWebImageDownloadToken封装为一个SDWebImageCombinedOperation,存放在runningOperations这个数组里面,主要是为了在取消的时候方便操作(这个在之前介绍下载模块的时候已经有说明)。
manager创建了一个集合如下:
|
|
主要用来存储下载失败的url,如果url下载失败会被添加到上面的集合中,如果当前url在该合集中,且option不是SDWebImageRetryFailed(这个option是消除黑名单用的),则直接返回下载失败。如果option是SDWebImageRetryFailed,则会去重新下载。
manager还提供了2个block,让使用者有机会修改一下内容。如下:
|
|
这个block可以修改当前图片缓存时的键值,默认是使用url.absoluteString作为键值进行缓存的。
|
|
这个block可以用来修改缓存图片的数据。它接收当前下载的图片的image对象,data对象和url,返回一个data.你可以在block对传入的图片数据做一些处理,比如做一下编码或者格式转化什么的。
manager中最主要的还是下面这个loadimageurl的函数:
|
|
就是一个先查找缓存,如果没有命中再去下载的过程,是一个比较经典的逻辑,网上一般说sd都会说到这里的这个逻辑。这里就不细说了。
分类
sd大家用的最多的其实是分类,特别是UIImageView的分类,这里我挑2个比较有内容的分类说一下,其他分类都比较简单,就是纯调api了。
NSData + ImageContentType
这个分类比较重要的知识点就是如何通过图片文件判断图片格式,看一下主要代码
|
|
看到代码就应该很明了了,通过文件数据的第一个字节来判断文件格式,这里用2个十六进制数表示一个字节。这个函数如果有需要可以直接拿来用。
UIView + WebCache
这个算是所有分类的父类了,大部分UI相关的分类最后都是调用的这个分类里面的一个方法,如下:
|
|
直接看加载图片部分吧,图片下载完成后,如果当前是MAC平台的话,还创建了一个转场动画SDWebImageTransition,这个转场动画比较简单,都是最基本的转场效果。
然后我们看一下设置图片给UI的方法:
|
|
这里看一下具体的转场动画是咋做的吧,如果是iOS,使用UIView的transitionWithView方法添加动画,具体的添加动画的代码再transition.animations这个block中,就是给当前view的layer添加一个animation.在mac上则是使用NSAnimationContext的runAnimationGroup方法添加动画,具体添加方法和iOS一样。
总结
初看sd的代码时,觉得有点杂乱无章,命名也不太友好,然后模块层级也不太明确,和af比起来在架构方面差好多。不过在按模块分析和研读sd的代码后,还有能学到很多而且是可以运用到实际开发中的姿势点的,比如自定义一个下载器部分,可以学习sd是如何自定义封装一个NSOpearion,如何管理每一个NSOpearion的生命周期,如何回调下载中的数据等。对于编解码能学到的东西也很多,比如针对不同编解码器如何使用同一套接口去编程,最主要的当然是对不同图片数据的编解码方式,这个也是该模块最核心的功能。对于缓存模块可以学到NSCache以及NSMapTable的使用,NSURLCache的使用,对于缓存内存的限制,以及缓存不足时的删除策略等。