关于SDWebImage源码常见问题

原文:关于SDWebImage源码常见问题

SDWebImage.png

简析

前段时间,和一个小伙交流,那小伙问我:
小伙:“NSString声明属性时,用什么修饰?”
我:“copy”
小伙:“为什么用copy,用strong有什么问题么?”
我:“如果使用strong修饰,只是对字符串做了浅拷贝,当某个对象持有这个属性时,会改变这个属性值。”
小伙:“那我就想让它改变呢?”
我:“……(⊙o⊙)?”

出来混也有三年了,竟然又在最基础的上面栽了,好尴尬。其实说到底还是自己内功修为不够。蜻蜓点水,对于开发者而言是大忌,做过几款APP就觉得自己怎样怎样,真真是井底之蛙🐸。
做为iOS开发者,相信大家都会或多或少的使用或了解过SDWebImage,剖析其源码的文章不在少数,今天我从问题驱动的角度来简单梳理下我所理解的SDWebImage。

SDWebImages图片类型识别问题

大家都知道,UIImageView默认情况下只能加载png类型的图片,加载jpg/gif等类型时是要单独处理的,那么SDWebImage是怎么识别网络图片的类型呢?
阅读源码,大家会发现在NSData的分类文件NSData+ImageContentType.m中,它是根据文件头来识别,即图片流文件的第一个字节判断。

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
#import "NSData+ImageContentType.h"
@implementation NSData (ImageContentType)
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52:
// R as RIFF for WEBP
if (data.length < 12) {
return SDImageFormatUndefined;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
return SDImageFormatUndefined;
}
@end

OpenCV图片类型识别也类似,参见:include1224的博客:读文件头判断图片类型

SDWebImage的下载队列机制

SDWebImage加载网络图片的方式是异步加载的方式,不管是从性能方面还是从为用户节省流量的角度而言,SDWebImage做的都是比较好的。
那么,问题来了:
1、 异步加载多张图片时,SDWebImage是怎么处理的?是否有对应的并发队列?
2、如果有,它的并发队列运行机制是怎样的呢?既然是并发队列,最大的并发数是多少?
3、当多个图片下载任务结束时,在队列中移除的策略是怎样的,是先进先出?还是后进先出?
4、当某个图片的URL为错误链接,或者服务器异常,或者网络异常的情况下,SDWebImage有没有异常超时处理?如果有超时机制,时长是多少呢?
下面我将一一为大家找到答案:
SDWebImage网络图片下载是通过SDWebImageDownloader和SDWebImageDownloaderOperation类来完成的。

  • SDWebImageDownloaderOperation封装了单个图片下载操作,它有一个start方法用来开启一个下载任务,看源码可以看到,在start方法体中有一段:
    1
    2
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfig.timeoutIntervalForRequest = 15;

在内部明确写了单个任务的超时时间15秒。

  • SDWebImageDownloader是用来管理SDWebImageDownloaderOperation图片下载任务的(另外在SDWebImageDownloader中也可以配置任务超时时长),它持有多个公有属性:maxConcurrentDownloads(最大并发数)、downloadTimeout(任务超时时长)、executionOrder(队列执行方式)等,维护着一个私有并发下载队列downloadQueue和一个最新任务添加任务lastAddedOperation。
    看源码我们可以轻松了解到,SDWebImage的下载队列默认情况下是SDWebImageDownloaderFIFOExecutionOrder,是先进先出的,下载队列并发数为6。

    1
    2
    3
    downloadQueue.maxConcurrentOperationCount = 6;
    downloadTimeout: 15.0;
    executionOrder: SDWebImageDownloaderFIFOExecutionOrder;
  • 详见源码:

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
@interface SDWebImageDownloader ()
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
......
@end
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}

SDWebImage缓存机制

SDWebImage缓存机制其实由两部分组成:内存缓存、磁盘缓存。从SDImageCache文件中我们可以清楚地看出这一点,其中memCache即内存缓存,diskCachePath即磁盘缓存,数据文件存储在沙盒中:

1
2
3
4
5
@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
......
@end

内存缓存
先说下内存缓存memCache,为了完善内存缓存,SDWebImage实现了NSCache的一个子类AutoPurgeCache,扩充了NSCache,当内存警告时,它会接受UIApplicationDidReceiveMemoryWarningNotification通知,自动执行removeAllObjects操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
@end

如果大家细心的话会发现,SDWebImage做了内存缓存,当我们频繁的使用SDWebImage加载多张图片时,却为何基本不会出现内存暴涨的情况呢?其实这一切归功于自动释放池@autoreleasepool。
磁盘缓存
接下来咱们说下磁盘缓存,磁盘缓存文件是存储在沙盒中的,存储过程比较复杂。我先简单说下,SDWebImage加载图片的大致流程,相信从中,大家会对diskCache有所了解。
在使用SDWebImage时,往往是从UIImageView+WebCache文件开始的,我们使用SDWebImage第一步就是要引入UIImageView的分类WebCache,然后调用sd_setImageWithURL:方法,完成图片的异步加载。
图片加载的具体流程如下:

  • 调用sd_setImageWithURL方法时,它首先是通过URL作为key查询内存缓存,即SDImageCache的memCache属性,如果存在直接显示到View上。
  • 反之,将通过md5编码URL作为文件名,去沙盒(即SDImageCache的diskCachePath路径下)中查询有无此文件,如果存在,就把沙盒中的文件加载到内存缓存memCache中,然后通过SDWebImageDecoder解码后,直接显示到View上。
  • 如果沙盒中不存在,则先将占位图片placeholderImage加载到View上,紧接着去SDWebImageDownloader的downloadQueue队列中,查找是否有正在下载该图片的下载任务,如果存在继续该任务。
  • 如果下载队列不存在,创建图片下载任务SDWebImageDownloaderOperation,然后通过lastAddedOperation,根据对应的机制添加到下载并发队列downloadQueue中,下载完毕后,将操作在队列中移除,将图片添加到内存缓存中,直接显示到View,并将该文件压缩编码后存储到沙盒中,将通过md5编码URL作为文件名。

相信看到上面的流程后,大家对磁盘缓存机制有所了解,当然也带来了一些疑问,比如:
1、图片文件为什么使用md5编码的URL作为文件名?
2、磁盘缓存,既然称为缓存,就肯定有一定的时间期限,缓存的时长是多少?
3、文件过期之后,在什么时机清除过期图片文件的?
4、沙盒大小是有限度的,那么为SDWebImage预留的磁盘空间有没有大小限制?
5、如果我想清空所有的SDWebImage缓存怎么清除?如果我们需要清除特定的图片缓存又该怎么处理?
下文,我将为大家一一解答这一系列疑问:

SDWebImage缓存图片命名问题

SDWebImage是怎样维护缓存图片的?在SDImageCache文件中,我们不难发现,它是利用了MD5的压缩性特性、容易计算、强抗碰撞等特性,将图片的URL进行md5编码,作为文件名存储到沙盒中的。

MD5百度百科
MD5算法具有以下特点:
1、压缩性:任意长度的数据,算出的MD5值长度都是固定的。
2、容易计算:从原数据计算出MD5值很容易。
3、抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
4、强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (nullable NSString *)cachedFileNameForKey:(nullable 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;
}
SDWebImage缓存文件保留时长及缓存空间大小

既然是缓存,肯定有相应的时间期限,默认情况下SDWebImage的缓存时长为一周,并且缓存空间可以自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "SDImageCacheConfig.h"
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
@implementation SDImageCacheConfig
- (instancetype)init {
if (self = [super init]) {
_shouldDecompressImages = YES;
_shouldDisableiCloud = YES;
_shouldCacheImagesInMemory = YES;
_maxCacheAge = kDefaultCacheMaxCacheAge;
_maxCacheSize = 0;
}
return self;
}
@end

过滤URL,禁用缓存

如果想过滤特定URL,不使用缓存机制,可以在对应位置加入如下代码过滤。

1
2
3
4
5
6
7
8
9
10
11
SDWebImageManager.sharedManager.cacheKeyFilter = ^NSString * _Nullable(NSURL * _Nullable url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
NSLog(@"url.scheme:%@, url.host:%@, url.path: %@", url.scheme, url.host, url.path);
// if([[url.host absoluteString] isEqualToString:@"upload-images.jianshu.io"])
if ([[url absoluteString] isEqualToString:@"http://upload-images.jianshu.io/upload_images/949086-5d2c51f1e3a9cddd.png"])
{
return nil;
}
return [url absoluteString];
};
清除特定图片缓存

刚说过,SDWebImage加载图片是有缓存的,默认存储一周的时间。使用SDWebImage加载同样URL的图片时,优先会从缓存中取,而不是每次重新请求加载,那么问题来了,我们的头像/广告图等,需要实时刷新,我们要需要清除特定的图片缓存。
单单就头像/广告图更新问题而言,无非是更新缓存问题,有很多方法解决,

  • 使用options:SDWebImageRefreshCached刷新缓存,但是有些童鞋反应该方法有闪烁问题,甚至有时并没有更新图片,所以保险起见,最好还是手动清缓存的方式。
  • 每次清除掉图片缓存,重新加载的方式,代码如下:
1
2
3
4
5
6
7
8
9
NSURL *imageURL = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/949086-5d2c51f1e3a9cddd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/999"];
// 获取对应URL链接的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:imageURL];
NSString *pathStr = [[SDImageCache sharedImageCache] defaultCachePathForKey:key];
NSLog(@"key存储的路径: %@", pathStr);
// 删除对应key的文件
[[SDImageCache sharedImageCache] removeImageForKey:key withCompletion:^{
[self.tempImageView sd_setImageWithURL:imageURL placeholderImage:[UIImage imageNamed:@"placeholderHead.png"]];
}];
清除过期文件的时机

通过上文的解答,大家知道磁盘缓存的文件是有时间期限的,那么,SDWebImage在什么时机清除过期文件的呢?在SDImageCache文件我们同样可以得到答案:
清除过期旧文件的时间点有两处:程序切到后台、杀死APP时。

1
2
3
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles) name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles) name:UIApplicationDidEnterBackgroundNotification object:nil];

具体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
@end

本文已在版权印备案,如需转载请在版权印获取授权。
获取版权