针对于 SDWebImage 4.0 版本的源码分析
View 层 负责 view 的 loading 控件管理,每个 view 的请求发起、取消入口。
UIView+WebCacheOperation 分类 负责管理每次请求产生的 SDWebImageCombinedOperation
,可以将这些operation
对象存储到自己关联的NSMapTable
中,对外提供根据key
添加、删除、取消某个operation
的接口。
UIView+WebCache 分类 内部调用 SDWebImageManager
类发起请求,请求前会调用UIView+WebCacheOperation
中的方法,取消上次的请求,并将这次请求方法产生的 SDWebImageCombinedOpreation
对象存储起来、并且管理 loading 的控件。
Manager 层 SDWebImageManager 负责整体请求的管理,包括缓存、请求的发起、取消。 内部有如下属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @interface SDWebImageManager () /// cache 负责图片在缓存中的查找、将图片缓存到内存和磁盘中,压缩,解压操作。 @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache; /// 负责图片的网络请求发起,取消等操作。 @property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader; /// 记录所有失败的网络请求。 @property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs; /// 负责多线程下的 failedUrls 的线程安全,信号量锁,值为 1 @property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe /// 保存每次产生的 SDWebImageCombinedOperation 对象,并管理 @property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations; /// 负责多线程下的 runningOperationsLock 线程安全,信号量锁,值为 1 @property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe @end
每次请求都先根据url
查找是否在失败请求集合中,假如url
为空或者 配置不是SDWebImageRetryFailed
,且之前url
请求失败过,那么直接返回失败。
根据url
查找内存、硬盘,有的话直接返回图片。
发起请求,并将产生的SDWebImageCombinedOpreation
存储到runningOperations
中。
SDWebImageDownloader 负责具体图片请求的发起。 内部属性如下:
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 @interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate> /// 是否解压下载下来的图片,默认是 YES,在子线程中解压,但是会消耗内存资源 @property (assign, nonatomic) BOOL shouldDecompressImages; /// 可以设置下载队列的最大并发量,默认为 6 @property (assign, nonatomic) NSInteger maxConcurrentDownloads; /// 当前下载队列中的任务数 @property (readonly, nonatomic) NSUInteger currentDownloadCount; /// 下载超时时间,默认是 15 秒 @property (assign, nonatomic) NSTimeInterval downloadTimeout; /// 内部 NSURLSession 初始化使用到的 configuration @property (readonly, nonatomic, nonnull) NSURLSessionConfiguration *sessionConfiguration; /// 下载任务的执行顺序,有先进先出,后进先出,默认先进先出,按添加顺序执行。 @property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; /// 下载队列,默认最大并发量为 6 @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue; /// 上次发起的 operation ,当 executionOrder 为 后进先出时,会用到,上次 operation 的 dependency 为当前 operation @property (weak, nonatomic, nullable) NSOperation *lastAddedOperation; /// 每次请求根据该属性生成一个 NSOperation 对象,默认为 [SDWebImageDownloaderOperation class]。 @property (assign, nonatomic, nullable) Class operationClass; /// 保存每次产生的 SDWebImageDownloaderOperation 对象,url 作为 key,也就是每个 url 只产生一个 SDWebImageDownloaderOperation 对象 @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations; /// 存储请求头,有默认值,每次发起 request 用该字典初始化请求头 @property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders; /// 信号量锁,值为 1,保证 URLOperations 多线程安全 @property (strong, nonatomic, nonnull) dispatch_semaphore_t operationsLock; // a lock to keep the access to `URLOperations` thread-safe /// 请求头锁,值为 1,保证 HTTPHeaders 多线程安全 @property (strong, nonatomic, nonnull) dispatch_semaphore_t headersLock; // a lock to keep the access to `HTTPHeaders` thread-safe /// 每次发起请求使用的 session ,delegate 为 self ,delegateQueue 为 nil,保证每个请求的回调会串行回调的。 @property (strong, nonatomic) NSURLSession *session; @end
每次请求,都检查 URLOperations
字典中是否有这个url
对应的operation
,有的话返回,没有的话生成一个新的SDWebImageDownloaderOperation
对象并加入到下载队列downloadQueue
中去发起请求,并且存储到URLOperations
中。 这样,针对同一url
的多次请求,只会生成一个operation
对象,去下载,每次请求都会生成一个SDWebImageDownloadToken
对象,保存有这次请求的回调block
。
SDImageCache 针对于图片的内存、硬盘缓存、并进行压缩、解压缩操作。 相关属性如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @interface SDImageCache () #pragma mark - Properties /// 针对于内存的 缓存管理,继承自 NSCache,但是内部有个 NSMapTable 进行存储,url 作为 key,image 作为 value,其中 value 配置属性 为 weak,对内存不足进行了监听,会清除所有内存缓存。 @property (strong, nonatomic, nonnull) SDMemoryCache *memCache; /// 硬盘存储路径,默认为 NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) 路径 @property (strong, nonatomic, nonnull) NSString *diskCachePath; /// 允许我们进行预缓存图片,并存储到这个路径数组中,之后加载图片时会查找这些路径。 @property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths; /// 图片读取队列,串行队列 @property (strong, nonatomic, nullable) dispatch_queue_t ioQueue; /// 文件管理 manager @property (strong, nonatomic, nonnull) NSFileManager *fileManager; @end
这个类支持对图片进行存储,查找,压缩,解压缩,其中压缩解压缩操作使用了CoreGraphics
库中的方法。
Operation 层 SDWebImageCombinedOpreation 该对象继承自 NSObject ,只要是对某一图片请求进行取消操作,包括缓存和网络请求,属性和取消方法如下:
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 @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation> /// 是否被取消 @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; /// 针对下请求管理 @property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken; /// 针对缓存管理 @property (strong, nonatomic, nullable) NSOperation *cacheOperation; /// 保存 downloadManager,引用其 downloader @property (weak, nonatomic, nullable) SDWebImageManager *manager; @end @implementation SDWebImageCombinedOperation - (void)cancel { @synchronized(self) { self.cancelled = YES; if (self.cacheOperation) { [self.cacheOperation cancel]; self.cacheOperation = nil; } if (self.downloadToken) { [self.manager.imageDownloader cancel:self.downloadToken]; } [self.manager safelyRemoveOperationFromRunning:self]; } }
可以看到,类中存储了downloadToken
和cacheOperation
对象,还有个SDWebImageManager
对象,当进行取消操作时,分别对缓存和网络请求进行取消操作,并且将该对象从SDWebImageManager
的runningOperations
集合中移除。
SDWebImageDownloaderOperation 继承自NSOperation
,主要负责某次网络请求的发起和取消等管理。内部属性如下:
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 @interface SDWebImageDownloaderOperation () /// 存储着对同一 url 发起的多次请求时,所有的回调 block,里面是多个字典,每个字典有 progressBlock 和 completionBlock @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks; /// 是否在执行 @property (assign, nonatomic, getter = isExecuting) BOOL executing; /// 是否已结束 @property (assign, nonatomic, getter = isFinished) BOOL finished; /// 存储网络返回的数据,每次网络回调时都拼接到该属性上 @property (strong, nonatomic, nullable) NSMutableData *imageData; /// NSURLCache 中缓存的数据 @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse` /// 发起请求的默认 session ,由 SDImageDownloader 持有 @property (weak, nonatomic, nullable) NSURLSession *unownedSession; /// 自定义 session 时,该 session 需要使用者手动创建 @property (strong, nonatomic, nullable) NSURLSession *ownedSession; /// 网络请求对应的 dataTask,每个 url 最多对应一个 dataTask 对象。 @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; /// 保证 callbackBlocks 多线程安全的信号量锁 @property (strong, nonatomic, nonnull) dispatch_semaphore_t callbacksLock; // a lock to keep the access to `callbackBlocks` thread-safe /// 压缩和解压缩时的串行队列 @property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding #if SD_UIKIT /// 后台下载的 identifier @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; #endif /// 解压工具 @property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder; @end
针对于每次发起的图片网络请求,每个 url 都对应生成一个SDWebImageDownloaderOperation
对象,这个类继承自NSOperation
类,重写了start
方法,通过外界传入的request
和session
,生成一个NSURLSessionTask
对象,进行网络请求,同时,每个对象都有一个callbackBlocks
数组,存储着对同一 url 请求的多个监听 block ,每次对同一 url 进行请求,都会将 progressBlock 和 completedBlock 存储到一个字典中,再将字典加入到 callbackBlocks 数组中,请求回调时,SDImageDownloader 会根据 dataTask 的标识,调用相应的 DownloaderOperation 的回调方法,方法中,依次遍历数组中所有的 block 执行。
SDWebImageDownloadToken 属性如下:
1 2 3 4 5 6 7 8 /// 发起请求时的 图片 url ,标识每次请求 @property (nonatomic, strong, nullable) NSURL *url; /// 字典类型,存储每次请求的回调 block @property (nonatomic, strong, nullable) id downloadOperationCancelToken; /// 网络请求 @property (nonatomic, weak, nullable) NSOperation<SDWebImageDownloaderOperationInterface> *downloadOperation;
每次发起请求时创建一个 downloadToken 对象,保存有 url、回调 block 和 downloadOperation ,但是其实 downloadOperation 是没有直接用到的,实际查找的时候是根据 url 查找到的,不知道为什么非要保持有一个 downloadOperation 的引用呢?这是我没搞清楚的地方。 为了更清楚的理解,我画了一张 SDWebImage 的 UML 类图,简单的指出了每个类的关系。
hash 表 hash 表组成由 一个数组 T,加上一个 hash 函数,这个 函数 将 key 映射成一个索引,表明值就存储到 T 的这个索引的位置上,一般来说,这个函数内部生成一个值 p,将 p 对 T 的长度 m 取余,得到索引值,从而保证了索引值在 0-m 之间。 最好的 hash 函数,保证得到的这个索引,尽可能的平均,但是也会发生冲突,比如两个不同的 key1,key2,得到的索引值是相同的,这时候要解决冲突:
拉链法 发生冲突时,索引位置指向了一个链表,节点是键值,该链表依次向后排列,这样查找时,根据链表依次查找 与 key 相同的 节点。
开放寻址法 存储的时候根据一定的规则去找值为空的索引,最简单的是线性探测法,直接找到下一个位置判断是否为空,一直找到一个为空的索引位置。查找的时候,也是按照这个规则,一直查找到与 key 相同的索引,取出值。
NSDictionary 的底层实现 NSDictionary 内部其实是用 NSMapTable 实现的,NSMapTable 结构体包括一系列的函数指针,比如 hash ,判断 key 是否相同,还包括一个链表指针,至于这个链表指针做什么用的,可能跟拉链法中处理冲突时相关。
NSMapTable 区别于 NSDictionary 的 key ,只能用 OC 对象(其实实现了 hash 和 isEqual 的其他对象也可以),必须支持NSCopying
协议,并且会对 key 进行 copy
操作,对 value 进行 retain
操作,而 NSMapTable 可以允许设置 key 和 value 的操作类型,copy 还是 strong,还是 weak。