本文接 SDWebImage 源码(一) ,本篇阅读 SDImageCache 里面的源码。

它主要负责图片缓存的存取操作。

1
2
3
// SDImageCache.m
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

上文中大体说了下这个方法内部操作,现在我们详细看一下内部实现。

取出在内存中的图片

1
2
3
4
5
// SDImageCache.m
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}

0.memCacheAutoPurgeCache 实例,AutoPurgeCacheNSCache 子类(AutoPurgeCache 仅多了注册了 UIApplicationDidReceiveMemoryWarningNotification 应用内存警告通知)。根据 key (图片 URL 字符串)取出内存中存储的图片数据。

取出在磁盘中的图片

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
// SDImageCache.m
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; // 0
if (data) {
UIImage *image = [UIImage sd_imageWithData:data]; // 1
image = [self scaledImageForKey:key image:image]; // 2
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image]; // 3
}
return image;
}
else {
return nil;
}
}
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath]; // 4
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]]; // 4
if (data) {
return data;
}
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath]; // 4
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]]; // 4
if (imageData) {
return imageData;
}
}
return nil;
}

注意:上述方法都是在 异步串行 队列中解码图片

0.从 - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key; 方法中获取图片数据 见 4

1.将 NSData 转化为 UIImage。里面区分了几种图片类型(Gif、WebP、其它)进行相应处理,为解码的图片添加方向、动态显示等。

2.缩放图片。非动态图片,如果图片有后缀如 @2x.@3x.,按照相应的比例来处理。如果没有,按照当前设备屏幕比例 Scale 来处理。处理方法:

1
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];

3.解码图片。对于非动态图片,无 alpha layer 的添加上 alpha layer。(为什么这么做,还不太清楚)

4.根据图片 URL 字符串找到图片文件路径,从文件中获取图片数据。

存储图片

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
// SDImageCache.m
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
// 0
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{ // 1
NSData *data = imageData;
// 2
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// We need to determine if the image is a PNG or a JPEG
// PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
// The first eight bytes of a PNG file always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
// If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
// and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
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
}
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// disable iCloud backup
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil]; // 3
}
}
});
}
}

0.缓存到内存。cost 一般指对象字节数。如果不知道直接传 0,或者是直接使用 [self.memCache setObject:image forKey:key]。SDWebImage 内部是这么计算的:

1
2
3
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}

1.对磁盘的操作,依然是放在异步串行队列里面

2.这里面有一个区分图片是 PNG 还是非 PNG 的判断方法,注释里已经写的很清楚

1
2
// PNG signature bytes and data (below)
static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};

3.是为了防止 iCloud 自动备份。self.shouldDisableiCloud 默认为 YES

清理缓存通知

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
// SDImageCache.m
// Subscribe to app events
// 0
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
// 1
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
// 2
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
- (void)clearMemory {
[self.memCache removeAllObjects];
}

0.出现内存警告时,清除内存缓存,即 - (void)clearMemory

1.程序将要终止时,清除磁盘里过期的文件。过期时间默认为 7 天

1
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

2.程序进入后台时,也执行 1 中的操作。

图片名字处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SDImageCache.m
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
// 0
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;
}

0.key 为图片 URL 字符串。文件名为 URL 字符串的 MD5 值。

缓存图片文件尺寸和数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SDImageCache.m
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{ // 0
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}
- (NSUInteger)getDiskCount {
__block NSUInteger count = 0;
dispatch_sync(self.ioQueue, ^{ // 0
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
count = [[fileEnumerator allObjects] count];
});
return count;
}

0.获取尺寸使用的是 同步串行队列