对于著名的第三方库 SDWebImage 我们都不陌生,它在 Github 上的功能介绍是:
- 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
- 提供一个异步的图片加载器
- 提供一个异步的内存+磁盘图片缓存,并会自动处理缓存过期问题
- 支持GIF图片
- 支持WebP图片
- 后台图片解压缩处理
- 确保同一个URL的图片不被下载多次
- 确保虚假的URL不会被反复加载
- 确保下载及缓存时,主线程不被阻塞
- 性能好
- 使用 GCD 和 ARC
- 支持 Arm64
虽然从上面的功能看起来很复杂,但其实非常简单好用,比如在项目开发中要在 tableView/collectionView 的 cell 中异步下载图片的时候都经常会用到它,而且只需要一句话:
1 | [self.iconImageView sd_setImageWithURL:[NSURL URLWithString:urlString] |
2 | placeholderImage:[UIImage imageNamed:@"img_default"]]; |
虽然对于 SDWebImage 的具体代码实现我们平时可能不怎么关注,但学习 SDWebImage 的代码及其思想对提高我们的水平还是很有帮助的,所以本文主要从上面的这个方法入手学习主要的类和方:
一、UIImageView+WebCache
在 cell 中调用:
1 | [self.iconImageView sd_setImageWithURL:[NSURL URLWithString:urlString] |
2 | placeholderImage:[UIImage imageNamed:@"img_default"]]; |
是调用了 UIImageView
的分类 UIImageView+WebCache.m
中的方法:
1 | - (void)sd_setImageWithURL:(NSURL *)url |
2 | placeholderImage:(UIImage *)placeholder; |
这个方法唯一的作用就是调用了另外一个包含更多参数的的方法(类似指定构造器?),这个方法也是 UIImageView+WebCache
的核心方法:
1 | - (void)sd_setImageWithURL:(NSURL *)url |
2 | placeholderImage:(UIImage *)placeholder |
3 | options:(SDWebImageOptions)options |
4 | progress:(SDWebImageDownloaderProgressBlock)progressBlock |
5 | completed:(SDWebImageCompletionBlock)completedBlock; |
后面三个参数默认 0 或 nil
操作缓存池
这个方法最开始要做的是:
1 | // UIImageView+WebCache |
2 | [self sd_cancelCurrentImageLoad]; |
这是在关闭当前图片的下载操作,避免重复操作,这对在 tableView
或 collectionView
被重用的 cell
尤为重要。
它调用的方法是:
1 | // UIImageView+WebCache |
2 | - (void)sd_cancelCurrentImageLoad { |
3 | [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]; |
4 | } |
5 | |
6 | // UIView+WebCacheOperation.m |
7 | - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key { |
8 | |
9 | // 获取操作缓存池 operationDictionary |
10 | NSMutableDictionary *operationDictionary = [self operationDictionary]; |
11 | |
12 | // 试图从 operationDictionary 获取与键 key 对应的 操作 |
13 | id operations = [operationDictionary objectForKey:key]; |
14 | |
15 | // 如果获取 operations 成功,则将其取消,并从缓存池中移除 |
16 | if (operations) { |
17 | if ([operations isKindOfClass:[NSArray class]]) { |
18 | for (id <SDWebImageOperation> operation in operations) { |
19 | if (operation) { |
20 | [operation cancel]; |
21 | } |
22 | } |
23 | } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){ |
24 | [(id<SDWebImageOperation>) operations cancel]; |
25 | } |
26 | [operationDictionary removeObjectForKey:key]; |
27 | } |
28 | } |
29 | |
30 | // UIView+WebCacheOperation.m |
31 | - (NSMutableDictionary *)operationDictionary { |
32 | |
33 | // 用 runtime 在 category 中获取之前关联的属性(操作缓存池) |
34 | NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey); |
35 | |
36 | // 如果获取成功,直接返回 operations |
37 | if (operations) { |
38 | return operations; |
39 | } |
40 | |
41 | // 如果没有,则新建一个对象,并关联属性 |
42 | operations = [NSMutableDictionary dictionary]; |
43 | objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
44 | return operations; |
45 | } |
可见框架中用 NSMutableDictionary
做缓存池 operationDictionary
来存储和管理操作,这个缓存池是动态关联到 UIView
上的属性。那为什么不是关联到 UIImageView
上呢?因为在 UIButton+WebCache.m
中也会调用 - (void)sd_cancelImageLoadForState:(UIControlState)state
来取消当前操作,所以把这操作缓存池关联到 UIButton
和 UIImageView
共同的父类 UIView
上了。对于来自 UIImageView
的操作都是用字符串 "UIImageViewImageLoad"
做 key,值为遵守协议 SDWebImageOperation
的单个对象或由其组成的数组。SDWebImageOperation
协议只声明一个方法:
1 | - (void)cancel; |
设置占位图
在图片开始下载之前会根据 options 参数来判断要不要先给 UIImageView 设置占位图:
1 | if (!(options & SDWebImageDelayPlaceholder)) { |
2 | dispatch_main_async_safe(^{ |
3 | self.image = placeholder; |
4 | }); |
5 | } |
options 默认为 0,所以与 SDWebImageDelayPlaceholder
枚举值做 & 运算的结果为非,即默认是先设置占位图。如果在 options 中选择了 SDWebImageDelayPlaceholder
则不会设置占位图,而是等图片下载完毕再设置图片。
获取图片
接下来就要调用 [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
来加载图片了:
1 | if (url) { |
2 | |
3 | // 判断,是否要在加载图片的时候添加转动的小菊花 |
4 | if ([self showActivityIndicatorView]) { |
5 | [self addActivityIndicator]; |
6 | } |
7 | |
8 | __weak __typeof(self)wself = self; |
9 | // 获取图片 |
10 | id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url |
11 | options:options |
12 | progress:progressBlock |
13 | completed:...]; |
14 | // 把操作存到操作缓存池中去,方便以后再用这个 UIImageView 加载图片前先取消掉现在这个操作,避免重复操作 |
15 | [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; |
图片加载过程结束后则会调用 downloadImageWithURL… 方法的最后一个参数 (SDWebImageCompletionWithFinishedBlock)completedBlock
,要做的事情有三个:移除转动的小菊花,回到主线程设置图片,执行 completedBlock (但默认为 nil):
1 | [wself removeActivityIndicator]; |
2 | if (!wself) return; |
3 | dispatch_main_sync_safe(^{ |
4 | if (!wself) return; |
5 | if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) |
6 | { |
7 | completedBlock(image, error, cacheType, url); |
8 | return; |
9 | } |
10 | else if (image) { |
11 | wself.image = image; |
12 | [wself setNeedsLayout]; |
13 | } else { |
14 | if ((options & SDWebImageDelayPlaceholder)) { |
15 | wself.image = placeholder; |
16 | [wself setNeedsLayout]; |
17 | } |
18 | } |
19 | if (completedBlock && finished) { |
20 | completedBlock(image, error, cacheType, url); |
21 | } |
22 | }); |
dispatch_main_sync_safe
的宏定义是这样的:
1 | #define dispatch_main_sync_safe(block)\ |
2 | if ([NSThread isMainThread]) {\ |
3 | block();\ |
4 | } else {\ |
5 | dispatch_sync(dispatch_get_main_queue(), block);\ |
6 | } |
即保证了只在主线程更新 UI 。
最后,如果传入的 url 为空,则创建 NSError 对象 error并传给 completedBlock
:
1 | dispatch_main_async_safe(^{ |
2 | [self removeActivityIndicator]; |
3 | if (completedBlock) { |
4 | NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain |
5 | code:-1 |
6 | userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; |
7 | completedBlock(nil, error, SDImageCacheTypeNone, url); |
8 | } |
至此给 UIImageView 设置图片的方法调用完毕,主要步骤是:取消操作缓存池中的操作 –> 设置占位图 –> 获取图片 –> 回主线程更新 UI –> 把当前操作加入到操作缓存池中。看似简单,但其实还有最重要的获取图片的过程还没展开学习呢,下面继续看下它是怎样从缓存、网络中获取图片的。
二、SDWebImageManager
上面函数中使用了下面方法来获取图片:
1 | [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]; |
那 SDWebImageManager 是什么呢?它又管理着什么?
下面是 SDWebImageManager.h
中对它的介绍:
- The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.
- It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).
- You can use this class directly to benefit from web image downloading with caching in another context than
- a UIView.
即它是隐藏在 UIImageView 的分类 UIImageView+WebCache
背后的类。它是异步下载器 SDWebImageDownloader
和缓存图片的 SDImageCache
之间的桥梁。除了在 UIView 中,在其他地方也可以直接使用它的 downloadImageWithURL:options:progress:completed:
方法来直接下载图片。
要获取 SDWebImageManager 的对象通常是使用 sharedManager 来获取单例对象:
1 | + (id)sharedManager { |
2 | static dispatch_once_t once; |
3 | static id instance; |
4 | dispatch_once(&once, ^{ |
5 | instance = [self new]; |
6 | }); |
7 | return instance; |
8 | } |
但它没有严格的重写 allocWithZone
等方法来保证这对象是唯一的。
接下来正式进入 downloadImageWithURL:options:progress:completed:
方法:
首先是确保 url 的正确性:
1 | // SDWebImageManager.m |
2 | if ([url isKindOfClass:NSString.class]) { |
3 | url = [NSURL URLWithString:(NSString *)url]; |
4 | } |
5 | if (![url isKindOfClass:NSURL.class]) { |
6 | url = nil; |
7 | } |
SDWebImageCombinedOperation
接着创建 operation:
1 | __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; |
2 | __weak SDWebImageCombinedOperation *weakOperation = operation; |
SDWebImageCombinedOperation
是一个继承自 NSObject、遵守了 SDWebImageOperation
协议的类,SDWebImageOperation
协议只有一个方法:
1 | @protocol SDWebImageOperation <NSObject> |
2 | |
3 | - (void)cancel; |
4 | |
5 | @end |
SDWebImageCombinedOperation
对 cancel 方法的实现只是把它持有的 NSOperation 属性 cancel 掉,以及回调并清空 cancelBlock
:
1 | - (void)cancel { |
2 | self.cancelled = YES; |
3 | if (self.cacheOperation) { |
4 | // cacheOperation 属性是一个 NSOperation 对象 |
5 | [self.cacheOperation cancel]; |
6 | self.cacheOperation = nil; |
7 | } |
8 | if (self.cancelBlock) { |
9 | self.cancelBlock(); |
10 | _cancelBlock = nil; |
11 | } |
12 | } |
接着判断 url 是否在之前加载失败的 url 记录中:
1 | BOOL isFailedUrl = NO; |
2 | @synchronized (self.failedURLs) { |
3 | isFailedUrl = [self.failedURLs containsObject:url]; |
4 | } |
后面我们可以看到如果这个 url 加载失败,则会被记录,以此保证无效的 url 不会被重复加载。
接着用该 url 生成对应的 key,并用此 key 到缓存中查找有没对应的图片:
1 | NSString *key = [self cacheKeyForURL:url]; |
2 | operation.cacheOperation = |
3 | [self.imageCache queryDiskCacheForKey:key |
4 | done:^(UIImage *image, SDImageCacheType cacheType); |
由 url 生成 key 的过程也很简单,默认只是 url 的字符串形式:
1 | - (NSString *)cacheKeyForURL:(NSURL *)url { |
2 | if (self.cacheKeyFilter) { |
3 | return self.cacheKeyFilter(url); |
4 | } |
5 | else { |
6 | return [url absoluteString]; |
7 | } |
8 | } |
如果在缓存中找到了对应的图片,则直接回调并返回该图片:
1 | dispatch_main_sync_safe(^{ |
2 | completedBlock(image, nil, cacheType, YES, url); |
3 | }); |
如果没有在缓存中找到对应的图片,则到网络下载:
1 | id <SDWebImageOperation> subOperation = |
2 | [self.imageDownloader downloadImageWithURL:url |
3 | options:downloaderOptions |
4 | progress:progressBlock |
5 | completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished){...}]; |
这个方法返回遵守了 SDWebImageOperation
协议的对象 subOperation。如果这个方法下载到了图片,则先在图片缓存中存储这个图片,再回执行回调 block:
1 | if (downloadedImage && finished) { |
2 | [self.imageCache storeImage:downloadedImage |
3 | recalculateFromImage:NO |
4 | imageData:data |
5 | forKey:key |
6 | toDisk:cacheOnDisk]; |
7 | } |
8 | dispatch_main_sync_safe(^{ |
9 | if (strongOperation && !strongOperation.isCancelled) { |
10 | completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); |
11 | } |
12 | }); |
如果图片要进行转换,则先进行转换,再存储和返回转换后的图片。
如果图片不在缓存中,而且其代理也不支持到网络下载,则图片为 nil:
1 | dispatch_main_sync_safe(^{ |
2 | __strong __typeof(weakOperation) strongOperation = weakOperation; |
3 | if (strongOperation && !weakOperation.isCancelled) { |
4 | completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); |
5 | } |
6 | }); |
至此用 SDWebImageManager
的对象方法 downloadImageWithURL:options:progress:completed:
获取图片已经结束,主要是先后从缓存、网络中获取图片,如果获取成功,则存储起来并返回图片。下面将继续学习图片缓存和到网络加载图片的两个过程。
三、SDImageCache
SDImageCache.h
中对 SDImageCache 类是这样介绍的:
- SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed
- asynchronous so it doesn’t add unnecessary latency to the UI.
即它维护了一个内存缓存和一个磁盘缓存,后一个缓存不是必须的。磁盘缓存的写入操作是异步的,所以它不会给 UI 造成延迟。
在上一节的方法中是用到了它的对象方法来异步查询图片缓存:
1 | - (NSOperation *)queryDiskCacheForKey:(NSString *)key |
2 | done:(SDWebImageQueryCompletedBlock)doneBlock; |
这个方法先在内存中查找是否有图片缓存,如果有,则回调:
1 | // First check the in-memory cache... |
2 | UIImage *image = [self imageFromMemoryCacheForKey:key]; |
3 | if (image) { |
4 | doneBlock(image, SDImageCacheTypeMemory); |
5 | return nil; |
6 | } |
这里用到的 imageFromMemoryCacheForKey
方法会在 SDImageCache 的属性 memCache
中查找。memCache
是一个 NSCache
对象。
1 | // SDImageCache.m |
2 | @property (strong, nonatomic) NSCache *memCache; |
3 | |
4 | - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key { |
5 | return [self.memCache objectForKey:key]; |
6 | } |
如果内存中没有,则从磁盘中查找并回调,如果找到则先保存到内存中,下次再要从缓存中查找则可先在内存中获取了。
1 | UIImage *diskImage = [self diskImageForKey:key]; |
2 | if (diskImage && self.shouldCacheImagesInMemory) { |
3 | NSUInteger cost = SDCacheCostForImage(diskImage); |
4 | [self.memCache setObject:diskImage forKey:key cost:cost]; |
5 | } |
6 | |
7 | dispatch_async(dispatch_get_main_queue(), ^{ |
8 | doneBlock(diskImage, SDImageCacheTypeDisk); |
9 | }); |
在磁盘中查找的路径是 沙盒的 Cache 文件夹 + 文件名。
其中 Cache 文件夹路径为:
1 | // SDImageCache.m |
2 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); |
文件名则比较复杂,是先将 key 作 MD5 转换,得到 16 个字符,将每个字符的 ASCII 码表示为两位的十六进制形式,然后拼起来,得到一个 32 个数字组成的文件名,如果有后缀则再加上后缀。
1 | // SDImageCache.m |
2 | #define CC_MD5_DIGEST_LENGTH 16 |
3 | |
4 | - (NSString *)cachedFileNameForKey:(NSString *)key { |
5 | const char *str = [key UTF8String]; |
6 | if (str == NULL) { |
7 | str = ""; |
8 | } |
9 | unsigned char r[CC_MD5_DIGEST_LENGTH]; |
10 | CC_MD5(str, (CC_LONG)strlen(str), r); |
11 | NSString *filename = |
12 | [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", |
13 | r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15], |
14 | [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]]; |
15 | |
16 | return filename; |
17 | } |
SDImageCache 类还会自动处理缓存过期问题。图片缓存最久能保持一周:
1 | // SDImageCache.m |
2 | static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week |
在程序结束的时候进行两次磁盘清理,第一次是将过期的文件清除:
1 | // SDImageCache.m |
2 | // 根据最大缓存时间计算出 过期日期 |
3 | NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; |
4 | ... |
5 | NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; |
6 | if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { |
7 | [urlsToDelete addObject:fileURL]; |
8 | continue; |
9 | } |
第二次是判断此时文件缓存是否大于配置的最大文件容量,如果是,则从老到新清除文件,直至文件缓存小于最大文件容量的一半:
1 | // SDImageCache.m |
2 | ... |
3 | const NSUInteger desiredCacheSize = self.maxCacheSize / 2; |
4 | ... |
5 | // 对所有缓存文件,按最后修改时间排序 |
6 | NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent |
7 | usingComparator:^NSComparisonResult(id obj1, id obj2) { |
8 | return [obj1[NSURLContentModificationDateKey] |
9 | compare:obj2[NSURLContentModificationDateKey]]; |
10 | }]; |
11 | |
12 | // 删除文件,直至文件缓存足够小 |
13 | for (NSURL *fileURL in sortedFiles) { |
14 | if ([_fileManager removeItemAtURL:fileURL error:nil]) { |
15 | NSDictionary *resourceValues = cacheFiles[fileURL]; |
16 | NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; |
17 | currentCacheSize -= [totalAllocatedSize unsignedIntegerValue]; |
18 | if (currentCacheSize < desiredCacheSize) { |
19 | break; |
20 | } |
21 | } |
22 | } |
23 | ... |
这就是 SDImageCache 类处理图片缓存的核心内容了。
如果在缓存中没能找到对应的图片,则需要到网络下载了,所以我们接着学习图片的下载过程。
四、SDWebImageDownloader
SDWebImageDownloader.h
中对 SDWebImageDownloader
类的简介是:
- Asynchronous downloader dedicated and optimized for image loading.
即它是经过优化了的专门用来异步下载图片的。
在 SDWebImageManager
的 downloadImageWithURL:options:progress:completed:
方法中正是使用下面的 SDWebImageDownloader
的对象方法来下载图片的:
1 | - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url |
2 | options:(SDWebImageDownloaderOptions)options |
3 | progress:(SDWebImageDownloaderProgressBlock)progressBlock |
4 | completed:(SDWebImageDownloaderCompletedBlock)completedBlock; |
但这个方法几乎调用了另一个方法:
1 | - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock |
2 | completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock |
3 | forURL:(NSURL *)url |
4 | createCallback:(SDWebImageNoParamsBlock)createCallback; |
但它主要是定义了上面方法的最后一个参数 createCallback
。
先看下 addProgressCallback:completedBlock:forURL:createCallback:
方法干了些什么:
1 | // SDWebImageDownloader.m |
2 | |
3 | dispatch_barrier_sync(self.barrierQueue, ^{ |
4 | BOOL first = NO; |
5 | // 先查看 URLCallbacks 属性(NSMutableDictionary 类的)中有没与该 url 对应的 callbacksForURL |
6 | // 如果没有,则新建一个可变数组 |
7 | if (!self.URLCallbacks[url]) { |
8 | self.URLCallbacks[url] = [NSMutableArray new]; |
9 | first = YES; |
10 | } |
11 | |
12 | // Handle single download of simultaneous download request for the same URL |
13 | NSMutableArray *callbacksForURL = self.URLCallbacks[url]; |
14 | NSMutableDictionary *callbacks = [NSMutableDictionary new]; |
15 | if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; |
16 | if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; |
17 | [callbacksForURL addObject:callbacks]; |
18 | self.URLCallbacks[url] = callbacksForURL; |
19 | |
20 | // 如果是第一次添加回调,则执行回调,做 初始化请求 等操作 |
21 | if (first) { |
22 | createCallback(); |
23 | } |
24 | }); |
即该方法主要是把 progressBlock
和 completedBlock
存进与 url 对应的数组 callbacksForURL
中,方便以后取用, 并且在一次添加回调时会执行传入的参数 createCallback
,这个就是在 downloadImageWithURL:options:progress:completed:
方法中定义的,它的内容如下:
1 | // 设置超时时间,默认为 15s |
2 | NSTimeInterval timeoutInterval = wself.downloadTimeout; |
3 | if (timeoutInterval == 0.0) { |
4 | timeoutInterval = 15.0; |
5 | } |
6 | // 创建并设置一个可变请求 |
7 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url... |
8 | request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); |
9 | request.HTTPShouldUsePipelining = YES; |
然后创建一个 SDWebImageDownloaderOperation
类的对象 operation
,并在这定义以后在监听下载过程的代理方法中会调用的三个 block(progressBlock
(主要是执行存储在上面的 callbacksForURL 中的progressBlock),completedBlock
(主要是执行存储在上面的 callbacksForURL 中的completedBlock),cancelBlock
(移除 url 对应的 callbacksForURL)):
1 | // SDWebImageDownloader.m |
2 | operation = [[wself.operationClass alloc] initWithRequest:request |
3 | options:options |
4 | progress: |
5 | completed: |
6 | cancelled:...]; |
然后把这个操作添加到队列中,使得开始执行操作,如果设置了操作的顺序是后进先出,还得设置操作之间的依赖关系:
1 | [wself.downloadQueue addOperation:operation]; |
2 | |
3 | if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { |
4 | [wself.lastAddedOperation addDependency:operation]; |
5 | wself.lastAddedOperation = operation; |
6 | } |
SDWebImageDownloaderOperation
上面说到的 operation
是 SDWebImageDownloaderOperation
类的实例,SDWebImageDownloaderOperation
类继承自 NSOperation,用于处理 HTTP 请求,URL 连接等。operation 被加入队列后,就会调用 start 方法:
1 | // SDWebImageDownloaderOperation.m |
2 | - (void)start { |
3 | @synchronized (self) { |
4 | // 如果被标记为 cancell,则清空属性并返回 |
5 | if (self.isCancelled) { |
6 | self.finished = YES; |
7 | [self reset]; |
8 | return; |
9 | } |
10 | // 创建一个 NSURLConnection 对象 |
11 | self.executing = YES; |
12 | self.connection = [[NSURLConnection alloc] initWithRequest:self.request |
13 | delegate:self |
14 | startImmediately:NO]; |
15 | self.thread = [NSThread currentThread]; |
16 | } |
17 | |
18 | // 新创建的 NSURLConnection 对象开始执行 |
19 | [self.connection start]; |
20 | |
21 | if (self.connection) { |
22 | // 如果 connection 创建成功,则开始调用 progressBlock,初始接收到的数据大小为 0 |
23 | if (self.progressBlock) { |
24 | self.progressBlock(0, NSURLResponseUnknownLength); |
25 | } |
26 | // 回到主线程,发出“开始下载”的通知 |
27 | dispatch_async(dispatch_get_main_queue(), ^{ |
28 | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification |
29 | object:self]; |
30 | }); |
31 | // 开启子线程的 RunLoop |
32 | if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { |
33 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); |
34 | } |
35 | else { |
36 | CFRunLoopRun(); |
37 | } |
38 | |
39 | if (!self.isFinished) { |
40 | [self.connection cancel]; |
41 | [self connection:self.connection |
42 | didFailWithError:[NSError errorWithDomain:NSURLErrorDomain |
43 | code:NSURLErrorTimedOut |
44 | userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; |
45 | } |
46 | } |
47 | // 如果 connection 创建失败,则调用 completedBlock,返回的图片和数据都为 nil |
48 | else { |
49 | if (self.completedBlock) { |
50 | self.completedBlock(nil, |
51 | nil, |
52 | [NSError errorWithDomain:NSURLErrorDomain |
53 | code:0 |
54 | userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], |
55 | YES); |
56 | } |
57 | } |
58 | } |
五、NSURLConnectionDataDelegate
在下载过程中 NSURLConnection
的代理会监听下载情况并调用以下三种方法:
1 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; |
(接收到服务器返回的 response 时调用该代理方法,一般调用一次(除非 HTTP 内容类型是 multipart/x-mixed-replace
才会收到多个 response),主要作用是执行属性 progressBlock
,回主线程发送接收到 response 的通知.如果出错则执行 cancelBlock
和 completedBlock
)
1 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; |
(每次接收到服务器返回的数据时调用该代理方法, 主要作用是用一个 CGImageSourceRef
对象对现有数据进行处理、生成图片供回调使用,并调用属性 progressBlock, 提示下载进度)
1 | - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; |
(结束加载时调用该方法, 主要作用是停止子线程的 RunLoop
,调用属性 completionBlock
,返回下载到的图片,或错误, completionBlock
则会更新图片)
最后
SDWebImage 的图片加载流程大致如下,但没把操作缓存池、图片下载完先存储到缓存等操作画上去:
参考:
How is SDWebImage better than X?
Is there a big advantage to using SDWebImage over AFNetworking for image loading?