源码来源:https://github.com/rs/SDWebImage
版本: 3.7
SDWebImage
是一个开源的第三方库,它提供了UIImageView
的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
- 提供
UIImageView
的一个分类,以支持网络图片的加载与缓存管理 - 一个异步的图片加载器
- 一个异步的内存+磁盘图片缓存
- 支持
GIF
图片 - 支持
WebP
图片 - 后台图片解压缩处理
- 确保同一个
URL
的图片不被下载多次 - 确保虚假的
URL
不会被反复加载 - 确保下载及缓存时,主线程不被阻塞
从github
上对SDWebImage
使用情况就可以看出,SDWebImage
在图片下载及缓存的处理方面还是很被认可的。在本文中,我们主要从源码的角度来分析一下SDWebImage
的实现机制。讨论的内容将主要集中在图片的下载及缓存,而不包含对GIF
图片及WebP
图片的支持操作。
下载
在SDWebImage
中,图片的下载是由SDWebImageDownloader
类来完成的。它是一个异步下载器,并对图像加载做了优化处理。下面我们就来看看它的具体实现。
下载选项
在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举SDWebImageDownloaderOptions
定义,具体如下
|
|
可以看出,这些选项主要涉及到下载的优先级、缓存、后台任务执行、cookie
处理以认证几个方面。
下载顺序
SDWebImage
的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示
|
|
下载管理器
SDWebImageDownloader
下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个NSOperationQueue
操作队列中来完成的,其声明如下:
|
|
默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader
类的maxConcurrentDownloads
属性来修改。
所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,其声明及定义如下:
|
|
每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调等,这些回调操作是以block
形式来呈现,为此在SDWebImageDownloader.h
中定义了几个block
,如下所示:
|
|
图片下载的这些回调信息存储在SDWebImageDownloader
类的URLCallbacks
属性中,该属性是一个字典,key
是图片的URL
地址,value
则是一个数组,包含每个图片的多组回调信息。由于我们允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks
属性。为了保证URLCallbacks
操作(添加、删除)的线程安全性,SDWebImageDownloader
将这些操作作为一个个任务放到barrierQueue
队列中,并设置屏障来确保同一时间只有一个线程操作URLCallbacks
属性,我们以添加操作为例,如下代码所示:
|
|
整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:
方法里面来处理的,该方法调用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue
操作队列中,最后方法返回新创建的操作。其具体实现如下:
|
|
另外,每个下载操作的超时时间可以通过downloadTimeout
属性来设置,默认值为15秒。
下载操作
每个图片的下载都是一个Operation
操作。我们在上面分析过这个操作的创建及加入操作队列的过程。现在我们来看看单个操作的具体实现。
SDWebImage
定义了一个协议,即SDWebImageOperation
作为图片下载操作的基础协议。它只声明了一个cancel
方法,用于取消操作。协议的具体声明如下:
|
|
SDWebImage
自定义了一个Operation
类,即SDWebImageDownloaderOperation
,它继承自NSOperation
,并采用了SDWebImageOperation
协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled:
。
对于图片的下载,SDWebImageDownloaderOperation
完全依赖于URL加载系统中的NSURLConnection
类(并未使用7.0以后的NSURLSession
类)。我们先来分析一下SDWebImageDownloaderOperation
类中对于图片实际数据的下载处理,即NSURLConnection
各代理方法的实现。
首先,SDWebImageDownloaderOperation
在分类中采用了NSURLConnectionDataDelegate
协议,并实现了该协议的以下几个方法:
|
|
我们在此不逐一分析每个方法的实现,就重点分析一下-connection:didReceiveData:
方法。该方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef
对象以做处理。在首次获取到数据时(width+height==0
)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef
对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage
对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
注:缩放操作可以查看SDWebImageCompat
文件中的SDScaledImageForKey
函数;解压缩操作可以查看SDWebImageDecoder
文件+decodedImageWithImage
方法
|
|
我们前面说过SDWebImageDownloaderOperation
类是继承自NSOperation
类。它没有简单的实现main
方法,而是采用更加灵活的start
方法,以便自己管理下载的状态。
在start
方法中,创建了我们下载所使用的NSURLConnection
对象,开启了图片的下载,同时抛出一个下载开始的通知。当然,如果我们期望下载在后台处理,则只需要配置我们的下载选项,使其包含SDWebImageDownloaderContinueInBackground
选项。start
方法的具体实现如下:
|
|
当然,在下载完成或下载失败后,需要停止当前线程的run loop
,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:
与-connection:didFailWithError:
的实现。
小结
下载的核心其实就是利用NSURLConnection
对象来加载数据。每个图片的下载都由一个Operation
操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。
缓存
为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。
SDWebImage
提供了对图片缓存的支持,而该功能是由SDImageCache
类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。
内存缓存及磁盘缓存
内存缓存的处理是使用NSCache
对象来实现的。NSCache
是一个类似于集合的容器。它存储key-value
对,这一点类似于NSDictionary
类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。
磁盘缓存的处理则是使用NSFileManager
对象来实现的。图片存储的位置是位于Cache
文件夹。另外,SDImageCache
还定义了一个串行队列,来异步存储图片。
内存缓存与磁盘缓存相关变量的声明及定义如下:
|
|
SDImageCache
提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个key
值来索引它。在内存中,我们将其作为NSCache
的key
值,而在磁盘中,我们用这个key
作为图片的文件名。对于一个远程服务器下载的图片,其url
是作为这个key
的最佳选择了。我们在后面会看到这个key
值的重要性。
存储图片
我们先来看看图片的缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task
放到串行队列中处理。在iOS中,会先检测图片是PNG
还是JPEG
,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5
摘要后的串)。缓存操作的基础方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk
,它的具体实现如下:
|
|
查询图片
如果我们想在内存或磁盘中查询是否有key
指定的图片,则可以分别使用以下方法:
|
|
而如果只是想查看本地是否在key指定的图片,则不管是在内存还是在磁盘上,则可以使用以下方法:
|
|
移除图片
图片的移除操作则可以使用以下方法:
|
|
我们可以选择同时移除内存及磁盘上的图片。
清理图片
磁盘缓存图片的清理操作可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下两个方法:
|
|
而部分清理则是根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge
属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize
属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。清理的操作在-cleanDiskWithCompletionBlock:
方法中,其实现如下:
|
|
小结
以上分析了图片缓存操作,当然,除了上面讲的几个操作,SDImageCache
类还提供了一些辅助方法。如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key
指定的图片。另外,SDImageCache
类提供了一个单例方法的实现,所以我们可以将其当作单例对象来处理。
SDWebImageManager
在实际的运用中,我们并不直接使用SDWebImageDownloader
类及SDImageCache
类来执行图片的下载及缓存。为了方便用户的使用,SDWebImage
提供了SDWebImageManager
对象来管理图片的下载与缓存。而且我们经常用到的诸如UIImageView+WebCache
等控件的分类都是基于SDWebImageManager
对象的。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,如下代码所示:
|
|
从上面的代码中我们还可以看到有一个delegate
属性,其是一个id\<SDWebImageManagerDelegate\>
对象。SDWebImageManagerDelegate
声明了两个可选实现的方法,如下所示:
|
|
这两个代理方法会在SDWebImageManager
的-downloadImageWithURL:options:progress:completed:
方法中调用,而这个方法是SDWebImageManager
类的核心所在。我们来看看它的具体实现:
|
|
对于这个方法,我们没有做过多的解释。其主要就是下载图片并根据操作选项来缓存图片。上面这个下载方法中的操作选项参数是由枚举SDWebImageOptions
来定义的,这个操作中的一些选项是与SDWebImageDownloaderOptions
中的选项对应的。我们来看看这个SDWebImageOptions
选项都有哪些:
|
|
大家在看-downloadImageWithURL:options:progress:completed:
,可以看到两个SDWebImageOptions
与SDWebImageDownloaderOptions
中的选项是如何对应起来的,在此不多做解释。
视图扩展
我在使用SDWebImage
的时候,使用得最多的是UIImageView+WebCache
中的针对UIImageView
的扩展方法,这些扩展方法将UIImageView
与WebCache
集成在一起,来让UIImageView
对象拥有异步下载和缓存远程图片的能力。其中最核心的方法是-sd_setImageWithURL:placeholderImage:options:progress:completed:
,其使用SDWebImageManager
单例对象下载并缓存图片,完成后将图片赋值给UIImageView
对象的image
属性,以使图片显示出来,其具体实现如下:
|
|
除了扩展UIImageView
之外,SDWebImage
还扩展了UIView
、UIButton
、MKAnnotationView
等视图类,大家可以参考源码。
当然,如果不想使用这些扩展,则可以直接使用SDWebImageManager
来下载图片,这也是很OK的。
技术点
SDWebImage
的主要任务就是图片的下载和缓存。为了支持这些操作,它主要使用了以下知识点:
dispatch_barrier_sync
函数:该方法用于对操作设置屏幕,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。NSMutableURLRequest
:用于创建一个网络请求对象,我们可以根据需要来配置请求报头等信息。NSOperation
及NSOperationQueue
:操作队列是Objective-C
中一种高级的并发处理方法,现在它是基于GCD
来实现的。相对于GCD
来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对SDWebImage
中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。NSURLConnection
:用于网络请求及响应处理。在iOS7.0后,苹果推出了一套新的网络请求接口,即NSURLSession
类。- 开启一个后台任务。
NSCache
类:一个类似于集合的容器。它存储key-value
对,这一点类似于NSDictionary
类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。- 清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
- 对图片的解压缩操作:这一操作可以查看
SDWebImageDecoder.m
中+decodedImageWithImage
方法的实现。 - 对
GIF
图片的处理 - 对
WebP
图片的处理
感兴趣的同学可以深入研究一下这些知识点。当然,这只是其中一部分,更多的知识还有待大家去发掘。