2018.9.24 HanniyaZhang
[一个个人学习记录,源码版本较落后,参考意义不大,今年会更新对 SD 最新版本的源码阅读]
一、 使用
1. 使用 UIImageView+WebCache
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
复制代码
在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
... completion code here ...
}];
复制代码
2. 单独使用 Manager/Downloader/Cache
单独使用 SDWebImageManager
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// do something with image
}
}];
复制代码
单独使用 SDWebImageDownloader 异步下载图片
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// do something with image
}
}];
复制代码
单独使用 SDImageCache 异步缓存图片 SDImageCache
支持内存缓存和异步的磁盘缓存(可选),可以使用单例,也可以创建一个有独立命名空间的 SDImageCache
实例。 添加缓存的方法:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
复制代码
默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
复制代码
读取缓存时可以使用 queryDiskCacheForKey:done:
方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];
复制代码
二、结构
1. 模块
- 下载(SDWebImageDownloader)
- 缓存(SDImageCache)
- 将缓存和下载的功能组合起来(SDWebImageManager)
- 封装成 UIImageView/UIButton 的分类方法(UIImageView+WebCache 等)
MKAnnotationView :地图大头针 属于 MapKit 框架的一个类,继承自 UIView,是用来展示地图上的 annotation 信息的,它有一个用来设置图片的属性 image。 官方文档 MKMapView 的使用
2. 目录结构
3. 核心逻辑
流程图图源:J_Knight_:SDWebImage源码解析,讲解清晰,十分感谢。
在一个UIImageView调用: sd_setImageWithURL: placeholderImage: options: progress: completed:
- 取消当前正在进行的加载任务 operation
- 设置占位图
- 如果 URL 不为
nil
,就通过Manager
单例开启图片加载的operation
在downloadImageWithURL:options:progress:completed:
中会先拿图片缓存的 key (默认是图片 URL)去 Cache
单例中读取内存缓存,如果有,就返回给 Manager
; 如果没有,就开启异步线程,拿经过 MD5 处理的 key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存,然后再返回给 Manager
。 downloadImageWithURL
会返回一个 SDWebImageCombinedOperation
对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。
如果内存和磁盘缓存中都没有图片,Manager
就会调用 Downloader
单例的 -downloadImageWithURL: options: progress: completed:
方法去下载,先将传入的 progressBlock
和 completedBlock
保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest
对象和一个 SDWebImageDownloaderOperation
对象,并将该 SDWebImageDownloaderOperation
对象添加到downloadQueue
来启动异步下载任务。
DownloaderOperation
中包装了一个 NSURLConnection
的网络请求,并通过 runloop 来保持 NSURLConnection
在 start 后、收到响应前不被干掉,下载图片时,监听 NSURLConnection
回调的 -connection:didReceiveData:
方法中会负责 progress 相关的处理和回调,- connectionDidFinishLoading:
方法中会负责将 data 转为 image,以及图片解码操作,并最终回调 completedBlock。
DownloaderOperation
中的图片下载请求完成后,会回调给 Downloader
,然后 Downloader
再回调给 Manager
,Manager
将图片缓存到内存和磁盘上(可选),并回调给 UIImageView
,UIImageView
中再回到主线程设置 image
属性。
4. 调用时序图
三、实现策略
1. Cache 缓存策略
SDImageCache
管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,不会阻塞主线程。
为什么需要缓存?
- 以空间换时间,速度更快
- 减少不必要的网络请求,节省流量
1.1 内存缓存
内存缓存通过一个继承 NSCache
的 AutoPurgeCache
类实现。
NSCache (NSCache官方文档) NSCache
是一个类似于 NSMutableDictionary
存储 key-value 的容器,特点如下:
- 自动删除机制:当系统内存紧张时,
NSCache
会自动删除一些缓存对象 - 线程安全:从不同线程对同一个
NSCache
对象进行增删改查时,不需加锁 - 不同于
NSMutableDictionary
,NSCache
存储对象时不会对 key 进行 copy 操作
1.2 磁盘缓存
磁盘缓存通过异步操作 NSFileManager
存储缓存文件到沙盒实现。
1.3 缓存操作
- 初始化
-init
方法中默认调用了-initWithNamespace:
方法,-initWithNamespace:
方法又调用了-makeDiskCachePath:
方法来初始化缓存目录路径, 同时还调用了-initWithNamespace:diskCacheDirectory:
方法来实现初始化。 初始化方法调用栈:
-init
-initWithNamespace:
-makeDiskCachePath:
-initWithNamespace:diskCacheDirectory:
复制代码
-initWithNamespace:diskCacheDirectory:
初始化实例变量、属性,设置属性默认值,并根据 namespace 设置完整的缓存目录路径,除此之外还添加了通知观察者,用于内存紧张时清空内存缓存,以及程序终止运行时和程序退到后台时清扫磁盘缓存。
- 写入缓存
storeImage:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk{
}
复制代码
写入的参数有三个。 添加内存缓存时,先计算像素,再加进去 添加磁盘缓存时,如果需要在存储之前将传进来的 image
转成 NSData
,而不是直接使用传入的 imageData
,那么就要按不同的图片格式来转成对应的 NSData
对象。
NSData 用来包装数据,存储的是二进制数据,屏蔽了数据之间的差异,文本、音频、图像等数据都可用NSData来存储。
判断图片格式:根据是否有 alpha 通道以及 imageData
的前8位字节 判断图片格式详解 如果 imageData 为 nil,就根据 image 是否有 alpha 通道来判断图片是否是 PNG 格式的 如果 imageData 不为 nil,就根据 imageData 的前 8 位字节来判断是不是 PNG 格式,因为 PNG 图片有一个唯一签名,前 8 位字节是(十进制): 137 80 78 71 13 10 26 10
拿到 imageData 后,借助 NSFileManager 将图片二进制存储到沙盒,存储的文件名是对 key 进行 MD5 处理后生成的字符串。 默认沙盒路径: Library - Caches
iOS的沙盒机制 SandBox 一种安全体制,规定应用程序只能在为该应用创建的文件夹内读取文件,不能访问其他地方的内容。保存所有的非代码文件,如图片,声音,属性列表和文本文件等。 应用程序向外请求或接收数据都需要经过权限认证。 默认情况下,每个沙盒含有3个文件夹:Documents, Library 和 tmp
- Documents:保存应用运行时生成的需要持久化的数据,iTunes会备份该目录。
- Library:存储程序的默认设置或其它状态信息;
- Caches:保存应用运行时生成的需要持久化的数据,一般存储体积大、不需要备份的非重要数据。iTunes不会备份此目录,此目录文件不会在应用退出时删除。
- Preferences:偏好设置文件,iTunes会备份该目录。
- tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes不会备份该目录。
- 读取缓存
queryDiskCacheForKey
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
}
复制代码
返回的是一个 NSOperation 对象 这个方法会先读取内存缓存,如果没有再读取磁盘缓存。 读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths
(也就是 bundle)中去找。 找到之后,对数据进行转换,后面的图片处理步骤跟图片下载成功后的图片处理步骤一样——先将 data 转成 image,再进行根据文件名中的 @2x、@3x 进行缩放处理,如果需要解压缩,最后再解压缩一下。
- 清扫磁盘缓存 每新加载一张图片,就会新增一份缓存,所以需要定期清除部分缓存。
- 清扫磁盘缓存 (clean):删除部分缓存文件
- 清空磁盘缓存 (clear):删除整个缓存目录
指标
- 缓存有效期:通过
maxCacheAge
属性设置,默认一周- 缓存体积最大限制:通过
maxCacheSize
来设置的,默认为 0。
SDImageCache
在初始化时添加了通知观察者,所以在应用即将终止时和退到后台时,都会调用 -cleanDiskWithCompletionBlock:
方法来异步清扫缓存。 清扫磁盘缓存(clean): 遍历所有缓存文件,如果设置了 maxCacheAge
(最大缓存不过期时间) 属性的话,先删掉过期的文件,同时记录文件的属性和总体积大小,把文件按修改时间从早到晚排序,再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。
2. Downloader 下载策略
主要任务
- 异步下载图片管理
- 图片加载优化
具体实现: +initialize
中主要是通过注册通知 让SDNetworkActivityIndicator
监听下载事件,来显示和隐藏状态栏上的 network activity indicator。 为了让 SDNetworkActivityIndicator
文件可以不用导入项目中来(如果不要的话),这里使用了 runtime 的方式来实现动态创建类以及调用方法。
+ (void)initialize {
if (NSClassFromString(@"SDNetworkActivityIndicator")) {
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
# 先移除通知观察者 SDNetworkActivityIndicator
# 再添加通知观察者 SDNetworkActivityIndicator
}
}
复制代码
+sharedDownloader
方法中调用了 -init
方法来创建一个单例,-init
方法中做了一些初始化设置和默认值设置,包括设置最大并发数(6)、下载超时时长(15s)等。
核心方法: - downloadImageWithURL: options: progress: completed:
方法 首先通过调用 -addProgressCallback: andCompletedBlock: forURL: createCallback:
方法来保存每个 url 对应的回调 block -addProgressCallback: ...
方法先进行错误检查,判断 URL 是否为空,然后再将 URL 对应的 progressBlock
和 completedBlock
保存到 URLCallbacks
属性中。
URLCallbacks
属性是一个 NSMutableDictionary
对象,key 是图片的 URL,value 是一个数组,包含每个图片的多组回调信息。
因为可能同时下载多张图片,所以就可能出现多个线程同时访问 URLCallbacks
属性的情况。为了保证线程安全,所以这里使用了 dispatch_barrier_sync
来分步执行添加到 barrierQueue
中的任务,这样就能保证同一时间只有一个线程能对 URLCallbacks
进行操作。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
//1. 判断 url 是否为 nil,如果为 nil 则直接回调 completedBlock,返回失败的结果,然后 return,因为 url 会作为存储 callbacks 的 key
//2. 处理同一个 URL 的多次下载请求(MARK: 使用 dispatch_barrier_sync 函数来保证同一时间只有一个线程能对 URLCallbacks 进行操作):
//3. 从属性 URLCallbacks(一个字典) 中取出对应 url 的 callBacksForURL(这是一个数组,因为可能一个 url 不止在一个地方下载)
//4. 如果没有取到,也就意味着这个 url 是第一次下载,那就初始化一个 callBacksForURL 放到属性 URLCallbacks 中
//5. 往数组 callBacksForURL 中添加 包装有 callbacks(progressBlock 和 completedBlock)的字典
//6. 更新 URLCallbacks 存储的对应 url 的 callBacksForUR
}
复制代码
如果这个 URL 是第一次被下载,就要回调 createCallback
,createCallback
主要做的就是创建并开启下载任务
createCallback
方法中调用了 - [SDWebImageDownloaderOperation initWithRequest: options: progress:]
方法来创建下载任务 SDWebImageDownloaderOperation
。
5. 其他
5.1 SDWebImageDecoder
由于 UIImage 的 imageWithData
函数是每次画图的时候才将 Data 解压成 ARGB 的图像,所以在每次画图的时候都会有一个解压操作,这样虽然只有瞬时的内存需求,但是效率很低。 为了提高效率,通过 SDWebImageDecoder 将包装在 Data 下的资源画在另外一张图片上,这样这张新图片就不再需要重复解压了,是空间换时间的做法。
图片的解码实际是将图片的二进制数据转换成像素数据的过程,SD 对图片进行重新绘制,得到一张位图。 显示图片需要 RGBA 的色彩空间(什么是 RGBA ?),但是 PNG 和 JPEG 自身的格式非 RGBA。所以创建一个 BitmapImage,先在非UI线程渲染图片,作为预解码,然后拿到UIImage去显示。 iOS 图片解码
5.2 SDWebImagePrefetcher
可以预先下载,但是下载是低优先级的。
四、TIPS
用 NSOperation 进行操作管理
1. NSOperation 的特性
- 状态
State
operation 的执行过程: isReady -> isExecuting -> isFinished
通过 keypath 的 KVO 通知来隐式的得到 state ,而不是显式的通过一个 state 的属性。当一个 operation 已经准备就绪,将要被执行时,它会为 isReadykeyPath 发送一个KVO的通知,对应的属性值也会变为YES.
为了构造一致的状态,每个属性都与其他属性相互排斥: isReady : 如果 operation 已经做好了执行的准备返回YES,如果它所依赖的操作存在一些未完成的初始化步骤则返回NO。 isExecuting :如果 operation 正在执行它的任务返回YES,否则返回NO。 isFinished : 任务成功的完成了执行,或者中途被 Cancel ,返回YES。
NSOperationQueue 只会把 isFinished 为 YES 的 operation 踢出队列, isFinished 为 NO 的永远不会被移除,所以实现时要保证其正确性,避免死锁发生
- 取消
Cancellation
取消一个 operation 的两种情况:- 显式的调用cancel方法
- operation 依赖的其他 operation 执行失败
NSOperation 的被取消也是通过 isCancelledkeypath 的 KVO 来获得。当 NSOperation 的子类覆写 cancel 方法时,注意清理掉内部分配的资源。 特别注意的是,这时 isCancelled 和 isFinished 的值都变为了 YES, isExecuting 为值变为NO。
cancel : 带一个”l” 表示方法 (动词) isCancelled : 带两个”l”表示属性(形容词)
-
优先级
Priority
设置 queuePriority 属性就可以提升和降低 operation 的优先级, queuePriority 属性可选的值如下: NSOperationQueuePriorityVeryHigh NSOperationQueuePriorityHigh NSOperationQueuePriorityNormal NSOperationQueuePriorityLow NSOperationQueuePriorityVeryLow 另外,operation 可以指定一个 threadPriority 值,它的取值范围是0.0到1.0,1.0代表最高的优先级。 queuePriority:决定执行顺序的优先级 threadPriority:决定 operation 开始执行之后分配的计算资源的多少 -
依赖
Dependencies
如果需要把一个大的任务分成多个子任务,可以使用依赖,来保证先后执行顺序。 B 操作如果依赖于 A,则必须在 A operation 的 isFinished 为 YES 的时候才会开始执行。 【避免循环依赖产生死锁】
[resizingOperation addDependency:networkingOperation];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];
复制代码
- completionBlock 当一个NSOperation完成之后,就会精确地只执行一次completionBlock。 Eg.当一个网络请求结束之后,可以在 completionBlock 里处理返回的数据。
2. Manager 中如何使用 NSOperation
SDWebImageCombinedOperation 当 url 被正确传入之后, 会实例一个非常奇怪的 “operation”, 它其实是一个遵循 SDWebImageOperation 协议的 NSObject 的子类. 而这个协议也非常的简单:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
复制代码
这里仅仅是将这个 SDWebImageOperation 类包装成一个看着像 NSOperation 其实并不是 NSOperation 的类, 而这个类唯一与 NSOperation 的相同之处就是它们都可以响应 cancel 方法. (不知道这句看似像绕口令的话, 你看懂没有, 如果没看懂..请多读几遍). 而调用这个类的存在实际是为了使代码更加的简洁, 因为调用这个类的 cancel 方法, 会使得它持有的两个 operation 都被 cancel.
// SDWebImageCombinedOperation
// cancel #1
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
复制代码
而这个类, 应该是为了实现更简洁的 cancel 操作而设计出来的.
3. Downloader 中如何使用 NSOperation
每张图片的下载都会发出一个异步的 HTTP 请求,由 DownloaderOperation
管理。
DownloaderOperation
继承 NSOperation
,遵守 SDWebImageOperation
、NSURLConnectionDataDelegate
协议。
SDWebImageOperation
协议只定义了一个方法 -cancel
,用来取消 operation。
当创建的 DownloaderOperation
对象被加入到 downloader
的 downloadQueue
中时,该对象的 -start
方法就会被自动调用。 -start
方法中首先创建了用来下载图片数据的 NSURLConnection
,然后开启 connection,同时发出开始图片下载的 当图片的所有数据下载完成后,Downloader
传入的 completionBlock
被调用,图片下载结束。
因此图片的数据下载是由一个 NSConnection
对象来完成的,这个对象的整个生命周期(从创建到下载结束)是由 DownloaderOperation
来控制的,将 operation
加入到 operation queue
中就可以实现多张图片同时下载了
其他小 TIPS
NS_OPTIONS 枚举类型的使用 使用 NS_OPTIONS 位运算枚举类型,可同时 通过“与”运算符,可以判断是否设置了某个枚举选项,因为每个枚举选择项中只有一位是1,其余位都是 0,所以只有参与运算的另一个二进制值在同样的位置上也为 1,与 运算的结果才不会为 0. Eg. 0101 (相当于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache) & 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache) = 0100 (> 0,也就意味着 option 参数中设置了 SDWebImageDownloaderUseNSURLCache)
初始化 一般来说,一个管理类都有一个全局的单例对象,根据业务需求设计不同的初始化方法。在设计类的时候,应该通过合理的初始化方法告诉别的开发者,该类应该如何创建。
- (nonnull instancetype)sharedImageCache 单例
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns 通过制定的namespace来初始化
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER 指定namespace和path.
使用@synchronized: 在 Manager 对 failedURLs
和 runningOperations
做操作时均使用了@synchronized,在新版本里换成了 GCD 实现
下载高分辨率图,导致内存暴增的解决办法
五、反思
1. 与最新版本(v4.4.2)
功能扩展
- 使用
FLAnimatedImage
来处理动图 - 增加了
SDImageCacheConfig
对缓存进行配置,可以选择是否解压缓存、iCloud、最大缓存大小。 - 大图缩放逻辑:
sd_decompressedAndScaledDownImageWithImage:
避免要缩放的图片太大,采用的方式是将图片分割成一系列大小的小方块,然后每个方块去获取 Image 并 draw 到目标 BitmapContext 上
2. 缓存优化
对于缓存如何做优化?
- 增删查找的速度
- 提高命中率
(1). -> LRU -> LRU+FIFO (2). 缓存模糊匹配 针对同一图片,不同大小的请求。如果缓存中有更大的图片,也视为命中缓存。