SDWebImage 源码分析

对于著名的第三方库 SDWebImage 我们都不陌生,它在 Github 上的功能介绍是:

  1. 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
  2. 提供一个异步的图片加载器
  3. 提供一个异步的内存+磁盘图片缓存,并会自动处理缓存过期问题
  4. 支持GIF图片
  5. 支持WebP图片
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被下载多次
  8. 确保虚假的URL不会被反复加载
  9. 确保下载及缓存时,主线程不被阻塞
  10. 性能好
  11. 使用 GCD 和 ARC
  12. 支持 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];

这是在关闭当前图片的下载操作,避免重复操作,这对在 tableViewcollectionView 被重用的 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 来取消当前操作,所以把这操作缓存池关联到 UIButtonUIImageView 共同的父类 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.

即它是经过优化了的专门用来异步下载图片的。

SDWebImageManagerdownloadImageWithURL: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
    });

即该方法主要是把 progressBlockcompletedBlock 存进与 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

上面说到的 operationSDWebImageDownloaderOperation 类的实例,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 的通知.如果出错则执行 cancelBlockcompletedBlock)

1
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

(每次接收到服务器返回的数据时调用该代理方法, 主要作用是用一个 CGImageSourceRef 对象对现有数据进行处理、生成图片供回调使用,并调用属性 progressBlock, 提示下载进度)

1
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

(结束加载时调用该方法, 主要作用是停止子线程的 RunLoop ,调用属性 completionBlock ,返回下载到的图片,或错误, completionBlock 则会更新图片)

最后

SDWebImage 的图片加载流程大致如下,但没把操作缓存池、图片下载完先存储到缓存等操作画上去:


参考:

SDWebImage

How is SDWebImage better than X?

iOS 源代码分析 — SDWebImage

SDWebImage实现分析

Is there a big advantage to using SDWebImage over AFNetworking for image loading?

iOS图片缓存库基准对比