SDWebImage源码阅读

来源:互联网 发布:盗版游戏 知乎 编辑:程序博客网 时间:2024/05/22 06:39

前言

SDWebImage强大的网络图片加载库,以前只是能够灵活使用,对底层的实现原理,也是知其然而不知其所以然,但是成为一名优秀的开发者来说,会用只是最简单的一步,更重要的是要研究其底层的技术实现和设计思路原理。这个月工作也不忙,所以阅读下类库源码。记录下自己的思考与总结。

SDWebImage 简介

  1. 提供UIImageView的一个分类,用来加载网络图片并且对网络图片的缓存进行管理
  2. 采用异步方式来下载网络图片
  3. 采用异步方式,使用memory+disk来缓存网络图片,自动管理缓存
  4. 支持GIF动画
  5. 支持WebP格式
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被重复下载
  8. 失效的URL不会被无限重试
  9. 下载及缓存时,主线程不被阻塞

下载

在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的,它是一个异步的下载器。并且对图片的加载做了优化处理。

下载选项

在下载的过程中,程序会根据设置的不同的下载选项,而执行不同操作。下载选项由枚举SDWebImageDownloaderOptions定义,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
// 默认的使用模式,前往下载,返回进度block信息,完成时调用completedBlock
SDWebImageDownloaderLowPriority = 1 << 0,
// 渐进式下载,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果从NSURLcache缓存中读取图片,则在调用完成block的时候,传递空的image或者imageData
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 在iOS 4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外时间来完成后台下载。如果后台任务终止,则操作将被取消
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允许不受信任的SSL证书。主要用于测试目的(生产环境慎用)
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 将图片下载放到高优先级队列中
SDWebImageDownloaderHighPriority = 1 << 7,
};

选项涉及了下载优先级,缓存策略,后台运行,cookie处理及认证

下载选项

SDWebImage定义了两种下载顺序,定义如下:

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 默认下载顺序 以队列的方式,按照先进先出的顺序下载
SDWebImageDownloaderFIFOExecutionOrder,
// 以栈的方式,按照后进先出的顺序下载
SDWebImageDownloaderLIFOExecutionOrder
};
下载管理器

SDWebImageDownloader下载管理器是一个单例类,主要管理图片的下载操作。图片的下载是放在NSOperationQueue操作队列中来完成的,队列的默认最大并发数为6.设置超时时间为15s.

所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,其声明及定义如下:

1
2
3
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
下载回调
1
2
3
4
5
6
// 下载进度(返回已经接收的图片数据的大小,未接收的图片数据的大小)
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
// Header 过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

图片下载的回调信息存储在URLCallbacks属性里面。该属性是一个字典。key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。value(数组里面)只包含一个元素,这个元素的类型是NSMutableDictionary类型,这个字典的key为NSString类型代表着回调类型,value为block,是对应的回调。目的都是为了给url绑定回调。由于允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证线程安全。将下载操作作为一个个任务放到barrierQueue队列中。并设置栅栏来确保同一时间只有一个线程操作URLCallbacks属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// 以dispatch_barrier_sync操作来保证同一时间只有一个线程能对URLCallbacks进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
// 处理同一URL的同步下载请求的单个下载
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
// 如果url第一次绑定它的回调,也就是第一次使用这个url创建下载任务则执行一次创建回调
if (first) {
createCallback();
}
});
}

在barrierQueue队列中创建下载任务。至此下载的任务都创建好了,下面到下载的操作了

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
[self addProgressCallback:progressBlock
completedBlock:completedBlock
forURL:url
createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 1. 创建请求对象,并根据options参数设置其属性
// 为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作
NSMutableURLRequest *request =
[[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData)
timeoutInterval:timeoutInterval];
// 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//返回在接到上一个请求得得响应之前,需要传输数据,YES传输,NO不传输
request.HTTPShouldUsePipelining = YES;
}];
};
/**
如果自定义了wself.headersFilter,那就用自己设置的
wself.headersFilter来设置HTTP的header field
它的定义是typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
一个返回结果为NSDictionary类型的block
如果没有自己设置wself.headersFilter那么就用SDWebImage提供的HTTPHeaders
HTTPHeaders在#import "SDWebImageDownloader.h",init方法里面初始化,下载webp图片需要的header不一样
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
*/
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
/**
创建SDWebImageDownLoaderOperation操作对象(下载的操作就是在SDWebImageDownLoaderOperation类里面进行的)
传入了进度回调,完成回调,取消回调
*/
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
// 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用,
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
// 如果这个任务已经完成,就根据url这个key从URLCallbacks字典里面删除
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 取消操作将该url对应的回调信息从URLCallbacks中删除
SDWebImageDownloader *sself = wself;
if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url];
});
}];
// 设置是否需要解压
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 将操作加入到操作队列downloadQueue中.如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
return operation;
}
下载操作

每个图片的下载都是一个Operation操作。SDWebImage自定义了一个SDWebImageDownloaderOperation类,继承自NSOperation。并遵守SDWebImageOperation协议。对于图片的下载。使用的是NSURLConnection(未使用7.0以后的NSURLSession)。在SDWebImageDownloaderOperation中重写了start方法,方便自己管理下载的状态。此方法是执行下载任务的核心代码。

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
- (void)start {
@synchronized (self) {
// 管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 如果设置了在后台执行,则进行后台执行
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
...
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
// 在主线程中发送开始下载的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
/**
在[self.connection start];有返回结果之前,代码会一直阻塞在CFRunLoopRun这里,也就是说下载就一直在进行中,一直到下载完成或者错误(这两种情况都会调用CFRunLoopStop),阻塞才会解除
*/
// 开启当前线程的runloop
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
// 如果未完成,则取消连接
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
//如果connection创建失败,直接执行完成回调,并传递一个connection没有初始化的错误
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
// 下载操作已经完成了,停止在后台的执行,使用endBackgroundTask
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}

主要看一下connection: didReceiveData拼接数据的协议

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
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 拼接数据
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// 获取已接收的数据的长度
const NSInteger totalSize = self.imageData.length;
// 每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理,而且这个数据应该是已接收的全部数据,而不仅仅是新的字节
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
/**
在首次接收到数据的时候,图片的长宽都是0(width+height == 0)
先从这些包含图像信息的数据中取出图像的长,宽,方向等信息以备使用
*/
if (width + height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
...
CFRelease(properties);
/**
使用Core Craphics框架绘制image时,使用的是
initWithCGImage这个函数,但是使用这个函数有时候会造成图片朝向的错误,所以在这里保存朝向信息,orientation是一个可以记录图片方向的枚举
*/
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
// 图片还未接收完全
if (width + height > 0 && totalSize < self.expectedSize) {
// 使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 对下载下来的图片做个颜色空间转换等处理
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
// 对图片进行缩放、解码操作
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
image = [UIImage decodedImageWithImage:scaledImage];
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}

下载的核心是利用NSURLConnection对象来加载数据。每个图片的下载都由一个Operation操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。之前在做好唱项目时,伴奏的下载也是采用这用设计思路。

缓存

SDWebImage提供了对图片缓存的支持,而该功能是由SDImageCache类来完成的。该类负责处理内存缓存及磁盘缓存。其中磁盘缓存的写操作是异步的。

内存缓存

NSCache是一个类似于集合的容器,即缓存。它存储key-value,这一点非常类似 NSDictionary。 一般用 NSCache来缓存临时存储短时间但是使用创建成本高的对象,重用这些对象可以优化性能,因为他们的值不需要被重新计算。另外一方面,这些对象对于程序来说是不要紧的,在内存紧张的时候会被丢弃,如果对象被丢弃了,则下次使用的时候需要重新计算

磁盘缓存

磁盘缓存则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。此外SDImageCache还定义了一个串行队列,来异步存储图片

图片存储

在iOS中,会先检测图片是PNG还是JPEG,并将其转换为相应的图片数据,最后将数据写入到磁盘中。判断是否是png格式的文件,除了看是不是.png后缀格式命名外,还能分析文件开头的部分数据,这部分数据就是文件签名,每个标准的PNG文件开头都有固定格式的数字签名。详细参考文件名的命名规则则是按照缓存的key做md5处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}

当查询图片时,该操作会在内存中放置一份缓存,如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task放到串行队列中处理。

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
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
if (self.shouldCacheImagesInMemory) {
// //查询图片大小,并存入内存(NSCache)中
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 加载到磁盘
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
// 是否要重新处理图片数据
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 确定图片是png还是jpeg. imageData为nil的话当做是png处理
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
// 创建缓存文件并存储图片
[self storeImageDataToDisk:data forKey:key];
});
}
}
图片查询

使用key作为参数,查询内存和磁盘中是否有对应的图片

1
2
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
图片移除

使用如下操作可以移除内存或者磁盘上的图片。

1
2
3
4
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

图片清理

两种清理方式:完全清空部分清空

完全清空是直接把文件夹移除掉

1
2
- (void)clearMemory
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion

部分清空是根据参数配置、移除文件。使文件的总大小小于最大使用空间。清理策略有两个文件缓存的有限期时间及最大缓存空间大小。
文件的缓存有效期:默认是一周。如果文件的缓存时间超过这个时间值,则将其移除
最大缓存空间大小:如果所有缓存文件的总大小超过最大缓存空间,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间

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
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 通过文件的枚举器来获取缓存文件的有用的属性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚举缓存文件夹中所有文件,移除过期的文件,存储文件属性便后面清理
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 忽略文件夹
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除过期文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 存储文件的引用并计算所有文件的总大小,以备后用
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,首先删除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以设置的最大缓存大小的一半作为清理目标
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 剩下的缓存文件按照最后修改时间来排序
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 删除文件,直到缓存总大小降到我们期望的大小
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}

对于缓存操作总结:

  1. 缓存分为内存缓存和磁盘缓存(以NSCache和文件的形式)
  2. 读取先从内存查找、如果没有再从磁盘读取并放入内存
  3. 提供完全移除和部分移除功能,部分移除根据配置、达到删除文件后的容量小于用户设定的最大值

SDWebImageManager

1
2
3
4
@interface SDWebImageManager : NSObject
@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;

在开发应用中,SDWebImageManager 管理图片的下载缓存,所以提供了SDImageCacheSDWebImageDownloader两个类的属性,我们经常用到的诸如UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。SDWebImageManagerDelegate还声明了两个可选实现的方法。

1
2
3
4
5
// 缓存图片没有找到的时候,去哪里下载
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

在查看源码时,发现了有趣的宏定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

保证了在主线程执行。

补充点

之前一直不明白,为什么将图片从磁盘读取出来后需要做Decode,后来参考
才明白。由于UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。这是典型的空间换时间的做法。

原创粉丝点击