iOS | SDWebImage 介绍 & 源码解析

简介

SDWebImage 是目前使用最广泛的第三方图片处理库,它不仅能够异步加载网络图片,还提供了一套图片缓存管理机制,功能非常强大。我们使用只要一行代码就可以轻松的实现图片异步下载和缓存功能。

image.png

本文主要针对v4.0.0版本进行分析, 从3.0升级4.0的小伙伴可以查看 SDWebImage 4.0迁移指南

特性

  • 提供UIImageViewUIButton的分类, 支持网络图片的加载与缓存管理
  • 一个异步的图片下载器
  • 异步(内存+磁盘)缓存和自动过期处理缓存
  • 后台图片解压缩
  • 同一个 URL 不会重复下载
  • 自动识别无效 URL,不会反复重试
  • 不阻塞主线程
  • 高性能
  • 使用 GCDARC

支持的图像格式

  • 支持UIImage(JPEG,PNG,...),包括GIF(从4.0版本开始,依靠FLAnimatedImage来处理动画图像)
  • WebP格式,包括动画WebP(使用WebPsubspec

使用

UIImageView+WebCache
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];;

这是UIImageView的一个扩展方法,通过这个方法就已经实现了 图片异步加载缓存机制功能了, 是不是超简单呢


加载流程

首先: 我们通过时序图,来了解一下框架的基本加载流程~

SDWebImageSequenceDiagram.png

  1. 当我们的UIImageViewUIButton控件调用sd_setImageWithURL: ()...方法 来进行加载图片;
  2. 框架会直接调用 UIView+WebCache中的sd_internalSetImageWithURL:() ..., 该方法是UIImageViewUIButton 的共有拓展方法
  3. 接下来调用SDWebImageManager类中的loadImageWithURL:() ...方法,会根据提供的图片URL 加载图片,SDWebImageManager 主要负责管理SDImageCache缓存和SDWebImageDownloader下载器
  4. 首先进入 SDImageCache类,调用 queryCacheOperationForKey...在内存或者磁盘进行查询,如果有图片缓存则进行回调展示, 如果没有查询到图片缓存,则进行下一步下载
  5. 在未查询到图片缓存时, SDWebImageDownloader类会进行网络下载,下载成功后进行回调展示,并将下载的图片缓存到内存和磁盘

通俗理解: 根据Url内存中查询图片,如果有则展示,没有则在磁盘查询图片,查询到展示, 没有查询到在会通过网络下载进行展示。下载完后会存储到内存和磁盘,方便下次直接使用,磁盘查询和网络下载都是异步的,不会影响主线程.

源码分析

UIView+WebCache 加载逻辑

我们了解了基本的加载流程后,接下来我们看看他是如何一步一步实现的,首先我们来看一下
UIView+WebCache.m文件中的加载图片源码:

/**
 @param url            图片URL
 @param placeholder    占位图
 @param options        加载选项(SDWebImageOptions枚举类型)
 @param operationKey   要用作操的key键。如果为nil,将使用类名
 @param setImageBlock  自定义Block块
 @param progressBlock  下载进度Block块
 @param completedBlock 加载完成Block块
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {

    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

    //  这个方法很强大,用于保障当前的加载操作,不会被之前的加载操作污染(会将之前的加载操作取消)
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //  占位图设置
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    if (url) {
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        __weak __typeof(self)wself = self;
        
        //  通过 SDWebImageManager类来进行图片加载
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    
            __strong __typeof (wself) sself = wself;
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            //  会判断是否是否在主线程,如果在则直接回调,不在则执行主线程异步回调
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                
                //  如果选项为 SDWebImageAvoidAutoSetImage(用户手动设置), 则Block回调.不会给ImageView赋值,由用户自己操作
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image)
                {
                    //  判断是UIimageView还是UIButton给对应的控件设置图像
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    //  刷新视图布局
                    [sself sd_setNeedsLayout];
                } else {
                    //  在加载网络图片失败后,展示展位图
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //  加载完成回调
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //  将 operation添加到 operation字典中
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    }
    else
    {
        //  url为nil 则直接进行错误回调提示
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

UIView+WebCacheUIImageView提供了方法扩展,上面方法作为加载图片的入口,逻辑也简单清晰;

视图每次再加载图片之前, 都会调用 sd_cancelImageLoadOperationWithKey:(nullable NSString *)key 方法, 用于取消上一个URl的加载操作, (比如: TalbeViewCell中的ImageView一般都是复用的,如果快速滑动, 这个ImageView会加载很多次不同的URL, 这个方法可以保证展示和加载的URL地址是最新的,并且会将之前的操作取消);
这个方法是UIView+WebCacheOperation类中提供的一个方法,我们看下具体实现

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    
    // SDOperationsDictionary是一个字典类型, key是当前视图的字符串, value是视图加载的操作
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    
    //  通过key获取到加载操作,operations实际是一个 SDWebImageCombinedOperation类型,并遵守了<SDWebImageOperation>协议
    id operations = operationDictionary[key];
    //  将操作取消掉
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel]; //在协议方法中实现取消操作
        }
        //  并从字典中将操作移除
        [operationDictionary removeObjectForKey:key];
    }
}

每个视图内部会有一个SDOperationsDictionary 字典, key是当前视图的字符串, value是视图加载的操作, 这个操作类遵守<SDWebImageOperation>协议, 在协议方法中实现真正的取消操作

dispatch_main_async_safe(^{})宏定义

#ifndef dispatch_main_async_safe        
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

该宏可以判断是否是在主线程,如果在主线程则直接异步执行block, 如果不在则调用 dispatch_async(dispatch_get_main_queue(), block);回主线程异步执行;

SDWebImageManager

SDWebImageManager是一个单例管理类,主要用于管理SDWebImageDownloader以及SDImageCacheSDImageCache类负责执行图片的缓存,SDWebImageDownloader类负责图片的下载任务。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,

@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

我们看到他有个内部有一个 id<SDWebImageManagerDelegate> delegate属性,SDWebImageManagerDelegate声明如下:

// 缓存中没有图片的话是否下载这张图片,如果返回NO的话,则如果缓存中没有这张图片,也不会去重新下载
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

如果我们要设置 只加载缓存,不加载网络图片,或者在拿到图片做一些处理,可以遵守改协议,自己实现加载逻辑;

SDWebImageManager.m页面同样还有一个 SDWebImageCombinedOperation类,并遵守<SDWebImageOperation>协议,主要负责取消一些未执行的NSOperation操作和正在执行的操作

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;

@end

协议方法实现:

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        _cancelBlock = nil;
    }
}

这个就是文章一开始,每次视图加载前都需要取消之前的操作所执行的方法,通过cancelled属性来中断正在进行的操作

下面我们来看下:SDWebImageManagerloadImageWithURL: options: progress: completed...加载图片源码具体实现:

/*
    加载图片,返回缓存版本。 如果不在缓存中,则使用给的URL下载图像
 */
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    
    // 如果是NSString类型则进行强转NSurl
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // 防止应用程序在参数类型错误上崩溃
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    // 自定义Operation对象(继承NSObject) 内部有一个 NSOperation对象
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    //  检查是否为Url是否是失效的
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    //  如果url为空或者是失效的,则直接block回调,并附加上错误信息,不再进行加载操作
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }
    //  将当前操作 添加 到正在运行的操作  集合中
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //  url转字符串 Key
    NSString *key = [self cacheKeyForURL:url];

    //  并在缓存中查询 图片,如果内存中有,直接回调, 内存中没有,则会创建NSoperation操作,异步在 磁盘查询
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        
        //  判断操作是否被取消了
        if (operation.isCancelled) {
            
            //  从正在执行的操作中移除,并renturn操作
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        //  没有图片缓存  或者 options 为 SDWebImageRefreshCached ,或者代理设置需要网络加载,从网络刷新
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            
            //  如果有缓存,但是提供了SDWebImageRefreshCached,先展示之前的缓存
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            
            // 根据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 (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                //如果图像已经缓存但强制刷新,则强制关闭
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                //忽略从NSURLCache读取的图像,如果缓存了图像,则强制刷新
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }   
            //  使用给定的URL创建SDWebImageDownloader加载器实例
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {      
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 如果Operation被取消
                }
                else if (error)
                {
                    // 错误回调
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) { 
                        //  加入黑名单
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else
                {
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    //  是否磁盘缓存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        
                        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];
                                // 如果图像被转换,传递nil,这样我们就可以重新计算图像中的数据
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            } 
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //  磁盘缓存
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                if (finished) {
                    //  移除 Operation操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };  
        }
        else if (cachedImage)   // 从内存或者磁盘中查询到图片信息
        {
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            //  进行Block回调
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            //  从正在运行的集合中移除 移除 operation操作
            [self safelyRemoveOperationFromRunning:operation];
        }
        else
        {
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];
    return operation;
}

以上代码逻辑还是比较清晰的,方法内部会创建一个 SDWebImageCombinedOperation对象,这个对象用于贯穿缓存查询和下载操作,如果operation.isCancelledYES,则回调操作会直接 return,不在继续执行;

方法内部首先会调用了[self.imageCache queryCacheOperationForKey:()...]方法,用于查询图片缓存,如果没有缓存则会调用[self.imageDownloader downloadImageWithURL:()...] 下载方法, 在获得到图片后,会将图片通过Block回调传给视图。

SDImageCache缓存类

SDImageCache是一个全局单例类, 负责处理内存缓存及磁盘缓存。其中磁盘缓存的读写操作是异步的,这样就不会对UI操作造成影响。在SDImageCache内部有一个NSCache属性,用于处理内存缓存, NSCache是一个类似NSDictionary一个可变的集合,当内存警告时内部自动清理部分缓存数据。磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache还定义了一个串行队列,来异步存储图片。

内存缓存与磁盘缓存相关变量的声明及定义如下:

@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;                      
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;  
@end
@implementation SDImageCache {
    NSFileManager *_fileManager;
}              
// 初始化
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

我们看下SDImageCache查询缓存源码:

//  异步查询缓存并在完成时调用完成的操作。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    
    //  key是空的则直接回调,并返回nil
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // 首先检查内存缓存…
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    //  如果内存中有图片
    if (image) {
        NSData *diskData = nil;
        //  判断是否为gif图
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        //  回调内存图片信息
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    // 内存未查到图片信息 则创建一个 operation
    NSOperation *operation = [NSOperation new];
    
    //  在一个串行里异步执行
    dispatch_async(self.ioQueue, ^{
        
         // 如果取消了,则进行回调
        if (operation.isCancelled) {
            return;
        }
        // 从磁盘进行查询
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            
            //  判断是否有图片并需要内存缓存
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //  内存缓存一份
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            
            //  进行回调
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

通过上面的代码,我们可以看出, 首先在内存中查询图片通过imageFromMemoryCacheForKey:() ...

//  从内存中查看是否有改图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

key为图片的 URL 地址, Value则是缓存图片,如果内存没有缓存,则会调用[self diskImageDataBySearchingAllPathsForKey:key]进行磁盘查询

//  磁盘查询
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
    if (data) {
        return data;
    }
    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }
    return nil;
}

如果内存和磁盘都未查询到图片,则会进行网络请求下载图片

另外SDImageCache还提供了存储图片

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // 内存缓存
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    // 如果磁盘缓存
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{ // 在一个串行队列里面进行
            NSData *data = imageData;
            
            if (!data && image) {
                SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];   // 获取图片格式
                data = [image sd_imageDataAsFormat:imageFormatFromData];  //根据格式返回二进制数据
            }
            
            [self storeImageDataToDisk:data forKey:key];    // 存储到磁盘
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task放到串行队列中处理。在方法中,会先检测图片是PNG还是JPEG,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5后的串)。

SDWebImageDownloader下载管理器

SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载的管理。实质的下载操作则是通过SDWebImageDownloaderOperation类继承NSOperation,图片的下载操作是放在一个NSOperationQueue操作队列中来完成的。

@property (strong, nonatomic) NSOperationQueue *downloadQueue;

默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader类的maxConcurrentDownloads属性来修改。

我看来看下downloadImageWithURL:()..options:(): progress():completed:()下载操作内部实现

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;
    
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        //  默认15秒
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        //  为了防止潜在的重复缓存(NSURLCache + SDImageCache),如果被告知其他信息,我们将禁用图像请求的缓存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        //  创建下载操作
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        //  是否解码
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        // 操作加入队列
        [sself.downloadQueue addOperation:operation];
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }
        return operation;
    }];
}

该方法的内部会调用addProgressCallback..方法,会将图片下载的一些回调信息存储在SDWebImageDownloaderOperation类的URLCallbacks属性中,该属性是一个字典,key是图片的URL地址value则是一个数组,包含每个图片的多组回调信息。

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    
    // URL将用作回调字典的键,因此它不能为nil。如果是nil,立即调用完整的块,没有图像或数据。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    __block SDWebImageDownloadToken *token = nil;
    
    //  按顺序依次执行
    dispatch_barrier_sync(self.barrierQueue, ^{
        
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        
        if (!operation) {
            // 创建下载 SDWebImageDownloaderOperation操作
            operation = createCallback();
            
            //  添加到URLOperations操作缓存中
            self.URLOperations[url] = operation;
            
            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                
                SDWebImageDownloaderOperation *soperation = woperation;
                if (!soperation) return;
                if (self.URLOperations[url] == soperation) {
                    //  下载完移除下载操作
                    [self.URLOperations removeObjectForKey:url];
                };
            };
        }
        //  将一些回到信息放入`SDWebImageDownloaderOperation`类的`callbackBlocks`属性中,并创建 downloadOperationCancelToken实例
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });
    
    return token;
}

SDWebImageDownloaderOperation对象加入到操作队列后,就开始调用该对象的start方法,代码如下

// SDWebImageDownloaderOperation.m
- (void)start {
    // 如果操作被取消,就reset设置
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

        ...
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            // 创建session的配置
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            // 创建session对象
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    // 开始下载任务
    [self.dataTask resume];

    if (self.dataTask) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        // 创建任务失败
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }
    ...
}

在下载过程中,会涉及鉴权、响应的statusCode判断(404304等等),以及收到数据后的进度回调等等,在最后的URLSession:task:didCompleteWithError里做最后的处理,然后回调完成的block,下面仅分析一下- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error的方法

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    ...
    if (error) {
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
             *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
             *    and images for which responseFromCached is YES (only the ones that cannot be cached).
             *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
             */
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                // 如果options是忽略缓存,而图片又是从缓存中取的,就给回调传入nil
                [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
            } else if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                // 缓存图片
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 跳转图片的大小
                image = [self scaledImageForKey:key image:image];
                
                // Do not force decoding animated GIFs
                if (!image.images) {
                    // 不是Gif图像
                    if (self.shouldDecompressImages) {
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
#endif
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    // 下载是图片大小的0
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                } else {
                    // 把下载的图片作为参数回调
                    [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    ...
}
以上是个人对SDWebImage加载图片流程的理解,如有错误之处,还望各路大侠给予指出!
SDWebImage源码地址: https://github.com/SDWebImage/SDWebImage
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容