内容简介:[一个个人学习记录,源码版本较落后,参考意义不大,今年会更新对 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. 模块
- 下载(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
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 里处理返回的数据。
参考: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
,遵守 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 sd_decompressedAndScaledDownImageWithImage:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
测试驱动的JavaScript开发
Christian Johansen / 赵勇、程德、凌杰、高博 / 机械工业出版社 / 2012-2-9 / 69.00元
本书是一本完整的、基于最佳实践的JavaScript敏捷测试指南,同时又有着测试驱动开发方法(TDD)所带来的质量保证。领先一步的JavaScript敏捷开发者Christian Johansen的讨论涵盖了将最先进的自动化测试用于JavaScript开发环境的方方面面,带领读者走查整个开发的生命周期,从项目启动到应用程序部署。本书的主要内容包括:掌握自动化测试和TDD;构建有效的自动化测试工作流......一起来看看 《测试驱动的JavaScript开发》 这本书的介绍吧!