SDWebImageManager 实现分析

  • UIButton+WebCache
  • UIImageView+WebCache
  • MKAnnotationView+WebCache

等Category 都是直接调用 SDWebImageManager 的方法
SDWebImageManager 内部使用 Downloader 和 Cache 来协调下载和缓存的任务.

SDWebImageManager分析

毫无疑问 SDWebImageManager 有这2个属性
**@property (strong, nonatomic, readonly) SDImageCache imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader imageDownloader;

SDWebImageManager 这些顾名思义的方法也都是直接调用 SDImageCache的方法来实现的.

- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
... ...

具体实现

大部分的缓存和下载工作都在这一个方法中完成

代码太长 ~ 无关代码有省略

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {

    1.如果你将 url 参数传成了字符串,我们帮你转成 NSURL
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    2.防止你乱传参数,console 输出奇怪的错误信息
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
    
    3.如果url 不正确,或者此 url 已经被下载过而且失败了, 也不需要重试,那么就取消下载,直接回调下载完成 block, 返回错误信息,
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    NSString *key = [self cacheKeyForURL:url];
    
    4.先查询缓存中是否存在
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    
    5.关于 cancel 下载操作,没有办法强制中断正在执行的 Operation, 当然你可以主动杀死 App..  
    一般取消的操作都是设置一个标记 flag, 在执行重要,耗时的操作之前检查这个标记,如果已经被取消了,就不继续执行.
        if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
            return;
        }
        
        6.询问代理是否应该下载这个 url 对应的图片
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                7.如果磁盘缓存中存在,直接回调下载完成 block 但是不中断下载任务,尝试,重新下载图片,为了让 NSURLCache 刷新缓存状态
                因为一个 url 对应的图片可能会变化,比如 url 对应一个用户的头像,而这个头像用户随时可能更改
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }
            
            7.终于开始准备下载图片了, 将SDWebImageOptions 和 SDWebImageDownloaderOptions 做一些协调转换的工作.
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) 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) {
            
                8.如果磁盘缓存中有图片,就关闭 progressive 下载方式(图片会从上到下,下载一部分,显示一部分)  
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;  
                9.如果磁盘缓存中有图片,让 NSURLCache 刷新缓存状态  
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;  
            }
            
            10.准备完成..正式调用下载工作
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                //什么都没有, Github issues 的 bugfix
                }
                else if (error) {
                
                11.出错了或者被取消了就回调下载完成的 block
                    dispatch_main_sync_safe(^{
                        if (strongOperation && !strongOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });
                    
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        
                        12.如果因为如上原因下载失败,就加入self.failedURLs 黑名单,如果没设置下载失败重试,下次就不下载了
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                    
                        13.如果设置了下载失败重试,就不加入黑名单,每次都重新下载图片,不管上次是否下载失败
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        14.如果这次下载是为了让 NSURLCache 刷新缓存状态 就不调用回调block
                    }
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                    
                        15.询问代理是否要在image 存储到缓存之前做一些最后的操作,(缩放,裁剪,圆角等)
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                
                                16.存入内存和磁盘缓存
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }
                            
                            17.回调主线程,图片终于下载完了,而且不是从缓存中取出来的...
                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    else {
                    18.如果没有实现代理,直接把图片存入缓存
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                        19,然后回调主线程,和17一样...
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
                
                19.将下载完成的 Operation 从runningOperations数组中移除
                SDWebImageManager的 isRunning 方法的实现是判断 self.runningOperations
                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];
            operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };
        }
        else if (image) {
        20.这怎么还有个完成的回调..其实些 block 回调嵌套的有点恶心..
        这个回调是在磁盘或者内存缓存中查询到图片时的回调,此时 image 为缓存中的数据, cacheType为 SDImageCacheTypeDisk或 SDImageCacheTypeMemory
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !strongOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
            21.这里的 else 是,如果缓存中没有,并且代理不允许下载这个 url 对应的图片,会执行下面的回调
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
    }];

    return operation;
}

这个虽然长但是并不难理解的下载缓存过程终于分析完了... SDWebImage 的核心我们也就理解了

SDWebImageOptions

这个 Options 类似第二篇中的 SDWebImageDownloaderOptions

在 UIImageView 等的 Category,或者直接调用SDWebImageManager 下载都能自定义SDWebImageOptions设置来完成更多自定义的操作

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {

    //1.每次都重新尝试下载图片
    //下载失败后不将 图片的 url 加入黑名单,每次都重新下载图片
    //如果加入了黑名单,下次再请求这个 url 时直接返回下载失败,不尝试下载,节省资源
    SDWebImageRetryFailed = 1 << 0,


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

    //3.只启用内存缓存,可以用它实现隐私浏览?
    SDWebImageCacheMemoryOnly = 1 << 2,

    //4.图片会从上到下,下载一些显示一些,网速慢的时候,优化体验,默认不开启
    SDWebImageProgressiveDownload = 1 << 3,


    //5.即使存在图片缓存,也尝试下载操作, 因为同一个 url 对应的图片可能会变化
    //例如用户的头像,用户可以随时上传更新头像,那我们就必须尝试下载更新这个图片,如果更新操作成功,会调用 下载完成的 completion Block
    SDWebImageRefreshCached = 1 << 4,

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

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

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

    //8.直接将这个图片下载任务放到下载队列的头,让这个下载任务先被执行
    SDWebImageHighPriority = 1 << 8,
    
    //9.延迟设置 PlaceHolder 图片,当图片下载完时才会设置 PlaceHolder, 那么默认情况下,ImageView 不会显示任何内容,只会显示其背景色.
    SDWebImageDelayPlaceholder = 1 << 9,

    //10.默认情况下,如果图片是 Gif ,不会调用代理方法 transformDownloadedImage 执行对图片的自定义操作,(关于代理方法下面一点点就会提到),设置这个 flag 对 Gif 也调用代理方法
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    //11.默认情况下,图片下载完成后就通过 imageView.image=image 被设置给 imageView,我们可以阻止这一行为,然后在下载完成回调方法中先处理图片,加圆角,加滤镜等,之后再手动设置给
    imageView
    SDWebImageAvoidAutoSetImage = 1 << 11
};

我们可以发现这有很多 Option和 SDWebImageDownloaderOptions 类似,因为在上面的具体实现中,就是将SDWebImageOptions和SDWebImageDownloaderOptions 做了一个转换或者说传递的工作
比如SDWebImageProgressiveDownload, SDWebImageContinueInBackground 等都是传递给它的下载模块来执行的.

P.S. 因为它是 NS_OPTIONS 所以我们可以同时设置多个 Option
类似 : SDWebImageRetryFailed | SDWebImageContinueInBackground

还有个 SDWebImageManagerDelegate

@protocol SDWebImageManagerDelegate <NSObject>
@optional

 可以控制当缓存中没有这个 url 对应的图片时,是否应该下载它,默认Yes 会下载
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

 允许下载,完成图片之后,放入缓存之前,做最后的操作,裁剪,圆角等
 注意,这个方法是在 get_global_queue 中执行的,不能调用设置 UI 的方法.
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

UIKit 的 Category

UIButton+WebCache
UIImageView+WebCache
MKAnnotationView+WebCache
这些的实现都很简单,都是调用 SDWebImageManager 来实现的..

SDWebImagePrefetcher

这个类能以低优先级预下载一些图片,以供后续的使用,提升用户体验.

以SDWebImageLowPriority 预下载,会在系统闲置时执行,不会影响主线程和 cpu的效率

主要的方法就一个

- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock;

也可以设置

NSUInteger maxConcurrentDownloads //最大同时下载的图片数量
SDWebImageOptions options  

内部实现也是调用 SDWebImageManager的方法,不在赘述


补充

UIImage+GIF

这个分类可以让UIImage 支持 Gif 图片

Gif 的本质是一张张的图片,每张展示一小段时间,连续的切换这些图片,看起来就是一张动图了..

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    
    1.获取 Gif 包含的真正图片数量
    size_t count = CGImageSourceGetCount(source);

    UIImage *animatedImage;
 
    if (count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data];
    }
    else {
        NSMutableArray *images = [NSMutableArray array];

        NSTimeInterval duration = 0.0f;

        for (size_t i = 0; i < count; i++) {
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            if (!image) {
                continue;
            }
            2.获取每一张图片,累加他们的播放时间,计算总的 Gif 播放时间
            duration += [self sd_frameDurationAtIndex:i source:source];
            
            3.将每一张图片存入数组中
            [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

            CGImageRelease(image);
        }

        if (!duration) {
            duration = (1.0f / 10.0f) * count;
        }
        
        4.根据总时长创建 UIImage 的 frame 动画
        animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }

    CFRelease(source);

    return animatedImage;
}

关于 Category 中的这段代码

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

不在本文章的范围内,如果你感兴趣,可以搜索关键字 Assiciate Object
或者看这篇不错的文章如何在 Category 中为类动态添加属性

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

推荐阅读更多精彩内容