美文网首页首页投稿(暂停使用,暂停投稿)iOS 开发
SDWebImage源码阅读3——图片下载管理器

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

作者: Wang66 | 来源:发表于2016-08-08 22:51 被阅读274次

    前言

    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下载

    相关文章

      网友评论

        本文标题:SDWebImage源码阅读3——图片下载管理器

        本文链接:https://www.haomeiwen.com/subject/dnprsttx.html