SDWebImage(v3.7.6) 源码学习

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 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存,然后再返回给 ManagerdownloadImageWithURL会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。

如果内存和磁盘缓存中都没有图片,Manager 就会调用 Downloader 单例的 -downloadImageWithURL: options: progress: completed: 方法去下载,先将传入的 progressBlockcompletedBlock 保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到downloadQueue 来启动异步下载任务。

DownloaderOperation 中包装了一个 NSURLConnection 的网络请求,并通过 runloop 来保持 NSURLConnection 在 start 后、收到响应前不被干掉,下载图片时,监听 NSURLConnection 回调的 -connection:didReceiveData: 方法中会负责 progress 相关的处理和回调,- connectionDidFinishLoading: 方法中会负责将 data 转为 image,以及图片解码操作,并最终回调 completedBlock。

DownloaderOperation 中的图片下载请求完成后,会回调给 Downloader,然后 Downloader 再回调给 ManagerManager将图片缓存到内存和磁盘上(可选),并回调给 UIImageViewUIImageView 中再回到主线程设置 image 属性。

4. 调用时序图

时序图

三、实现策略

1. Cache 缓存策略

SDImageCache 管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,不会阻塞主线程。

为什么需要缓存?

  • 以空间换时间,速度更快
  • 减少不必要的网络请求,节省流量

1.1 内存缓存

内存缓存通过一个继承 NSCacheAutoPurgeCache 类实现。

NSCache (NSCache官方文档) NSCache 是一个类似于 NSMutableDictionary 存储 key-value 的容器,特点如下:

  • 自动删除机制:当系统内存紧张时,NSCache会自动删除一些缓存对象
  • 线程安全:从不同线程对同一个 NSCache 对象进行增删改查时,不需加锁
  • 不同于 NSMutableDictionaryNSCache存储对象时不会对 key 进行 copy 操作

1.2 磁盘缓存

磁盘缓存通过异步操作 NSFileManager 存储缓存文件到沙盒实现。

1.3 缓存操作

  1. 初始化 -init 方法中默认调用了 -initWithNamespace: 方法,-initWithNamespace: 方法又调用了 -makeDiskCachePath: 方法来初始化缓存目录路径, 同时还调用了 -initWithNamespace:diskCacheDirectory: 方法来实现初始化。 初始化方法调用栈:
-init
    -initWithNamespace:
        -makeDiskCachePath: 
        -initWithNamespace:diskCacheDirectory:
复制代码

-initWithNamespace:diskCacheDirectory: 初始化实例变量、属性,设置属性默认值,并根据 namespace 设置完整的缓存目录路径,除此之外还添加了通知观察者,用于内存紧张时清空内存缓存,以及程序终止运行时和程序退到后台时清扫磁盘缓存。

  1. 写入缓存 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不会备份该目录。
  1. 读取缓存 queryDiskCacheForKey
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
}
复制代码

返回的是一个 NSOperation 对象 这个方法会先读取内存缓存,如果没有再读取磁盘缓存。 读取磁盘缓存时,会先从沙盒中去找,如果沙盒中没有,再从 customPaths (也就是 bundle)中去找。 找到之后,对数据进行转换,后面的图片处理步骤跟图片下载成功后的图片处理步骤一样——先将 data 转成 image,再进行根据文件名中的 @2x、@3x 进行缩放处理,如果需要解压缩,最后再解压缩一下。

  1. 清扫磁盘缓存 每新加载一张图片,就会新增一份缓存,所以需要定期清除部分缓存。
  • 清扫磁盘缓存 (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 对应的 progressBlockcompletedBlock 保存到 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 是第一次被下载,就要回调 createCallbackcreateCallback 主要做的就是创建并开启下载任务

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 里处理返回的数据。

参考:Mattt - NSOperation

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,遵守 SDWebImageOperationNSURLConnectionDataDelegate 协议。

SDWebImageOperation 协议只定义了一个方法 -cancel,用来取消 operation。

当创建的 DownloaderOperation 对象被加入到 downloaderdownloadQueue 中时,该对象的 -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 对 failedURLsrunningOperations做操作时均使用了@synchronized,在新版本里换成了 GCD 实现

下载高分辨率图,导致内存暴增的解决办法

五、反思

1. 与最新版本(v4.4.2)

功能扩展

  • 使用 FLAnimatedImage 来处理动图
  • 增加了 SDImageCacheConfig 对缓存进行配置,可以选择是否解压缓存、iCloud、最大缓存大小。
  • 大图缩放逻辑:sd_decompressedAndScaledDownImageWithImage: 避免要缩放的图片太大,采用的方式是将图片分割成一系列大小的小方块,然后每个方块去获取 Image 并 draw 到目标 BitmapContext 上

2. 缓存优化

对于缓存如何做优化?

  • 增删查找的速度
  • 提高命中率

(1). -> LRU -> LRU+FIFO (2). 缓存模糊匹配 针对同一图片,不同大小的请求。如果缓存中有更大的图片,也视为命中缓存。

猜你喜欢

转载自juejin.im/post/5c32ff4ce51d4551140d7276