针对于 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
  1. 每次请求都先根据url查找是否在失败请求集合中,假如url为空或者 配置不是SDWebImageRetryFailed,且之前url请求失败过,那么直接返回失败。
  2. 根据url查找内存、硬盘,有的话直接返回图片。
  3. 发起请求,并将产生的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];
}
}

可以看到,类中存储了downloadTokencacheOperation对象,还有个SDWebImageManager对象,当进行取消操作时,分别对缓存和网络请求进行取消操作,并且将该对象从SDWebImageManagerrunningOperations集合中移除。

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方法,通过外界传入的requestsession,生成一个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,得到的索引值是相同的,这时候要解决冲突:

  1. 拉链法 发生冲突时,索引位置指向了一个链表,节点是键值,该链表依次向后排列,这样查找时,根据链表依次查找 与 key 相同的 节点。
  2. 开放寻址法 存储的时候根据一定的规则去找值为空的索引,最简单的是线性探测法,直接找到下一个位置判断是否为空,一直找到一个为空的索引位置。查找的时候,也是按照这个规则,一直查找到与 key 相同的索引,取出值。

NSDictionary 的底层实现

NSDictionary 内部其实是用 NSMapTable 实现的,NSMapTable 结构体包括一系列的函数指针,比如 hash ,判断 key 是否相同,还包括一个链表指针,至于这个链表指针做什么用的,可能跟拉链法中处理冲突时相关。

NSMapTable

区别于 NSDictionary 的 key ,只能用 OC 对象(其实实现了 hash 和 isEqual 的其他对象也可以),必须支持NSCopying协议,并且会对 key 进行 copy 操作,对 value 进行 retain操作,而 NSMapTable 可以允许设置 key 和 value 的操作类型,copy 还是 strong,还是 weak。