一、简介
1. 设计目的
SDWebImage 提供了 UIImageView、UIButton 、MKAnnotationView 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。
2. 特性
- 提供 UIImageView, UIButton, MKAnnotationView 的分类,用来显示网络图片,以及缓存管理
- 异步下载图片
- 异步缓存(内存+磁盘),并且自动管理缓存有效性
- 后台图片解压缩
- 同一个 URL 不会重复下载
- 自动识别无效 URL,不会反复重试
- 不阻塞主线程
- 高性能
- 使用 GCD 和 ARC
- 支持多种图片格式(包括 WebP 格式)
- 支持动图(GIF)(4.0 之前的动图效果并不是太好,4.0 以后基于 FLAnimatedImage加载动图)
3. 用法
3.1 UITableView 中使用 UIImageView+WebCache
UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可
3.2 使用回调 blocks
在 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 ...
}];
5.3 SDWebImageManager 的使用
UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。SDWebImageManager也可以单独使用。
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable 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, NSURL * _Nullable targetURL) {
// progression tracking code
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable 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 queryCacheOperationForKey:myCacheKey done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
// 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];
};
二、原理
官方给的设置图片后执行时序图:在了解细节之前我们先大概浏览一遍主流程,也就是最核心的逻辑。
我们从 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。
流程简述
- 1、当我门需要获取网络图片的时候,我们首先需要的便是URL,没有URL什么都没有,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URL相关的图片,如果有则直接返回image,如果没有则进行下一步。
- 2、当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。
- 3、如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果正在下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。
- 4、刷新UI(当然根据实际情况操作)将image存入沙盒缓存。
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 中的实现细节。
查看SDWebImage的源码,与缓存有关的一共有四个文件SDImageCacheConfig和SDImageCache,首先看一下SDImageCacheConfig的头文件:
//是否压缩图片,默认为YES,压缩图片可以提高性能,但是会消耗内存
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否关闭iCloud备份,默认为YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否使用内存做缓存,默认为YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
/** 缓存图片的最长时间,单位是秒,默认是缓存一周
* 这个缓存图片最长时间是使用磁盘缓存才有意义
* 使用内存缓存在前文中讲解的几种情况下会自动删除缓存对象
* 超过最长时间后,会将磁盘中存储的图片自动删除
*/
@property (assign, nonatomic) NSInteger maxCacheAge;
//缓存占用最大的空间,单位是字节
@property (assign, nonatomic) NSUInteger maxCacheSize;
NSCacheConfig类可以看得出来就是一个配置类,保存一些缓存策略的信息,没有太多可以讲解的地方,看懂就好,看一下NSCacheConfig.m文件的源码:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
@implementation SDImageCacheConfig
- (instancetype)init {
if (self = [super init]) {
_shouldDecompressImages = YES;
_shouldDisableiCloud = YES;
_shouldCacheImagesInMemory = YES;
_maxCacheAge = kDefaultCacheMaxCacheAge;
_maxCacheSize = 0;
}
return self;
}
@end
从上面源码可以看出相关属性的默认值,以及maxCacheAge的默认值为一周时间。
/*
SDWebImage真正执行缓存的类
SDImageCache支持内存缓存,默认也可以进行磁盘存储,也可以选择不进行磁盘存储
*/
@interface SDImageCache : NSObject
#pragma mark - Properties
//SDImageCacheConfig对象,缓存策略的配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
//内存缓存的最大cost,以像素为单位,后面有具体计算方法
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//内存缓存,缓存对象的最大个数
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
上面这一部分是属性的声明,属性很少,但我们在NSCache中都见过了,首先是SDImageCacheConfig,即前面讲解的缓存策略配置,maxMemoryCost其实就是NSCache的totalCostLimit,这里它使用像素为单位进行计算,maxMemoryCountLimit其实就是NSCache的countLimit,需要注意的是SDImageCache继承自NSObject没有继承NSCache,所以它需要保存这些属性。
/*
真正执行存储操作的方法
*/
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
//如果image为nil或image的URL为空直接返回即不执行保存操作
if (!image || !key) {
//如果回调块存在就执行完成回调块
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
//如果缓存策略指明要进行内存缓存
if (self.config.shouldCacheImagesInMemory) {
//根据前面的内联函数计算图片的大小作为cost
NSUInteger cost = SDCacheCostForImage(image);
//向memCache中添加图片对象,key即图片的URL,cost为上面计算的
[self.memCache setObject:image forKey:key cost:cost];
}
//如果要保存到磁盘中
if (toDisk) {
//异步提交任务到串行的ioQueue中执行
dispatch_async(self.ioQueue, ^{
//进行磁盘存储的具体的操作,使用@autoreleasepool包围,执行完成后自动释放相关对象
//我猜测这么做是为了尽快释放产生的局部变量,释放内存
@autoreleasepool {
NSData *data = imageData;
//如果传入的imageData为空,图片不为空
if (!data && image) {
// If we do not have any data to detect image format, use PNG format
//调用编码方法,获取NSData对象
//图片编码为NSData不在本文的讲述范围,可自行查阅
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
}
//调用下面的方法用于磁盘存储操作
[self storeImageDataToDisk:data forKey:key];
}
//存储完成后检查是否存在回调块
if (completionBlock) {
//异步提交在主线程中执行回调块
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
//如果不需要保存到磁盘中判断后执行回调块
} else {
if (completionBlock) {
completionBlock();
}
}
}
//具体执行磁盘存储的方法
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
//判断图片NSData数据以及图片key是否为空,如果为空直接返回
if (!imageData || !key) {
return;
}
//检查当前执行队列是否为ioQueue,如果不是会提示开发者
[self checkIfQueueIsIOQueue];
//如果构造函数中构造的磁盘缓存存储图片路径的文件夹不存在
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
//那就根据这个路径创建需要的文件夹
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
// 根据key获取默认磁盘缓存存储路径下的MD5文件名的文件的绝对路径
// 感觉有点绕口。。就是获取图片二进制文件在磁盘中的绝对路径,名称就是前面使用MD5散列的,路径就是构造函数默认构造的那个路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
// 根据这个绝对路径创建一个NSURL对象
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//使用NSFileManager创建一个文件,文件存储的数据就是imageData
//到此,图片二进制数据就存储在了磁盘中了
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
上面就是图片缓存存储的核心方法了,如果要进行内存缓存就直接添加到memCache对象中,如果要进行磁盘缓存,就构造一个路径,构造一个文件名,然后存储起来就好了。这里面有几个重要的点,首先就是@autoreleasepool的使用,其实这里不添加这个autoreleasepool同样会自动释放内存,但添加后在这个代码块结束后就会立即释放,不会占用太多内存。其次,对于磁盘写入的操作是通过一个指定的串行队列实现的,这样不管执行多少个磁盘存储的操作,都必须一个一个的存储,这样就可以不用编写加锁的操作,可能有读者会疑惑为什么要进行加锁,因为并发情况下这些存储操作都不是线程安全的,很有可能会把路径修改掉或者产生其他异常行为,但使用了串行队列就完全不需要考虑加锁释放锁,一张图片存储完成才可以进行下一张图片存储的操作。
以上参考了以下文章:
iOS缓存 NSCache详解及SDWebImage缓存策略源码分析
SDWebImage 源码阅读笔记
SDWebImage实现分析
【iOS开源库】SDWebImage源码阅读&原理解析
转载请备注原文出处,不得用于商业传播——凡几多