SDWebImage下载模块实现分析

上一篇 中我们已经了解了 SDWebImage 的基本使用 ,这篇文章我们来分析 SDWebImage 的下载模块

只负责图片下载相关操作,与缓存无关


下载模块由2个类组成

SDWebImageDownloaderOperation 每一个下载请求都是一个 SDWebImageDownloaderOperation

SDWebImageDownloader 负责集中创建和管理 DownloaderOperation

image下载过程

SDWebImageDownloader中的这个方法负责为每个下载图片请求创建一个SDWebImageDownloaderOperation并添加到OperationQueue中管理

SDWebImageDownloaderOperation

类扩展中的属性

NSURLConnection connection; //内部用NSURLConnection下载图片

NSMutableData imageData; //NSUrlconnection的response 数据会append 到imageData中

SDWebImageDownloaderOperation只有一个方法,就是实例化自己来创建下载图片的任务,它只是做了一下初始化操作

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock {
    if ((self = [super init])) {
        _request = request;
        _shouldDecompressImages = YES;
        _shouldUseCredentialStorage = YES;
        _options = options;
        _progressBlock = [progressBlock copy];
        _completedBlock = [completedBlock copy];
        _cancelBlock = [cancelBlock copy];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
    }
    return self;
}

真正干活的是start方法 部分不重要的代码有省略

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0

        1.下面是App 进入后台时,请求继续执行一段时间的方法
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];

            2.App 进入后台时(按Home键)请求额外的执行时间
            beginBackgroundTaskWithExpirationHandler handler 意味着你请求的的额外任务时间已经到了,程序马上终止运行,你可以在handler做一些最后的清理工作,后面有详解
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

        3.创建并开始NSUrlConnection下载任务
        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
    }

    [self.connection start];

    if (self.connection) {

    4.下载进度回调和通知
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });

    5.下载完成和失败的回调和通知    
        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }
}

创建图片等操作在 NSURLConnectionDataDelegate 代理方法中执行

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
	1.储存接收到的图片Data 
    [self.imageData appendData:data];
     ... ...
    }
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

    1.如果Reponse Code 符合要求则开始接收图片Data
    if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
        NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
        self.expectedSize = expected;
        if (self.progressBlock) {
            self.progressBlock(0, expected);
        }

        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];

        2.如果 statusCode ==304 意味着图片没有改变,使用本地缓存的就可以 直接取消下载操作,返回缓存的图片即可
       if (code == 304) {
            [self cancelInternal];
        } else {
            [self.connection cancel];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
        }
        CFRunLoopStop(CFRunLoopGetCurrent());
        [self done];
    }
}

SDWebImageDownloader怎样创建.管理每一个下载 Operation

SDWebImageDownloader重要的property

BOOL shouldDecompressImages; //解压缩下载和缓存的图片,能提升性能但会消耗内存,如果内存过分占用的话设置为NO,默认为YES

NSInteger maxConcurrentDownloads; //最大同时下载的图片数量 默认6

NSTimeInterval downloadTimeout; //超时 默认15s

SDWebImageDownloaderExecutionOrder executionOrder; //执行下载图片请求的顺序, 第一篇博客有详解

NSURLCredential urlCredential; //身份鉴定相关的

NSString username;

NSString password;

还有几个和 http Header相关的方法就不一一列举了

方法

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

正如官方文档所说,我们也可以单独使用SDWebImage的其中一部分,使用这个方法我们能直接下载Image而不使用其缓存,或者进行我们需要的操作等, SDWebImage也会内部调用此方法来连接异步下载和缓存操作

类扩展中的属性

NSOperationQueue downloadQueue; //我们的每一个下载图片请求都是一个SDWebImageDownloaderOperation,所有的Operation放在这里管理

NSMutableDictionary URLCallbacks; //管理回调Block的Dictionary,键是图片的Url,值是一个数组,数组包含下载进度回调Block和下载完成回调的Block,下面会展开说明

NSMutableDictionary HTTPHeaders

dispatch_queue_t barrierQueue; :_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

//这是一个手动创建的并行的Queue,为什么叫_barrierQueue最后会展开说明

创建 DownloadOperation

每一个下载图片的请求都会调用

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

在这个方法中会直接调用

-(void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback;

此方法具体实现如下:

1.url要做为self.URLCallbacks 这个存储回调Block的字典的键,所以不能为空,再说url为空下载什么呢
if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
2.为什么是barrier 后面会说,暂且当作Dispatch_sync
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;

        3.判断是否是第一次下载这个Url对应的图片
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        4.把下载图片的回调Block存到 self.URLCallbacks 中
        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;

        5.如果是第一次下载这个Url对应的图片,继续执行createCallback()
        if (first) {
            createCallback();
        }
    });

createCallBack() 具体实现

NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        1.因为SDWebImage会为我们做内存和磁盘缓存,所以默认会关闭Url缓存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }

        2.这里的 wself.operationClass 就是SDWebImageDownloaderOperation 继承自NSOperation, 内部用NSUrlConnection完成下载图片的草错
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;

                                                             3.从 sself.URLCallbacks 中取出进度回调的Block,回调进度的变化
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;

                                                            4.从 sself.URLCallbacks 中取出下载完成的回调Block,并移除回调Block
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
        operation.shouldDecompressImages = wself.shouldDecompressImages;

        5.用户认证相关
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }

        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        6.把创建的SDWebImageDownloaderOperation 加入wself.downloadQueue 中管理
        [wself.downloadQueue addOperation:operation];
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            7.通过添加Operation 依赖(A依赖于B,那么B执行完毕才会执行A),来模拟后进先出的执行顺序
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }

总结

SDWebImageDownloader为每一个下载图片请求创建一个 SDWebImageDownloaderOperation ,并放入自定义的队列中管理,当图片下载完成时,在主线程回调下载完成的block ,执行imageView.image=image 等其它自定义操作


补充

Options参数

我们在使用UIImageView Category
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:
(SDWebImageOptions)options ;
或者直接使用SDWebImageDownloader 
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

这个options参数提供我们更多的自定义操作

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
	//1.下载图片优先级低
	//默认情况下,有 UI 事件发生时,比如点击按钮, tableview 滚动,下载任务也会同时在其他线程异步执行,并不会阻塞主线程,但下载会消耗 cpu, 可能会造成卡顿.
	//设置这个LowPriority 后,只有 tableview 不滚动时才会下载.
    SDWebImageDownloaderLowPriority = 1 << 0,

    //2.图片会从上到下,下载一些显示一些,网速慢的时候,优化体验
    SDWebImageDownloaderProgressiveDownload = 1 << 1, 

    //3.默认不使用URL缓存,使用SDWebImage的磁盘内存缓存 使用此参数开启URL缓存
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    //4.回调下载完成Block时, 如果图片是从NSURL读取的 那么,回调函数 imageData=nil 
    //通常和 SDWebImageDownloaderUseNSURLCache一起使用
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    //5.如果App进入后台,启用这个参数会在向系统要求额外的时间来将下载图片队列中的下载请求执行完毕 
    //如果额外的下载时间过长可能会被系统主动取消下载操作
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    //6.设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES; 处理Cookie的存储
    SDWebImageDownloaderHandleCookies = 1 << 5,

    //7.允许不安全的SSL传输,如果后台配置了https,测试阶段可以加这个参数,Release时取消这参数
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    //8.下载图片优先级高
    SDWebImageDownloaderHighPriority = 1 << 7,
};

下载的部分大概就是这样,NSUrlConnection 在iOS 9 以后被弃用了,不知以后SDWebImage会如何改动


关于 beginBackgroundTaskWithExpirationHandler

可能会有人有疑问,程序可能还没有进入后台,为什么现在,而且每次下载一张图片都做一次后台运行的请求呢,这个在App Programming Guide 的BackGround Excution中有说明,里面有一段最佳实践的代码,详细请看 BackGround Excution 一节

关于 dispatch_barrier_sync 和 dispatch_barrier_async

这个可以理解为给 dispatch_sync和dispatch_async 加了一把锁

当这个 barrier block到队列头时,他会等待前面正在执行的block执行完才开始执行自己,执行本身的同时,后面的block等待barrier block 执行完毕才会继续执行。这保证了它能单独执行.

这个方法的用于多线程的读写情况,

我们有一个Mutable字典,我们需要读写这个字典,在多线程的环境下,假如我们在读的过程中,写操作也同时在执行,那么我们可能会得到意想不到的结果,和脏乱的字典数据,我们允许多个线程同时读取字典,但要保证同时只能有一段代码执行写字典的操作.

这时我们这样写

_cache = [[NSMutableDictionary alloc] init];
_queue = dispatch_queue_create("com.mikeash.cachequeue", DISPATCH_QUEUE_CONCURRENT);

1.读取的代码用dispatch_sync包裹
    - (id)cacheObjectForKey: (id)key
    {
        __block obj;
        dispatch_sync(_queue, ^{
            obj = [[_cache objectForKey: key] retain];
        });
        return [obj autorelease];
    }

2.写入的代码用 barrier包裹
    - (void)setCacheObject: (id)obj forKey: (id)key
    {
        dispatch_barrier_async(_queue, ^{
            [_cache setObject: obj forKey: key];
        });
    }

当这个 barrier block到队列头时,他会等待前面正在执行的block执行完才开始执行自己,执行本身的同时,后面的block等待barrier block 执行完毕才会继续执行。这保证了它能单独执行.

barrier保证了字典在多线程环境下的读写安全

如果你对Dispatch_barrier_async有兴趣,这里有2篇关于 barrier的文章

Dispatch_barrier_async的研究

What's New in GCD

下一篇 中我会分析 SDWebImage 源码中的缓存模块的实现