近期公司项目用到SDWebImage(v3.8.2),来实现聊天图片的缓存,正好有时间,想好好研究下SDWebImage。本文是阅读网上许多关于SDWebImage文章加上个人理解写下的,也方便日后复习。
目录
- 前言
- 思维导图
- 源码结构
- 目录结构
- 运行流程
- 核心方法的解析
- 完整核心方法注释
前言
首先老套路来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:
Asynchronous image downloader with cache support with an UIImageView category.
一个异步下载图片并且支持缓存的 UIImageView 分类.就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:
基本方法1
| [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
|
在看源码之前,先对思维导图、源码结构、运行流程先有个总体把握可以加深我们对后续代码的理解。
思维导图
源码结构
源码结构图已经将这个框架是如何组织的基本展现出来, UIImageView+WebCache 和 UIButton+WebCache 直接为表层的 UIKit框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloader 和 SDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.
目录结构
- Downloader
- SDWebImageDownloader
- SDWebImageDownloaderOperation
- Cache
- Utils
- SDWebImageManager
- SDWebImageDecoder
- SDWebImagePrefetcher
- Categories
- UIView+WebCacheOperation
- UIImageView+WebCache
- UIImageView+HighlightedWebCache
- UIButton+WebCache
- MKAnnotationView+WebCache
- NSData+ImageContentType
- UIImage+GIF
- UIImage+MultiFormat
- UIImage+WebP
- Other
- SDWebImageOperation(协议)
- SDWebImageCompat(宏定义、常量、通用函数)
| 类名 |
功能 |
SDWebImageDownloader |
是专门用来下载图片和优化图片加载的,跟缓存没有关系 |
SDWebImageDownloaderOperation |
继承于 NSOperation,用来处理下载任务的 |
SDImageCache |
用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程 |
SDWebImageManager |
作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来 |
SDWebImageDecoder |
图片解码器,用于图片下载完成后进行解码 |
SDWebImagePrefetcher |
预下载图片,方便后续使用,图片下载的优先级低,其内部由 SDWebImageManager 来处理图片下载和缓存 |
UIView+WebCacheOperation |
用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation |
UIImageView+WebCache |
集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用 |
UIImageView+HighlightedWebCache |
跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片 |
UIButton+WebCache |
跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用 |
MKAnnotationView+WebCache |
跟 UIImageView+WebCache 类似 |
NSData+ImageContentType |
用于获取图片数据的格式(JPEG、PNG等) |
UIImage+GIF |
用于加载 GIF 动图 |
UIImage+WebP |
用于解码并加载 WebP 图片 |
运行流程
了解了工程目录结构之后,我们就可以看之前跳过的执行流程图了。
核心方法的解析
SDWebImage 的接口简单易用,开发者一句代码就能使用,代码: - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; ,复杂的逻辑和实现都被隐藏在这句代码的后面。各模块独立,Cache缓存模块提供库的内存缓存和可选的磁盘缓存支持,Downloader下载模块提供基于 NSURLSession 和NSOperation 的下载器。SDWebImageManager 则将 Cache 和 Downloader 两个模块很好的整合在一起,提供基于缓存的图片加载功能。最后是使用 Categories 封装了常用 UI 控件的接口。
我们看UIImageView+WebCache.h文件,我们可以发现为开发者提供了很多类似于下面的这个方法
1
| (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
|
这些方法最终都会调用- sd_setImageWithURL: placeholderImage: options: progress: completed:,这个方法可以算是核心方法,下面我们详细看一下内部实现 。
代码的第一句是
1
| [self sd_cancelCurrentImageLoad];
|
看方法名就知道它的作用,就是取消这个视图 ImageView 正在加载图片的操作,如果这个 ImageView 正在加载图片,保障在开始新的加载图片任务之前,取消掉正在进行的加载操作。
看下具体的实现代码 UIImageView+WebCache.m:
1 2 3 4 5 6
| - (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]; } - (void)sd_cancelCurrentAnimationImagesLoad { [self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"]; }
|
两个 key 说明有两个不一样的加载方式,一个是单张图片的,另一个是连续下载多张,放到 NSArray<UIImage *> *animationImages 中。
看下取消操作的代码实现,UIView+WebCacheOperation.m:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| - (NSMutableDictionary *)operationDictionary { NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey); if (operations) { return operations; } operations = [NSMutableDictionary dictionary]; objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return operations; } - (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key { [self sd_cancelImageLoadOperationWithKey:key]; NSMutableDictionary *operationDictionary = [self operationDictionary]; [operationDictionary setObject:operation forKey:key]; } - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key { NSMutableDictionary *operationDictionary = [self operationDictionary]; id operations = [operationDictionary objectForKey: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]; } }
|
代码中,通过 objc_setAssociatedObject 关联对象的方法,给 UIImageView 动态添加了一个 NSMutableDictionary 的属性。通过 key-value 维护这个 ImageView 已经有了哪些下载操作,如果是数组就是 UIImageViewAnimationImages 否则就是 UIImageViewImageLoad 。最后获得的都是遵从了 <SDWebImageOperation> 协议的对象,可以统一调用定义好的方法 cancel,达到取消下载操作的目的,如果 operation 都被取消了,则删除对应 key 的值。
继续看 - (void)sd_setImageWithURL: placeholderImage: options: ; 里的代码
1 2 3 4 5
| if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ self.image = placeholder; }); }
|
如果加载图片的选项不是 SDWebImageDelayPlaceholder 则会在主线程中先设置 placeholder 的占位图,
SDWebImageDelayPlaceholder 的情况后面说。dispatch_main_async_safe 是一个宏定义,点进去一看发现宏是这样定义的
1 2 3 4 5 6
| #define dispatch_main_sync_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_sync(dispatch_get_main_queue(), block);\ }
|
相信这个宏的名字已经讲他的作用解释的很清楚了: 因为图像的绘制只能在主线程完成, 所以, dispatch_main_sync_safe 就是为了保证 block 能在主线程中执行.
下面这段就是这个类中比较关键的代码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| if (url) { if ([self showActivityIndicatorView]) { [self addActivityIndicator]; } __weak __typeof(self)wself = self; id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); }
|
先检查 activityView 是否可用,可用的话给 ImageView 正中间添加一个活动指示器,并旋转,加载图片完成或失败都会清除掉。__weak __typeof(self)wself = self; 避免循环引用,接下来就是调用 SDWebImageManager 的方法 downloadImageWithURL: options: progress: completed: ,在该方法的 completed block 回调中,如果 option 是 SDWebImageAvoidAutoSetImage ,就是要求不要给 ImageView 自动设置图片,则只回调 completedBlock 然后 return,否则有 image 就设置给 ImageView 。没有 image 通常就是错误情况,如果 option 是 SDWebImageDelayPlaceholder 则设置占位图(可以设置成提示用户图片没加载出来的图片),最后回调 completedBlock。上面代码最后一句是把这个 operation 存到 ImageView 的 NSMutableDictionary 中,为了之前提到的 [self sd_cancelCurrentImageLoad]; 操作准备的。
完整核心方法注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { [self sd_cancelCurrentImageLoad]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 这是一个宏定义,因为图像的绘制只能在主线程完成,所以dispatch_main_sync_safe就是为了保证block在主线程中执行 if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ self.image = placeholder; }); } if (url) { if ([self showActivityIndicatorView]) { [self addActivityIndicator]; } __weak __typeof(self)wself = self; id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); } }
|
UIImageView+WebCache 类中的代码还是很容易的,逻辑很清晰,没有难懂的地方。请接着下篇 SDWebImageManager 源码阅读记录(二) 将SDWebImageManager解析核心方法:
相关阅读:
SDWebImage 源码阅读记录(一)
SDWebImageManager 源码阅读记录(二)
SDWebImageDownloader 源码阅读记录(三)
SDWebImageDownloaderOperation 源码阅读记录(四)
SDWebImageCache 源码阅读记录(五)
SDWebImage相关类 源码阅读记录(六)