SDWebImage源码初探(一)

最近项目进度缓慢了下来,决定看看各种源码来涨点知识。就先从SDWebImage开始吧!

在项目中用的最多的方法应该是UIImageView+WebCache与UIButton+WebCache里面的sd_setImageWithURL:这个系列的方法了。这里从UIImageView+WebCache开始看起。

其中该系列所有方法都基于:


- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL*)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

参数很简单明了,url是需要加载的图片url;placeholder是在url指向的图片加载完成之前暂时使用的图片;options是个枚举,用来控制加载图片时的一些设置(具体百度即可,大把);progressBlock则可以通过返回的receivedSize与expectedSize来花式展现下载进度;completedBlock则是下载完成后的Block;

下面来看作者的具体实现:


[self sd_cancelCurrentImageLoad];

按照意思来看是取消当前所有的图片加载,进入方法内部看:


- (void)sd_cancelImageLoadOperationWithKey:(NSString*)key {

// Cancel in progress downloader from queue

NSMutableDictionary*operationDictionary = [selfoperationDictionary];

idoperations = [operationDictionaryobjectForKey:key];

if(operations) {

if([operationsisKindOfClass:[NSArrayclass]]) {

for(id operationinoperations) {

if(operation) {

[operationcancel];

}

}

}elseif([operationsconformsToProtocol:@protocol(SDWebImageOperation)]){

[(id) operationscancel];

}

[operationDictionaryremoveObjectForKey:key];

}

}

首先取出一个operationDictionary,在取出这个字典的过程中,作者在分类中使用了objc_setAssociatedObject与objc_getAssociatedObject,来给分类添加属性。

接着通过传过来的key(@"UIImageViewImageLoad")来获取ImageView的加载队列。

最后,花式cancle掉这个队列中的任务。


[operation cancel];

[operation DictionaryremoveObjectForKey:key];

回到主方法,第二行又通过objc_setAssociatedObject方法将url关联到分类中,接着通过位与运算判断下option是否为SDWebImageDelayPlaceholder,来设置默认的占位图片。其中dispatch_main_async_safe这个宏很好用,避免了在主线程中造成死锁的情况。

然后是判断一下是否需要转动菊花:


if([selfshowActivityIndicatorView]) {

[selfaddActivityIndicator];

}

接下来是这个方法的核心部分,调用了SDWebImageManager中的


- (id)downloadImageWithURL:(NSURL*)url

options:(SDWebImageOptions)options

progress:(SDWebImageDownloaderProgressBlock)progressBlock

completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

由此方法完成图片的下载。

我们可以跳到SDWebImageManager.h中查看一下作者对于该方法的描述:

翻译过来的意思大概是:如果URL指定的图片不在缓存中就下载该图片,否则就返回缓存的版本。

回到这个方法的实现,前几行是判断URL的类型是否正确。


BOOLisFailedUrl =NO;

@synchronized(self.failedURLs) {

isFailedUrl = [self.failedURLscontainsObject:url];

}

这几句来获取传入的URL是否为之前下载失败过的URL,用一个BOOL值来记录下来。

如果URL不为空或者未设置options为SDWebImageRetryFailed项、且URL在黑名单之中,就会直接返回掉。


@synchronized(self.runningOperations) {

[self.runningOperationsaddObject:operation];

}

这段代码是先给运行中的下载队列加锁,避免多个线程同时对数组进行操作,将一个SDWebImageCombinedOperation对象加入到下载队列中。


NSString*key = [selfcacheKeyForURL:url];

operation.cacheOperation= [self.imageCache queryDiskCacheForKey:key done:^(UIImage*image,SDImageCacheTypecacheType) {

if(operation.isCancelled) {

@synchronized(self.runningOperations) {

[self.runningOperationsremoveObject:operation];

}

return;

}

将图片的URL当做key值,再调用 queryDiskCacheForKey:done:来获取缓存中的图片。
我们来看看这个方法的内部实现:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
//这里封装了NSChace的objectForKey方法,直接从内存缓存中获取图片对象
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
//如果内存缓存中获取到则直接返回

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }
//创建一个串行队列来获取磁盘缓存中的图片
        @autoreleasepool {
//创建内存池来及时的释放内存
            UIImage *diskImage = [self diskImageForKey:key];
//获取磁盘缓存中的图片
            if (diskImage && self.shouldCacheImagesInMemory) {
//如果磁盘缓存中有该图片,并且设置将图片缓存到内存中,则取出磁盘缓存的图片并且将其放入内存缓存中
                NSUInteger cost = SDCacheCostForImage(diskImage);
//计算出图片需要开销的内存大小
                [self.memCache setObject:diskImage forKey:key cost:cost];
//将图片缓存到内存中
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
//在主线程中回调Block
            });
        }
    });

    return operation;
}

缓存这里获取完成之后,来看下面的代码,有点长,我们分解开来一部分一部分阅读,先看这个判断:

if ((!image || options & SDWebImageRefreshCached)
//图片未从缓存中获取,或者是 option设置需要刷新缓存
&& (![self.delegate respondsToSelector:
@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
/*这部分条件中,如果imageManager:shouldDownloadImageForURL:方法未实现、或是实现了并且返回YES。可以从方法的名字中来理解,代理方法返回的BOOL为是否应该下载URL对应的图片。
*/

总而言之就是判断各种条件之下,图片是否应该被下载,让我们进入方法内部。

if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // If image was found in the cache bug SDWebImageRefreshCached is provided, notify about the cached image
                    // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
//翻译一下,如果图片在缓存中被找到但是options设置了SDWebImageRefreshCached(刷新缓存),通知这个缓存图片,并且试图从新下载这个图片,让服务端有机会刷新这个缓存。
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }
SDWebImageDownloaderOptions downloaderOptions = 0;
//初始化downloaderOptions
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            //如果options为低优先级,则设置downloaderOptions 为SDWebImageDownloaderLowPriority,下面的同理。
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (image && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
                //翻译一哈,两行代码的意思是,如果图片存在缓存并且需要刷新缓存,则强制取消掉SDWebImageDownloaderProgressiveDownload模式(渐进下载),
然后忽略从缓存中读取的图片。
            }

接下来使用SDWebImageDownloader来执行一个下载任务

id <SDWebImageOperation> subOperation = 
[self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:
^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) 

来看下载completedBlock中的第一部分处理

if (weakOperation.isCancelled) {
                  /*这里什么都没做操作,源代码中提及了#699号更新,于是我去看了下,大概意思是说:
当weakOperation取消的时候不要试图去调用completion block,dispatch_main_sync_safe()也无法保证这个block被终止的时候没有其他的代码在运行,所以其他代码运行时可能会被截断。
比如说,如果取消weakOperation后再调用completion block,那么在随后的一个TableViewCell中加载Image的completion block将会和这个completion block产生竞争关系。说的通俗一点就是,先调用的completion block里面的数据可能会被第二个completion block的数据覆盖掉。
*/
                }
                else if (error) {
                    dispatch_main_sync_safe(^{
                        if (!weakOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
//有错误信息,完成回调。
                    });

                    if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
//将发送错误的URL添加到黑名单里面
                        }
                    }
                }
else {
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
//options设置为SDWebImageRefreshCached选项,在缓存中又找到了image且没有下载成功
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                      //图片刷新时遇到了具有缓存的情况,不调用 completion block
                    }
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage))
                    && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                    //图片下载成功且图片设置为SDWebImageTransformAnimatedImage并且实现了imageManager:transformDownloadedImage:withURL:方法            
          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                            //调用delegate方法完成图片的变形
                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
                              //将变形后的图片缓存起来
                            }

                            dispatch_main_sync_safe(^{
                                if (!weakOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                              //在主线程中回调completedBlock
                                }
                            });
                        });
                    }
                    else {
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                          //如果没设置图片变形,并且下载完成,则直接缓存图片
                        }

                        dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            //在主线程中完成回调
                            }
                        });
                    }
                }
if (finished) {
                    @synchronized (self.runningOperations) {
                        [self.runningOperations removeObject:operation];
                   //从下载队列移除
                    }
                }
            }];
operation.cancelBlock = ^{
                [subOperation cancel];
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:weakOperation];
                
                }
            };
             //设置operation取消之后的一些操作
else if (image) {
        else if (image) {
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
                //在缓存中找到图片并且设置了不能下载的选项,完成回调
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
         //在缓存中没有找到图片,并且设置不能下载的选项
      
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
             //完成回调
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
    }];

    return operation;

这么一大段的方法按照功能排序来看,分解为首先创建下载operation,再读取系统的内存缓存与磁盘缓存,接着判断是否需要下载来进行下载操作,最后对下载的图片进行处理。

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

推荐阅读更多精彩内容