SDWebImageManager

上一篇 文章中我们已经了解 SDWebImage 缓存模块
这篇文章我们来了解 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 中为类动态添加属性