前不久做了一个生成快照的需求,其中用到 SDWebImage 来下载图片,在使用该框架的过程中也遇到了一些问题,索性正好就把 SDWebImage (v3.7.3) 源码细读了一下,学习一下其中的设计思想和技术点,为了梳理思路,顺便写下了这篇文章。
目录
简介
设计目的
特性
SDWebImage 与其他框架的对比
常见问题
用法
SDWebImage 4.0 迁移指南
实现原理
架构图
流程图
目录结构
核心逻辑
实现细节
- 图片下载
1.1 SDWebImageDownloader
1.2 SDWebImageDownloader
图片缓存——SDImageCache
图片加载管理器——SDWebImageManager
设置 UIImageView 的图片——UIImageView+WebCache
知识点
收获与疑问
启发与实践
延伸阅读
一、简介
1. 设计目的
SDWebImage 提供了 UIImageView、UIButton 、MKAnnotationView 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。
2. 特性
提供 UIImageView, UIButton, MKAnnotationView 的分类,用来显示网络图片,以及缓存管理
异步下载图片
异步缓存(内存+磁盘),并且自动管理缓存有效性
后台图片解压缩
同一个 URL 不会重复下载
自动识别无效 URL,不会反复重试
不阻塞主线程
高性能
使用 GCD 和 ARC
支持多种图片格式(包括 WebP 格式)
支持动图(GIF)
4.0 之前的动图效果并不是太好
4.0 以后基于 FLAnimatedImage加载动图
注:本文选读的代码是 3.7.3 版本的,所以动图加载还不支持 FLAnimatedImage。
3. SDWebImage 与其他框架的对比
利益相关:以下两篇文章都是 SDWebImage 的维护者所写,具有一定的主观性,仅供参考。
How is SDWebImage better than X?
iOS image caching. Libraries benchmark (SDWebImage vs FastImageCache)
4. 常见问题
问题 1:使用 UITableViewCell 中的 imageView 加载不同尺寸的网络图片时会出现尺寸缩放问题
解决方案:自定义 UITableViewCell,重写 -layoutSubviews 方法,调整位置尺寸;或者直接弃用 UITableViewCell 的 imageView,自己添加一个 imageView 作为子控件。
问题 2:图片刷新问题:SDWebImage 在进行缓存时忽略了所有服务器返回的 caching control 设置,并且在缓存时没有做时间限制,这也就意味着图片 URL 必须是静态的了,要求服务器上一个 URL 对应的图片内容不允许更新。但是如果存储图片的服务器不由自己控制,也就是说 图片内容更新了,URL 却没有更新,这种情况怎么办?
解决方案:在调用 sd_setImageWithURL: placeholderImage: options:方法时设置 options 参数为 SDWebImageRefreshCached,这样虽然会降低性能,但是下载图片时会照顾到服务器返回的 caching control。
问题 3:在加载图片时,如何添加默认的 progress indicator ?
解决方案:在调用 -sd_setImageWithURL:方法之前,先调用下面的方法:
[imageView sd_setShowActivityIndicatorView:YES];
[imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];
5. 用法
5.1 UITableView 中使用 UIImageView+WebCache
UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可
5.2 使用回调 blocks
在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@
"[http://www.domain.com/path/to/image.jpg](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 ...
}];
5.3 SDWebImageManager 的使用
UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。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
}
}];
5.4 单独使用 SDWebImageDownloader 异步下载图片
我们还可以单独使用 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
}
}];
5.5 单独使用 SDImageCache 异步缓存图片
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
}];
5.6 自定义缓存 key
有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key。
SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
};
6. SDWebImage 4.0 迁移指南
按照版本号惯例(Semantic Versioning),从版本号可以看出 SDWebImage 4.0 是一个大版本,在结构上和 API 方面都有所改动。
除了 iOS 和 tvOS 之外,SDWebImage 4.0 还支持更多的平台——watchOS 和 Max OS X。
借助 FLAnimatedImage 在动图支持上做了改进,尤其是 GIF。
1. 目录结构
Downloader
SDWebImageDownloader
SDWebImageDownloaderOperation
Cache
SDImageCache
Utils
SDWebImageManager
SDWebImageDecoder
SDWebImagePrefetcher
Categories
UIView+WebCacheOperation
UIImageView+WebCache
UIImageView+HighlightedWebCache
UIButton+WebCache
MKAnnotationView+WebCache
NSData+ImageContentType
UIImage+GIF
UIImage+MultiFormat
UIImage+WebP
Other
SDWebImageOperation(协议)
SDWebImageCompat(宏定义、常量、通用函数)
- 核心逻辑
下载 Source code(3.7.3),运行 pod install,然后打开 SDWebImage.xcworkspace,先 run 起来感受一下。
在了解细节之前我们先大概浏览一遍主流程,也就是最核心的逻辑。
我们从 MasterViewController 中的 [cell.imageView sd_setImageWithURL:url placeholderImage:placeholderImage]; 开始看起。
经过层层调用,直到 UIImageView+WebCache 中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:。该方法中,主要做了以下几件事:
取消当前正在进行的加载任务 operation
设置 placeholder
如果 URL 不为 nil,就通过 SDWebImageManager 单例开启图片加载任务 operation,SDWebImageManager 的图片加载方法中会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。
SDWebImageManager 的图片加载方法 downloadImageWithURL:options:progress:completed: 中会先拿图片缓存的 key (这个 key 默认是图片 URL)去 SDImageCache 单例中读取内存缓存,如果有,就返回给 SDWebImageManager;如果内存缓存没有,就开启异步线程,拿经过 MD5 处理的 key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存中去,然后再返回给 SDWebImageManager。
如果内存缓存和磁盘缓存中都没有,SDWebImageManager 就会调用 SDWebImageDownloader 单例的 -downloadImageWithURL: options: progress: completed: 方法去下载,该会先将传入的 progressBlock 和 completedBlock 保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务。
SDWebImageDownloaderOperation 中包装了一个 NSURLConnection 的网络请求,并通过 runloop 来保持 NSURLConnection 在 start 后、收到响应前不被干掉,下载图片时,监听 NSURLConnection 回调的 -connection:didReceiveData: 方法中会负责 progress 相关的处理和回调,- connectionDidFinishLoading: 方法中会负责将 data 转为 image,以及图片解码操作,并最终回调 completedBlock。
SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManager,SDWebImageManager 中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageView,UIImageView 中再回到主线程设置 image 属性。至此,图片的下载和缓存操作就圆满结束了。
当然,SDWebImage 中还有很多细节可以深挖,包括一些巧妙设计和知识点,接下来再看看SDWebImage 中的实现细节。