SDWebImage(v3.7.6) 源码学习

栏目: IOS · 发布时间: 7年前

内容简介:[一个个人学习记录,源码版本较落后,参考意义不大,今年会更新对 SD 最新版本的源码阅读]在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调单独使用 SDImageCache 异步缓存图片

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. 模块

SDWebImage(v3.7.6) 源码学习
  • 下载(SDWebImageDownloader)
  • 缓存(SDImageCache)
  • 将缓存和下载的功能组合起来(SDWebImageManager)
  • 封装成 UIImageView/UIButton 的分类方法(UIImageView+WebCache 等)

MKAnnotationView :地图大头针 属于 MapKit 框架的一个类,继承自 UIView,是用来展示地图上的 annotation 信息的,它有一个用来设置图片的属性 image。官方文档 MKMapView 的使用

2. 目录结构

SDWebImage(v3.7.6) 源码学习

3. 核心逻辑

SDWebImage(v3.7.6) 源码学习

流程图图源: 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. 调用时序图

SDWebImage(v3.7.6) 源码学习

三、实现策略

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

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
sd_decompressedAndScaledDownImageWithImage:

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Creative Curve

The Creative Curve

Allen Gannett / Knopf Doubleday Publishing Group / 2018-6-12

Big data entrepreneur Allen Gannett overturns the mythology around creative genius, and reveals the science and secrets behind achieving breakout commercial success in any field. We have been s......一起来看看 《The Creative Curve》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具