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 = 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


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

推荐阅读更多精彩内容