SDWebImage源码学习.md 19 KB

SDWebImage源码学习

[TOC]

@[git 地址] https://github.com/SDWebImage/SDWebImage

一、整体介绍

各个类关系

框架如何运行的

二、5.x的新增特性和变化

迁移文档 https://github.com/SDWebImage/SDWebImage/wiki/5.0-Migration-guide

SDWebImageContext

新增SDWebImageContext/SDWebImageMutableContext参数

typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
typedef NSMutableDictionary<SDWebImageContextOption, id>SDWebImageMutableContext;

SDWebImageContextOption 是一个可扩展的String枚举 SDWebImageContext / SDWebImageMutableContext 是以 SDWebImageContextOption为key、id(指定类型或者协议)为value 的NSDictionary/NSMutableDictionary 具体定义见 SDWebImageDefine.h、SDImageLoader.h

举例解释下 1、 SDWebImageContextSetImageOperationKey

通常情况下,这个key对应的value用于指定当前的id<SDWebImageOperation>保存在NSMapTable中的key,后续流程的cancel、remove等都需要通过这个key来找到对应的operation。但需要注意,SDWebImageContextSetImageOperationKey并不是在所有地方使用都生效的。如使用UIImageView+HighlightedWebCache或者NSButton+WebCache的一些方法使用时,这个值被来保存、区分不同状态图片加载operation,所以即使设置了SDWebImageContextSetImageOperationKey也可能会被覆盖。

2、SDWebImageContextCacheSerializer

值为id<SDWebImageCacheSerializer>,转换需要缓存的图片格式可以传入遵循协议的对象,实现协议中的方法

- (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL;

再这方法里可以去进行image的序列化处理。 值得注意的是SDWebImageManager也有id<SDWebImageCacheSerializer> cacheSerializer属性,但只有当SDWebImageContext没有配置SDWebImageContextCacheSerializer时才使用前者。

 if (!context[SDWebImageContextCacheSerializer]) {
    id<SDWebImageCacheSerializer> 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标识

####Photos Plugin 更方便地从Photos Library读取图片,具体见SDWebImagePhotosPlugin https://github.com/SDWebImage/SDWebImagePhotosPlugin

Customization

定制化 在5.0版本支持更多高级的定制,而无需使用hook的方式。支持了自定义缓存、自定义解码器、自定义Loader,可以从自己的源进行自定义加载(不一定是网络)。

####协议化 协议化是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<SDImageCache> cache = [[self class] defaultImageCache];
    if (!cache) {
        cache = [SDImageCache sharedImageCache];
    }
    id<SDImageLoader> loader = [[self class] defaultImageLoader];
    if (!loader) {
        loader = [SDWebImageDownloader sharedDownloader];
    }
    return [self initWithCache:cache loader:loader];
}

协议化处理扩展性强,支持自定义,插件化 依赖注入,可以上层注入符合这个指定协议的对象

例子: https://github.com/SDWebImage/SDWebImageYYPlugin

####Image Prefetcher SDWebImagePrefetcher 基础的api

- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;

5.x版本后,调用prefetchURLs:接口会返回SDWebImagePrefetchToken对象,SDWebImagePrefetchToken包含一个urls属性,图片预取时不会取消上一次预取任务,这做法实现预取器的共享;之前版本每次调用会取消上一次预取操作,如果想保持原来的逻辑,可以在调用前手动去取消预取。

开始预取方法是 startPrefetchWithToken: 实际的预取的operation外还套着SDAsyncBlockOperation,应该是为了方便任务的取消。

####其他一些变化

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<SDWebImageDownloaderOperation>` 以实现 SDImageLoader 定制
@property (nonatomic, assign, nullable) Class operationClass;
/// 图片下载顺序,默认 FIFO
@property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder;

Request/Response Modifier、decryptor

///下载前修改request
@property (nonatomic, strong, nullable) id`<SDWebImageDownloaderRequestModifier>` requestModifier;
///收到response后对返回值进行修改
@property (nonatomic, strong, nullable) id`<SDWebImageDownloaderResponseModifier>` responseModifier;
///用于图片解密,默认提供了对imageData的base64转换
@property (nonatomic, strong, nullable) id<SDWebImageDownloaderDecryptor> decryptor;

开始下载时判断operation是否存在, 如果 operation不存在、任务被取消、任务已完成,调用 createDownloaderOperationWithUrl:options:context: 创建出新的 operation 并存储在 URLOperations 中 。同时会配置operation的 completionBlock,当任务完成时清理 URLOperations。

@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, NSOperation<SDWebImageDownloaderOperation> *> *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取消一个下载操作。

解码

下载的 png /jpeg 是压缩的位图,在图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,也就是需要decode解压。一般系统会在主线程对图片进行解压缩。SD是在后台线程将图片提前进行解压缩,基本的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图,也就是将二进制数据转化成像素数据。

SDImageCoder 核心的类SDImageCodersManager SDWebImageDownloaderOperation 图片下载完毕后调用 if (imageData && self.decryptor) {

            imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}

支持自定义配置解码器,比如遇到一些特殊格式的图片,而SD库中默认不支持,可以调用前配置里增加符合自己要求的coder,内部有一个codes数组,调用解码时会逆序遍历数组去判断是否支持解码,最后再调用具体的decodedImageWithData:

四、框架设计

回到之前整体类关系图

设计模式

SD用到很多设计模式,列举一些常见的。

  • 单例

  • 代理模式

  • 装饰器模式 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

  • 外观模式

外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。为子系统中一组不同的接口提供统一的接口。 外观定义了上层接口,通过降低复杂度和隐藏子系统间的通信及依存关系,让子系统更易于使用。 特点

  • 每个子系统层级有一个外观作为入口
  • 让它们通过其外观进行通信,可以简化它们之间的依赖关系

SDWenImageManager

  • 策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

  • 适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

所有实现该协议的对象都可以cancel,哪怕是不同类型的对象 如SDWebImageDownloaderOperation、SDWebImageCombinedOperation 我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作

感想

粗略阅读SD这个库后,我认为了解基本的图片加载流程如何实现是一回事,更重要的体会这个库的设计思路,更多地从框架设计上去思考,带着如果是我们自己设计会怎么做去阅读,可能更能直接感受设计的巧妙。

整体来说,给我启发还是比较大,虽然不一定能马上付诸实践。组件化开发过程中,很多其实可以借鉴,比如SDWebImageCache单独抽出独立为一个缓存组件也可以的,只需要暴露SDWebImageCache协议,外部使用完全不用关心内部具体实现,不需要依赖内部的类。看着人家的方案,有一种觉得就应该这么设计的感觉,但自己做就不是这个效果,我后来想想,除了架构思维这和经验方面外,主要有一个问题,就是开发或者重构某个模块时,没有做好提前设计,可能只是简单罗列需要几个类做什么事。但感觉可以进一步去思考,明确每一个类的职责,看看哪些是可以复用,各个类之间的关系,怎么做让耦合更小等等。比如最近要开发的监控相关的库,各个子库都依赖读写的模块,是否可以设计一个读写相关的协议,到时候假设需要更换具体实现就可以比较方便地替换;还有云端配置,可能从接收到服务端下发后会分配到各个库,例如网络监控存在各个时间节点,和SD中的SDWebImageContextOption配置有异曲同工的感觉,当然云端配置更多是开关,不需要配置太复杂的事务。

###参考 https://github.com/SDWebImage/SDWebImage https://juejin.im/post/5e63d5a9f265da5729789ac3 https://github.com/SDWebImage/SDWebImage/wiki/5.0-Migration-guide