SDWebImage 简要介绍
SDWebImage 是一款性能卓越、流行度高的网络图片下载框架。对 SDWebImage 框架代码解读的文章数不胜数,这里只胡说八道框架的设计思想。
[注]:推荐南峰子的博客 《源码解析:SDWebImage 实现分析》
《计算机组成与设计:硬件/软件接口》 CPU 和 存储管理讲的很好。第三版、第四版均可。
框架设计思想剖析
SDWebImage 加载图片的流程
流程参考这里 《那些著名或非著名的 iOS 面试题 - 前编》
1.入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
2.进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.
3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
4.SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
5.如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
6.根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
7.如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
8.如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
9.共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
10.图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
11.connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
12.connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
13.图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
14.在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
15.imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
16.通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
17.将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
18.SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
总结
总体设计思路
- 内存中维护一个图片缓存作为一级缓存,辅存中维护一个图片缓存作为二级缓存。同时共享的下载器缓存下载任务,减少下载时的资源消耗。进行文件查找、文件下载、图片解码、文件读写操作等都使用后台线程完成,减少对程序性能的影响。
设计经验
- 设计模式
Observer、Delegate 、Singleton 的混合使用,使各功能组件的配合更加高效。这些简单的设计模式也没有那么不堪,关键是不乱套、不滥用。 - 性能优化
- 功能明确分开
文件查找、文件下载、文件读写、图片解码等操作都交由对应的功能组件完成,代码清晰,结构明确。
- 功能明确分开
- 多线程的使用
处理必要的 UI 数据交换,其他的操作都尽可能的在后台线程完成,减少对主线程的影响。 - 架构设计
- 多级缓存的灵活使用
内存中缓存图片作为一级缓存,辅存中缓存图片作为二级缓存;还对可能出现资源损耗的下载操作实现一次缓存。
[注] 缓存的使用不是越多多好,请慎重选择使用。 - 功能组件的设置
将功能组件分开,各司其职。做到代码结构清晰,功能有条不紊。(这里将‘文件下载’、‘文件IO’的操作称为功能组件并不合适,只是为了便于理解思路。)
题外话
- 多线程
什么时候应该使用多线程? - 完成线程或者任务间的同步操作
- 减少多主线程的性能影响,提高反应速度。使用后台线程。
多线程中应该考虑什么?
添加什么方式的任务到队列中?同步方式还是异步方式?(同步任务和异步任务的区别是什么?为是否阻塞当前线程,以同步方式添加任务到队列中会阻塞当前线程)
怎么去执行线程?串行还是并行?
3. 线程安全的?
典型的解决线程安全问题的方法:使用线程锁实现。对应各有优缺点。
这里推荐解决线程安全问题几篇文章:1.Objc 线程安全类的设计系列文章 2.多线程开发之线程安全 3.简书上的 iOS 多线程开发-线程安全
[注]1. 一个典型的 GCD 死锁问题
// 1 打印当前线程号
NSLog("之前 - %@", NSThread.currentThread())
// 2 在当前线程中,向主队列中添加一个同步任务
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
NSLog("sync - %@", NSThread.currentThread())
})
// 3 没有切换线程,在主线程中打印
NSLog("之后 - %@", NSThread.currentThread())
结果:只能输出:之前 - XX
步骤分解:
1 在主线程中先获取主线程队列,以同步的方式添加一个任务到主线程队列中。
2 主线程队列中的任务会被取出串行执行,同时因为是同步方式,所以会阻塞当前主队列线程
3 主线程被阻塞,不能被执行
[注] 2. 实现线程间数据同步或者通信
两种实现方式:GCD 实现 和 NSOperation 实现。
- 缓存
强烈推荐阅读《计算机组成与设计:硬件与软件接口》。 - 架构
好的架构是一步一步进化出来的,而不是一次就完成所有架构的。但是不可否认,编写代码时,应该遵循良好的编码规范和设计方法。
小技巧
- 不同网络状态下的图片加载处理
场景:3/4G 和 WiFi 环境下的高清图片和普通质量图片的显示问题
SDWebImage 会默认加载缓存中的高清图片,为了节省用户的流量,应该对网络状态进行判断,控制加载不同质量的图片。但是这还没有完,有些用户默认设置在 3/4 G 网络环境下加载普通质量的图片(设置在偏好设置中),该怎么进行省流处理?
例子:《iOS 开发-你真的会用 SDWebImage?》 - 下载进度展示
场景:正在下载的 GIF 图片,被点击浏览大图,怎么保证小图和大图中显示的下载进度是一致的? - SDWebImage 设置图片圆角,避免离屏渲染
场景:设置图片圆角,既要快,又不能有离屏渲染。
key: 在 ImageView 的分类中,开启后台线程使用 Q2D 进行图片绘制。
任务
- 自己实现一个文件下载器
例子:《高效图片轮播 - 两个 UIImageView 实现》 - 断点续传
key:使用 HTTP 中的 HEAD 方法,设置每次下载的文件长度。
其他的方法请评论告知我,谢谢。