SDWebImage的知名度就不用说了,github上近10k的star,国内外太多的App使用其进行图片加载。简单介绍一下,它是一个图片框架,支持从网络中下载且缓存图片,并设置图片到对应的UIImageView 控件或者 UIButton 控件。使用SDWebImage来管理图片加载,会极大地提高我们的开发效率,从而让我们更加专注于业务逻辑实现。也正是因为这样,让我们很多开发都只会用SDWebImage,而忽略了它的内部实现,今天我就给大家介绍一下SDWebImage的内部实现及原理。
一、SDWebImage 概述
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
1、提供了一个UIImageView的 category用来加载网络图片,并对网络图片的缓存进行管理。
2、采用异步方式来下载网络图片
3、采用异步方式,使用 memory+disk 来缓存网络图片,自动管理缓存。
4、支持 GIF 动画
5、支持 WebP 格式
6、同一个 URL 的网络图片不会被重复下载
7、失效的 URL 不会被无限重试
8、耗时操作都在子线程,确保不会阻塞主线程
9、使用 GCD 和 ARC
10、支持 Arm64
二、SDWebImage 使用
1、使用 ImageView+WebCache category 来加载 网络图片
2.、使用Blocks,在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调
3、使用SDWebImageManager,SDWebImageManager为UIImageView+WebCache category的实现提供下载图片接口。
4、单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存。
5、使用 SDImageCache 异步缓存图片
SDImageCache 同时缓存到内存和磁盘中。
SDImageCache 只缓存到内存:
读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。
6、清除缓存文件
7、获取所有缓存图片的总大小
8、获取缓存图片张数
9、直接从缓存中提取图片
10、直接删除缓存中得图片
11、判断本地缓存中是否存在网络中的图片
在实际的运用中,我们并不直接使用SDWebImageDownloader类及SDImageCache类来执行图片的下载及缓存。为了方便用户的使用,SDWebImage提供了SDWebImageManager对象来管理图片的下载与缓存。而且我们经常用到的诸如UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。该对象将一个下载器SDWebImageDownloader和一个图片缓存SDImageCache绑定在一起,并对外提供两个只读属性来获取它们。
三、实现原理
1、SDWebImage 流程图
2、核心逻辑
1、UIImageView+WebCache 中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:。给UIImageView设置图片,就是调用此方法,方法内部的具体做以下几件事:
- 取消当前正在进行的加载任务 operation
- 设置 placeholder
-
如果 URL 不为 nil,就通过 SDWebImageManager 单例开启图片加载任务 operation,SDWebImageManager 的图片加载方法中会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。
a.开始加载之前,先取消对应的UIImageView先前的图片下载操作。框架将对应的下载操作放在UIView的一个自定义字典属性(operationDictionary)中,取消下载操作第一步也是从这个UIView的自定义字典属性(operationDictionary)中取出所有的下载操作,然后依次调用取消方法,最后将取消的操作从(operationDictionary)字典属性中移除。
b.移除之前没用的图片下载操作之后就创建一个新的图片下载操作,然后设置到UIView的一个自定义字典属性(operationDictionary)中。创建一个新的图片下载操作,框架保存了一个失效的URL列表,如果URL失效了就会被加入这个列表,保证不会重复多次请求失效的URL。根据给定的URL生成一个唯一的Key,之后利用这个key到缓存中查找对应的图片缓存。
c.读取图片缓存,先拿图片缓存的 key去内存中读取缓存,如果有,就返回给 SDWebImageManager ;如果内存没有缓存,就开启异步线程去磁盘开始读取,如果有就从磁盘同步到内存中去,再返回给SDWebImageManager 。如果内存和磁盘中都没有,SDWebImageManager 就会调用 downloadImageWithURL: options: progress: completed: 方法去下载。
d.在图片下载方法中,调用了一个方法用于添加创建和下载过程中的各类Block回调。
e.如果该URL是第一次加载的话,那么就会执行createCallback这个回调Block,然后在createCallback里面开始构建网络请求,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务,在下载过程中执行各类进度Block回调。
f.当图片下载完成之后会回到done的Block回调中做图片转换处理和缓存操作。
g.回到UIImageView控件的设置图片方法Block回调中,给对应的UIImageView设置图片,操作流程到此完成。
3、实现细节
SDWebImage 最核心的功能也就是以下 4 件事:
- 下载(SDWebImageDownloader)
- 缓存(SDImageCache)
- 将缓存和下载的功能组合起来(SDWebImageManager)
- 封装成 UIImageView 等类的分类方法(UIImageView+WebCache 等)
3.1 SDWebImageDownloader
3.1.1 SDWebImageDownloader 继承于 NSObject,主要承担了异步下载图片和优化图片加载的任务。
SDWebImageDownloader 通过+initialize 和 -init 这两个方法实现注册通知和做一些初始化设置以及默认设置。SDWebImageDownloader包括设置最大并发数(6)、下载超时时长(15s)等。
SDWebImageDownloader这个类里面除了这两个方法,最核心的方法就是downloadImageWithURL:options: progress: completed:方法,这个方法中首先调用 -addProgressCallback: andCompletedBlock: forURL: createCallback: 方法来保存每个 url 对应的回调 block,-addProgressCallback 方法会先进行错误检查,判断 URL 是否为空,如果为 空, 则直接回调 completedBlock,返回失败的结果,然后 return,否则将 URL 对应的 progressBlock 和 completedBlock 保存到 URLCallbacks 属性中去。
有个细节要注意,因为可能同时下载多张图片,所以就可能出现多个线程同时访问 URLCallbacks 属性的情况。为了保证线程安全,所以这里使用了 dispatch_barrier_sync 来分步执行添加到 barrierQueue 中的任务,这样就能保证同一时间只有一个线程能对 URLCallbacks 进行操作。
(dispatch_barrier_sync 将某个任务插入某个队列中执行:想了解的可以看看这篇文章 https://blog.csdn.net/u013046795/article/details/47057585)
如果这个 URL 是第一次被下载,就要回调 createCallback,createCallback 主要做的就是创建并开启下载任务,下面是 createCallback 的具体实现逻辑:
1.创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务。
2.SDWebImageDownloaderOperation 中有3个block回调,分别是progressBlock(接收到的数据大小和预计数据大小),completedBlock(图片 UIImage,图片数据 NSData,错误 NSError,是否结束 isFinished),cancelBlock(移除 url 对应的所有回调 block);这三个block都用了weak-strong dance,使用 strongSelf 强引用 weakSelf,目的是为了保住 self 不被释放,然后检查 self 是否已经被释放(这里为什么先“保活”后“判空”呢?因为如果先判空的话,有可能判空后 self 就被释放了)
(对weak-strong dance不理解可以看看 https://www.jianshu.com/p/4e6153ea2734)
- 设置下载完成后是否需要解压缩
- 如果设置了 username 和 password,就给 operation 的下载请求设置一个 NSURLCredential 身份认证
- 设置 operation 的队列优先级
- 将 operation 加入到队列 downloadQueue 中,队列(NSOperationQueue)会自动管理 operation 的执行
- 如果 operation 执行顺序是先进后出,就设置 operation 依赖关系(先加入的依赖于后加入的),并记录最后一个 operation(lastAddedOperation)
3.1.2 SDWebImageDownloaderOperation
当创建的 SDWebImageDownloaderOperation 对象被加入到 downloader 的 downloadQueue 中时,该对象的 -start 方法就会被自动调用。
-start 方法中首先创建了用来下载图片数据的 NSURLSession,然后开启 connection,同时发出开始图片下载的 SDWebImageDownloadStartNotification 通知,为了防止非主线程的请求被 kill 掉,这里开启 runloop 保活,直到请求返回。
一张图片的数据下载是由一个 NSConnection 对象来完成的,这个对象的整个生命周期(从创建到下载结束)又是由 SDWebImageDownloaderOperation 来控制的,将 operation 加入到 operation queue 中就可以实现多张图片同时下载了。
简单概括成一句话就是,NSURLSession 负责网络请求,NSOperation 负责多线程。
(为什么要用runloop :https://blog.ibireme.com/2015/05/18/runloop/)
3.2 SDImageCache
3.2.1 SDImageCache 管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,所以不会阻塞主线程,影响用户体验。为什么需要缓存?
- 以空间换时间,提升用户体验:加载同一张图片,读取缓存是肯定比远程下载的速度要快得多的
- 减少不必要的网络请求,提升性能,节省流量:一般来讲,同一张图片的 URL 是不会经常变化的,所以没有必要重复下载。另外,现在的手机存储空间都比较大,相对于流量来,缓存占的那点空间算不了什么。
3.2.2 SDImageCache 的内存缓存是通过一个继承 NSCache 的 AutoPurgeCache 类来实现的,NSCache 是一个类似于 NSMutableDictionary 存储 key-value 的容器,主要有以下几个特点:
- 自动删除机制:当系统内存紧张时,NSCache会自动删除一些缓存对象
- 线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
- 不同于 NSMutableDictionary,NSCache存储对象时不会对 key 进行 copy 操作
3.2.3 SDImageCache 的磁盘缓存是通过异步操作 NSFileManager 存储缓存文件到沙盒来实现的。
3.2.4 磁盘清理
(1).清扫磁盘缓存(clean)和清空磁盘缓存(clear)是两个不同的概念,清空是删除整个缓存目录,清扫只是删除部分缓存文件。
(2).清扫磁盘缓存有两个指标:一是缓存有效期,二是缓存体积最大限制。SDImageCache中的缓存有效期是通过 maxCacheAge 属性来设置的,默认值是 1 周,缓存体积最大限制是通过 maxCacheSize 来设置的,默认值为 0。
一般在应用即将终止时和退到后台时,都会调用 -cleanDiskWithCompletionBlock: 方法来异步清扫缓存,清扫磁盘缓存的逻辑是,先遍历所有缓存文件,并根据文件的修改时间来删除过期的文件,同时记录剩下的文件的属性和总体积大小,如果设置了 maxCacheAge 属性的话,接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面),最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。
(3).清理内存缓存,一般在系统抛出内存警告的时候,SDWebImage会自动调用清除内存缓存方法,等系统抛出内存警告可能有点困难,所以可以根据需求,自己调用[[SDImageCache sharedImageCache] clearMemory]; ,不过这个方法只是清除了SDWebImage的缓存,并没有清除系统缓存。如果需要清楚系统缓存可以调用
[[NSURLCache sharedURLCache] removeAllCachedResponses];来实现。
3.3 SDWebImageManager
SDWebImageManager是图片加载管理器,主要是将下载和缓存两个功能结合起来,才算是一个完整的图片加载器。
SDWebImageManager 的核心任务是由 -downloadImageWithURL:options:progress:completed: 方法来实现的,这个方法中先会从 SDImageCache 中读取缓存,如果有缓存,就直接返回缓存,如果没有就通过 SDWebImageDownloader 去下载,下载成功后再保存到缓存中去,然后再回调 completedBlock。其中 progressBlock 的回调是直接交给了 SDWebImageDownloader 的 progressBlock 来处理的。
SDWebImageManager 在读取缓存和下载之前会创建一个 SDWebImageCombinedOperation 对象,这个对象是用来管理缓存读取操作和下载操作的,SDWebImageCombinedOperation 对象有 3 个属性:
- cancelled:用来取消当前加载任务的
- cancelBlock:用来移除当前加载任务和取消下载任务的
- cacheOperation:用来取消读取缓存操作
3.4 UIImageView+WebCache
UIImageView+WebCache就是我们平时最常用的图片加载,是通过调用 UIImageView+WebCache 的 -sd_setImageWithURL:... 系列方法来加载的,UIImageView+WebCache 实际上是将 SDWebImageManager 封装了一层,内部针对 UIImageView 做了一些处理,使用起来更方便、更直接、更简单。
UIImageView+WebCache 的核心逻辑都在 - sd_setImageWithURL:placeholderImage:options:progress:completed: 方法中,为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,该方法中首先通过调用 -sd_cancelCurrentImageLoad 方法取消这个 UIImageView 当前的下载任务,然后设置了占位图,如果 url 不为 nil,接着就调用 SDWebImageManager 的 -downloadImage... 方法开始加载图片,并将这个加载任务 operation 保存起来,用于后面的 cancel 操作。图片获取成功后,再重新设置 imageView 的 image,并回调 completedBlock。
需要注意的是为了防止多个异步加载任务同时存在时,可能出现互相冲突和干扰,每个 UIImageView 的图片加载任务都会保存成一个 Associated Object,方便需要时取消任务。这个 Associated Object 的操作是在 UIView+WebCacheOperation 中实现的,因为除了 UIImageView 用到图片加载功能之外,还有 UIButton 等其他类也用到了加载远程图片的功能,所以需要进行同样的处理,这样设计实现了代码的复用。
四、遇到的问题
- 问题 1:使用SDWebImage加载较多图片时,会出现内存警告;
解决方案:定期调用 [[SDImageCache sharedImageCache] setValue:nil forKey:@"memCache"]; - 问题 2:图片刷新问题:SDWebImage 在进行缓存时忽略了所有服务器返回的 caching control 设置,并且在缓存时没有做时间限制,这也就意味着图片 URL 必须是静态的了,要求服务器上一个 URL 对应的图片内容不允许更新。但是如果存储图片的服务器不由自己控制,也就是说 图片内容更新了,URL 却没有更新,这种情况怎么办?
解决方案:在调用 sd_setImageWithURL: placeholderImage: options:方法时设置 options 参数为SDWebImageRefreshCached,这样虽然会降低性能,但是下载图片时会照顾到服务器返回的 caching control。 - 问题 3:在加载图片时,如何添加默认的 progress indicator ?
解决方案:在调用 -sd_setImageWithURL:方法之前,先调用下面的方法:
[imageView sd_setShowActivityIndicatorView:YES];
[imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];
五、总结
SDWebImage作为一个优秀的图片加载框架,提供的使用方法和接口对开发者来说非常友好。
其内部实现多是采用Block的方式来实现回调,代码阅读起来可能没有那么直观。
内部有用到了很多好的技术,比如,GCD,线程加锁等,可供我们学习。
SDWebImage中有两个比较好用的宏定义定义之dispatch_main_sync_safe和dispatch_main_async_safe。
define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
SDWebImage频繁运用了runtime中的关联函数,用关联函数关联一个对象,称关联对象,关联对象相当于实例变量,在类别里面,不能创建实例变量,关联对象就可以解决这种问题。
主要函数有:
objc_setAssociatedObject 相当于 setValue:forKey 进行关联value对象
objc_getAssociatedObject 用来读取对象
objc_AssociationPolicy 属性 是设定该value在object内的属性,即 assgin, (retain,nonatomic)...等
objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。
相关参数:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
id object :表示关联者,是一个对象,变量名理所当然也是object
const void *key :获取被关联者的索引key
id value :被关联者,这里是一个block
objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使 用OBJC_ASSOCIATION_RETAIN_NONATOMIC。
相关协议:
OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN等价于@property(strong,atomic)。
OBJC_ASSOCIATION_COPY等价于@property(copy, atomic)。
六、SDWebImage3.8.2与4.0的区别
SDWebImage4.0是一个大版本,在结构上和API方面都有所改动。
区别:
1、新增targetURL,存放原始图像url;
2、新增一个FLAnimatedImageView+WebCache类,主要在动图支持上做了改进,尤其是 GIF;
3、SDWebImage4.0新增一个UIView+WebCache类中,将UIImageView+WebCache类中的方法移动到UIView+WebCache类中,UIImageView对象依然响应这些方法。
SDWebImage4.0新增了很多方法,详细的在GitHub上看官方文档。
https://github.com/rs/SDWebImage/blob/master/Docs/SDWebImage-4.0-Migration-guide.md