# SDWebImage源码阅读 @[git 地址] [https://github.com/SDWebImage/SDWebImage](https://github.com/SDWebImage/SDWebImage) ### 5\.x的新增特性和变化 迁移文档 [https://github.com/SDWebImage/SDWebImage/wiki/5.0\-Migration\-guide](https://github.com/SDWebImage/SDWebImage/wiki/5.0-Migration-guide) #### SDWebImageContext 新增SDWebImageContext/SDWebImageMutableContext参数 ``` objectivec typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM; typedef NSDictionary SDWebImageContext; typedef NSMutableDictionarySDWebImageMutableContext; ``` SDWebImageContextOption 是一个可扩展的String枚举 SDWebImageContext / SDWebImageMutableContext 是以 SDWebImageContextOption为key、id(指定类型或者协议)为value 的NSDictionary/NSMutableDictionary 具体定义见 SDWebImageDefine.h、SDImageLoader.h 举例解释下 1、 SDWebImageContextSetImageOperationKey 通常情况下,这个key对应的value用于指定当前的id保存在NSMapTable中的key,后续流程的cancel、remove等都需要通过这个key来找到对应的operation。但需要注意,SDWebImageContextSetImageOperationKey并不是在所有地方使用都生效的。如使用UIImageView+HighlightedWebCache或者NSButton+WebCache的一些方法使用时,这个值被来保存、区分不同状态图片加载operation,所以即使设置了SDWebImageContextSetImageOperationKey也可能会被覆盖。 2、SDWebImageContextCacheSerializer 值为 id,转换需要缓存的图片格式可以传入遵循协议的对象,实现协议中的方法 ``` objectivec - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL; ``` 再这方法里可以去进行image的序列化处理。 值得注意的是SDWebImageManager也有id cacheSerializer属性,但只有当SDWebImageContext没有配置SDWebImageContextCacheSerializer时才使用前者。 ``` objectivec if (!context[SDWebImageContextCacheSerializer]) { id cacheSerializer = self.cacheSerializer; [mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer]; } ``` 个人觉得SDWebImageContextOption很灵活,可以支持很多自定义,比之前版本的option强大很多,之前的option都是内部实现好,而context是支持上层自定义实现的,有人把SDWebImageContextOption比作流水线,从UIKit层到Cache和Download层,流水线经过每一次都会去获取相应应的配置。 想到的可能的缺点是必须注意配置优先级 ,或者必须先了解清楚具体context是做什么哪个阶段使用的,这个比起之前option多了点学习成本。 #### Animated Image View 5.0引入全新的机制来支持动画图像。这包括动图加载、渲染、解码,还支持自定义。 #### Image Transformer 支持用户配置图片下载后缩放、旋转、添加圆角等操作 #### View Indicator 视图loading标识 #### Customization 协议化是5.0后非常棒的特性,可以支持自定义cache、loader和coder 看一段SDWebImageManager\.m 中的代码 5\.0前 ``` - (nonnull instancetype)init { SDImageCache *cache = [SDImageCache sharedImageCache]; SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader]; return [self initWithCache:cache downloader:downloader]; } ``` 5\.x后 ``` - (nonnull instancetype)init { id cache = [[self class] defaultImageCache]; if (!cache) { cache = [SDImageCache sharedImageCache]; } id loader = [[self class] defaultImageLoader]; if (!loader) { loader = [SDWebImageDownloader sharedDownloader]; } return [self initWithCache:cache loader:loader]; } ``` 协议化处理扩展性强,支持自定义,插件化 依赖注入,可以上层注入符合这个指定协议的对象 例子: [https://github.com/SDWebImage/SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) ####其他一些变化 1、主队列的判断 SDWebImageCompat.h 中 一个条件编译判断条件由 isMainThread 改为了 dispatch_queue_t label 是否相等 ``` #ifndef dispatch_main_async_safe #define dispatch_main_async_safe(block)\ if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\ block();\ } else {\ dispatch_async(dispatch_get_main_queue(), block);\ } #endif ``` 以前版本 ``` #define dispatch_main_async_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_async(dispatch_get_main_queue(), block);\ } #endif ``` dispatch\_get\_main\_queue 的文档说明 ``` Return Value Returns the main queue. This queue is created automatically on behalf of the main thread before is called. ## Discussion The system automatically creates the main queue and associates it with your application’s main thread. Your app uses one (and only one) of the following three approaches to invoke blocks submitted to the main queue: * Calling [](apple-reference-documentation://hcLi_X2d-x) * Calling [](apple-reference-documentation://hcwqXjGwiK) (iOS) or [](apple-reference-documentation://hcN2ZoKwK1) (macOS) * Using a [](apple-reference-documentation://hcfamW1966) on the main thread As with the global concurrent queues, calls to [](apple-reference-documentation://hc23MT12Cv), [](apple-reference-documentation://hcM68CEpXQ), [](apple-reference-documentation://hclMsqx2ga), and the like have no effect when used with queues returned by this function. ``` 上述文档大概说明了: 主队列的任务一定在主线程上执行 主队列伴随主线程产生 主队列是系统创建,关联到主线程中 把任务提交到主队列的三种方式 http://blog.benjamin-encz.de/post/main-queue-vs-main-thread/ > Calling an API from a non-main queue that is executing on the main thread will lead to issues if the library (like VektorKit) relies on checking for execution on the main queue. 2、NS_TYPED_EXTENSIBLE_ENUM 上面提到 SDWebImageContextOption 是一个可扩展的String枚举 ,下面的SDImageFormat也是可扩展枚举NS_TYPED_EXTENSIBLE_ENUM。 它们都是用于为Objective-C桥接Swift所使用的 NS_EXTENSIBLE_STRING_ENUM 这个宏用于组织字符串常量 NS_TYPED_EXTENSIBLE_ENUM 可以组织任意类型的相关常量 ``` typedef NSInteger SDImageFormat NS_TYPED_EXTENSIBLE_ENUM; static const SDImageFormat SDImageFormatUndefined = -1; static const SDImageFormat SDImageFormatJPEG = 0; static const SDImageFormat SDImageFormatPNG = 1; static const SDImageFormat SDImageFormatGIF = 2; ... ``` 比如以上代码,swift中可能以以下形式导入的: ``` struct SDImageFormat: RawRepresentable, Equatable, Hashable { typealias RawValue = Int init(_ rawValue: RawValue) init(rawValue: RawValue) var rawValue: RawValue { get } static var undefined: SDImageFormat { get } static var jepg: SDImageFormat { get } static var png: SDImageFormat { get } } ``` 可以进行扩展 ``` extension SDImageFormat { static var abc: SDImageFormat { return x } } ``` ### 整体流程 1、上层UIKit 的Catergory 发起网络请求 2、SDWebImageManager 根据类型不同进行分发交给不同的类 3、取消当前控件上进行的Operation ``` operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; ``` 这里用了NSMapTable 为什么不用NSDictionary? NSDictionary key值遵循NSCoping协议, akey对象被copy一份后存入。 如果想以对象为key,对象必须遵循NSCoping协议,实现NSMutableDictionary使用hash表来实现key和value之间的映射和存储,所以作为key值的类型必须重写\-hash 和 \-isEqual: 方法。 而NSMapTable 可以指定option key值释放后,可以自动移除弱引用的value值 4、设置placeholder 5、通过SDWebImageManager去获取图片 可能是从缓存读取也可能是新的下载, 创建SDWebImageCombinedOperation对象 ,有两个遵循SDWebImageOperation协议的属性:cacheOperation和loaderOperation. 6、容错处理 对url判断等,包括对一些option值的判断,之前一段时间对SDWebImageRetryFailed有误解,以为会自动进行失败重试,实际上如果没设置SDWebImageRetryFailed默认失败后会加入到failedURLs中,后续不再请求该url。 7、SDWebImageCombinedOperation加入self.runningOperations中 SD_LOCK(self.runningOperationsLock); [self.runningOperations addObject:operation]; SD_UNLOCK(self.runningOperationsLock); SD_LOCK 是信号量的宏,比较简洁,以后可以借鉴。 以上是图片实际加载前的manager中的一些关键节点,接下来分别看看对几个主要流程。 #### 缓存 缓存包括memCache和diskCache两部分。 内存缓存采用的是NSCache 和 NSMapTable结合的方式 weakCache作为二级缓存的意义? 之前浏览器遇到一个问题,收到内存警告时,由于NSCache可能被系统清除,如果再访问MemoryCache时因为内存中找不到缓存就会读取磁盘缓存,可能导致闪烁的现象。weakCache可以解决这个问题,只要开始shouldUseWeakMemoryCache,weakCache保存着图片对象的弱引用,当收到内存警告时NSCache被清除,也就是取消对所有缓存image的引用,然而weakCache的value只是弱引用,所以只要没有其他强引用的image对象就会被释放, 被UIImageView实例强引用的image仍然保留,可以通过weakCache获取,并同步回NSCache中,这样可以有效减少磁盘IO。 #### 下载器 之前版本下载只能SDWebImageDownloader来实现,现在SDWebImageDownloader 只是 SDImageLoader 协议在 内部的默认实现,而且比之起之前开放整个下载过程的的可配置性。 强大的可配置性具体体现在 SDWebImageDownloaderConfig ``` ///默认6个 @property (nonatomic, assign) NSInteger maxConcurrentDownloads; /// 默认 15.0s. @property (nonatomic, assign) NSTimeInterval downloadTimeout; /// 自定义 sessionConfiguration @property (nonatomic, strong, nullable) NSURLSessionConfiguration *sessionConfiguration; /// 动态扩展类,需要遵循 `NSOperation` 以实现 SDImageLoader 定制 @property (nonatomic, assign, nullable) Class operationClass; /// 图片下载顺序,默认 FIFO @property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder; ``` Request/Response Modifier、decryptor ``` ///下载前修改request @property (nonatomic, strong, nullable) id`` requestModifier; ///收到response后对返回值进行修改 @property (nonatomic, strong, nullable) id`` responseModifier; ///用于图片解密,默认提供了对imageData的base64转换 @property (nonatomic, strong, nullable) id decryptor; ``` 开始下载时判断operation是否存在, 如果 operation不存在、任务被取消、任务已完成,调用 createDownloaderOperationWithUrl:options:context: 创建出新的 operation 并存储在 URLOperations 中 。同时会配置operation的 completionBlock,当任务完成时清理 URLOperations。 ``` @property (strong, nonatomic, nonnull) NSMutableDictionary *> *URLOperations; ``` URLOperations是一个字典,key值是URL,也就是一个operation对应一个URL,相同URL下载可以复用之前URLOperations中的operation。 如果存在则执行以下 ``` // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue) // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. @synchronized (operation) { downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; } ``` 这里使用@synchronized(operation)且在SDWebImageDownloaderOperation内部用@synchonzied (self)是为了保证不同类间operation的线程安全,因为重用download operation会引起多个回调,回调可能在不同的队列中(解码或者代理队列)这样可能引起线程安全问题。 operation 执行 addHandlersForProgress: 时并没有清除之前存储的 callbacks ,如果有多次调用的的情况那么 callBack 在完成后都会被依次执行。 当然如果执行opearation cancel,则立即执行所有的callback,传入error等 ``` for (SDWebImageDownloaderCompletedBlock completedBlock in self.completedBlocks) { completedBlock(nil, nil, error, YES); } ``` 最后执行 ``` SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation]; token.url = url; token.request = operation.request; token.downloadOperationCancelToken = downloadOperationCancelToken; ``` 相当于SDWebImageDownloadToken是每一个下载的唯一身份标识,外部可以直接根据token取消一个下载操作。 #### 解码 磁盘 存储 NSData 读取到内存中 NSData \-\> UIImage png /jpeg 压缩的图片 decode解压成位图 (内存比较大、耗时 子线程) 在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率通过 方法把图片提前解压加载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种空间换时间的做法。 ### 相关设计模式 1、单例 2、代理模式 3、装饰器模式 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。 eg.类扩展 4、外观模式 外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。为子系统中一组不同的接口提供统一的接口。 外观定义了上层接口,通过降低复杂度和隐藏子系统间的通信及依存关系,让子系统更易于使用。 特点 * 每个子系统层级有一个外观作为入口 * 让它们通过其外观进行通信,可以简化它们之间的依赖关系 SDWenImageManager 5、策略模式 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。 6、适配器模式 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 @protocol SDWebImageOperation \- \(void\)cancel; @end 所有实现该协议的对象都可以cancel,哪怕是不同类型的对象 如SDWebImageDownloaderOperation、SDWebImageCombinedOperation 我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作