SDWebImage源码阅读3——图片下载管理器

前言

SDWebImage源码阅读1——整体脉络结构
SDWebImage源码阅读2——缓存机制
前两篇研究了SDWebImage的整体结构和缓存机制,本篇主要研究一下它的网络图片下载部分的代码。


分析

前面已经说过在SDWebImageManager中有个SDWebImageDownloader类型的imageDownloader属性,意为** 下载管理器 **。当SDWebImageManager在内存中查询图片不得时便开始了从网络下载图片,即调用了imageDownloaderdownloadImageWithURL:options:progress:completed:方法。它返回了一个id <SDWebImageOperation>类型的对象,并通过block回调下载的图片信息。
有关图片网络下载的东西都在这个下载管理器SDWebImageDownloader内,我们现在开始仔细阅读它的代码:

—— SDWebImageDownloader.h——

// 下载的配置选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    
    // 这个属于默认的使用模式了,前往下载,返回进度block信息,完成时调用completedBlock
    SDWebImageDownloaderLowPriority = 1 << 0,
    
    // 渐进式下载 ,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    // 通常情况下request阻止使用NSURLCache.这个选项会默认使用NSURLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    // 如果从NSURLCache中读取图片,会在调用完成block的时候,传递空的image或者imageData
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    // 进入后台,继续下载
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    // 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,

    // 允许不受信任的SSL证书,在测试环境中很有用,在生产环境中要谨慎使用
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,


    // 将图片下载放到高优先级队列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

// 下载顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    
    // first in first out 先进先出
    SDWebImageDownloaderFIFOExecutionOrder,

    // last in first out
    SDWebImageDownloaderLIFOExecutionOrder
};

// 定义通知的全局变量
extern NSString *const SDWebImageDownloadStartNotification;
extern NSString *const SDWebImageDownloadStopNotification;

// 定义回调的block
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);

typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);

.h文件一开头定义了一些变量。有俩枚举变量,第一个是网络下载的配置选项,第二个是下载顺序。然后定义了两个全局性的常量,用于发送和观察下载开始和结束的通知。

值得注意的是在iOS中怎样去定义一个全局性的变量。标准方式就是这样,先在xxx.h文件中声明extern NSString *const kCDKAPIHost;,然后在xxx.m文件中声明NSString *const kCDKAPIHost = @"api.cheddarapp.com";。除此之外,还可以通过定义setter/getter的方式定义全局性变量。请参考:Objective-c怎么定义全局的静态变量

/**
 * Asynchronous downloader dedicated and optimized for image loading.
 */
@interface SDWebImageDownloader : NSObject


@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

// 当前下载队列最大的并发数,即队列中最多同时运行几条线程
@property (readonly, nonatomic) NSUInteger currentDownloadCount;

// 超时时间
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

// 下载顺序
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;


+ (SDWebImageDownloader *)sharedDownloader;

// 设置一个过滤器,为下载图片的HTTP request选取header.意味着最终使用的headers是经过这个block过滤之后的返回值。
@property (nonatomic, strong) NSDictionary *(^headersFilter)(NSURL *url, NSDictionary *headers);


- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;


- (NSString *)valueForHTTPHeaderField:(NSString *)field;


- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

// 是否暂停、挂起
- (void)setSuspended:(BOOL)suspended;

@end

可以看到它首先定义了一些暴露给外部、允许进行设置的、有关下载的属性,比如允许同时下载的最大线程数、当前线程数、超时时间、下载顺序、HTTP请求头等。然后给外部提供的接口最重要的就是downloadImageWithURL: options: completed:了。
现在我们开始阅读SDWebImageDownloader.m的代码。

——SDWebImageDownloader.m——

首先看下关于数据初始化部分的代码:

@interface SDWebImageDownloader ()

@property (strong, nonatomic) NSOperationQueue *downloadQueue; // 下载队列
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // URL回调字典,以URL为key,以该URL下载进度block和完成block的数组为value
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; // HTTP请求头
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
// barrierQueue是一个并行队列,在一个单一队列中顺序处理所有下载操作的网络响应
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

@end

@implementation SDWebImageDownloader

+ (void)initialize {
    // Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator )
    // To use it, just add #import "SDNetworkActivityIndicator.h" in addition to the SDWebImage import
    if (NSClassFromString(@"SDNetworkActivityIndicator")) {

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#pragma clang diagnostic pop

        // Remove observer in case it was previously added.
        [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
        [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                                 selector:NSSelectorFromString(@"startActivity")
                                                     name:SDWebImageDownloadStartNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                                 selector:NSSelectorFromString(@"stopActivity")
                                                     name:SDWebImageDownloadStopNotification object:nil];
    }
}

+ (SDWebImageDownloader *)sharedDownloader {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (id)init {
    if ((self = [super init])) {
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; // 下载顺序(先进先出)
        _downloadQueue = [NSOperationQueue new]; 
        _downloadQueue.maxConcurrentOperationCount = 2;
        _URLCallbacks = [NSMutableDictionary new];
        _HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"]; // 初始化HTTP请求头
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); // 创建一个并行队列
        _downloadTimeout = 15.0;
    }
    return self;
}

- (void)dealloc {
    [self.downloadQueue cancelAllOperations]; // 移除下载队列中的所有操作
    SDDispatchQueueRelease(_barrierQueue); // 销毁了自建的队列_barrierQueue
}

可以看到在类内部也定义了几个变量。有下载队列downloadQueue,所有创建的下载操作operation都会加入该队列管理;还有URL回调字典URLCallbacks,以url为key,以下载进度回调progressBlock和下载完成回调completedBlock信息为value构建的字典。这个存有url回调信息字典,后面还会讲到;除此外,还定义了一个并行队列barrierQueue,在该队列中做图片的网络响应处理,它在init中进行了初始化。
然后重写了initialize方法,在内主要添加了网络开始下载和停止下载的观察。这个我有个疑问是为什么要重写initialize方法呢?不可以在init方法中完成吗?这个应该要清楚initializeinitload3个方法的区别与联系。
继续往下看代码。通过单例方法生成单例对象。然后在init方法内对一些变量进行了初始化。最后在dealloc方法中移除了下载队列中的所有操作,并销毁了自己创建的,用于图片网络响应处理的并行队列_barrierQueue

下面就是下载管理器里最重要的部分了:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(void (^)(NSInteger, NSInteger))progressBlock
                                       completed:(void (^)(UIImage *, NSData *, NSError *, BOOL))completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak SDWebImageDownloader *wself = self;   

    // addProgressCallback:andCompletedBlock:forURL:createCallback:
    // 这个方法的目的是在下载队列中为url开启下载操作,并且保证一条url只会被创建一次下载操作
    [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0){
            timeoutInterval = 15.0;
        }
        // 创建请求对象request,并设置request的相关属性。
        // 需要注意的是为避免重复缓存(NSURLCache + SDImageCache),若没有明确告知要进行URL请求缓存,则禁用NSURLCache缓存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES; // 是否开启HTTP管道,可以降低请求的加载时间
        // 若自己通过headersFilter设置了请求头信息,则将其设为HTTP的请求头;否则,用在初始化方法中默认的HTTPHeaders作为HTTP请求头。
        if (wself.headersFilter){
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else{
            request.allHTTPHeaderFields = wself.HTTPHeaders; 
        }
        // 创建SDWebImageDownLoaderOperation的operation下载操作对象
        operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request
                                                                   options:options
                                                                  progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                                      if (!wself) return;
                                                                      SDWebImageDownloader *sself = wself;
                                                                      NSArray *callbacksForURL = [sself callbacksForURL:url];
                                                                      for (NSDictionary *callbacks in callbacksForURL) {
                                                                          SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                          if (callback) callback(receivedSize, expectedSize);
                                                                      }
                                                                  }
                                                                 completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                                     if (!wself) return;
                                                                     SDWebImageDownloader *sself = wself;
                                                                     NSArray *callbacksForURL = [sself callbacksForURL:url];
                                                                     if (finished) {
                                                                         [sself removeCallbacksForURL:url];
                                                                     }
                                                                     for (NSDictionary *callbacks in callbacksForURL) {
                                                                         SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                         if (callback) callback(image, data, error, finished);
                                                                     }
                                                                 }
                                                                 cancelled:^{
                                                                     if (!wself) return;
                                                                     SDWebImageDownloader *sself = wself;
                                                                     [sself removeCallbacksForURL:url];
                                                                 }];
        
        // 设置操作的优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        }
        // 将下载操作放入下载队列中
        [wself.downloadQueue addOperation:operation];
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // 如果是后进先出操作顺序 则将该操作置为最后一个操作
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];

    return operation;
}

- (void)addProgressCallback:(void (^)(NSInteger, NSInteger))progressBlock andCompletedBlock:(void (^)(UIImage *, NSData *data, NSError *, BOOL))completedBlock forURL:(NSURL *)url createCallback:(void (^)())createCallback {
    // URL作为callbacks字典的key,所以不能为空。若为空,则立即回调返回。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    // dispatch_barrier_sync“屏障”:保证同一时间只有一个线程操作URLCallbacks
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        // 处理 同一个URL的单个下载
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback(); // 通过这个回调,可以实时获取下载进度以及是下载完成情况
        }
    });
}

可以看到在方法downloadImageWithURL: options: progress: completed:方法内部首先是调用了addProgressCallback: andCompletedBlock:forURL:createCallback:这个方法,然后在其createCallback回调block里进行了大量操作。这些操作主要是设置图片网络请求request,创建下载操作operation,并做回调的响应处理。关于addProgressCallback:方法这块一直不太明白,搞不明白多出这个方法的目的,现在终于理解了。我们观察该方法内部:
它实际上是将某url对应下载操作的回调信息存储在了URLCallbacks字典中。并且,若该url是第一次被下载,那字典URLCallbacks中该url对应的value应该是空的,所以if(!self.URLCallbacks[url])是YES,所以value被初始化,并将first赋为YESfirst = YES,后面完成该url对应value的赋值,也因为first==YES,所以会执行createCallback(),调用block回调。然后我们上面说了后续创建下载操作这些都是在该回调里完成的。那若该url不是第一次下载,它是不会调用createCallback()回调的,也就是说根本不会执行到该block里写的创建下载操作的代码。简单的说,这儿的代码实现了防止了同一个url下载多次,只有第一次下载才会创建下载操作。

- (NSArray *)callbacksForURL:(NSURL *)url {
    __block NSArray *callbacksForURL;
    dispatch_sync(self.barrierQueue, ^{
        callbacksForURL = self.URLCallbacks[url];
    });
    return [callbacksForURL copy];
}

- (void)removeCallbacksForURL:(NSURL *)url {
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.URLCallbacks removeObjectForKey:url];
    });
}

可以看到在创建下载操作的回调block中调用了以上两个方法,用于图片下载响应后的处理。第一个方法是从字典URLCallbacks中读取出该url对应的回调信息。第二个方法是从URLCallbacks中移除该url对应的回调信息。需要留意的是对该操作加了“屏障”,保证在进行移除这个动作时,只有一个线程存在。若在移除时存在多个线程,则是比较危险的,相反第一个方法纯粹读取字典信息时,即使是多线程也并不存在危险,所以没加“屏障”。

其实,进行网络连接,网络下载的核心是SDWebImageDownloaderOperation类。在该类里通过对NSURLConnection的封装完成了网络连接的管理,图片下载响应处理及优化等操作。这部分的具体实现就不写了。


结尾

图片的网络下载这部分很复杂,在这块儿磨了很久,总算捋出大致逻辑了。不过在阅读的过程中还是发现有很多需要补习的知识点。比如关于NSOperationQueue,关于RunLoop,关于HTTP等都需要好好学习一下。

在阅读SDWebImage源码的过程中发现网上有的版本和我的不太一致,我的应该不是最新的,所以我把我这个版本的代码也放上来:SDWebImage下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容