关于SDWebImage源码常见问题

SDWebImage.png

简析

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

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

SDWebImages图片类型识别问题

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

#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方法体中有一段:
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;

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

  • SDWebImageDownloader是用来管理SDWebImageDownloaderOperation图片下载任务的(另外在SDWebImageDownloader中也可以配置任务超时时长),它持有多个公有属性:maxConcurrentDownloads(最大并发数)、downloadTimeout(任务超时时长)、executionOrder(队列执行方式)等,维护着一个私有并发下载队列downloadQueue和一个最新任务添加任务lastAddedOperation。
    看源码我们可以轻松了解到,SDWebImage的下载队列默认情况下是SDWebImageDownloaderFIFOExecutionOrder,是先进先出的,下载队列并发数为6。
downloadQueue.maxConcurrentOperationCount = 6;
downloadTimeout: 15.0;
executionOrder: SDWebImageDownloaderFIFOExecutionOrder;
  • 详见源码:
@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即磁盘缓存,数据文件存储在沙盒中:

@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
......
@end

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

@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值的数据(即伪造数据)是非常困难的。

- (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的缓存时长为一周,并且缓存空间可以自定义。

#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,不使用缓存机制,可以在对应位置加入如下代码过滤。

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刷新缓存,但是有些童鞋反应该方法有闪烁问题,甚至有时并没有更新图片,所以保险起见,最好还是手动清缓存的方式。
  • 每次清除掉图片缓存,重新加载的方式,代码如下:
    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时。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles) name:UIApplicationWillTerminateNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles) name:UIApplicationDidEnterBackgroundNotification object:nil];

具体源码如下:

@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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容