iOSMore Stronger程序员

关于SDWebImage源码常见问题

2017-05-17  本文已影响1360人  仁伯
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类来完成的。

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;

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

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:方法,完成图片的异步加载。
图片加载的具体流程如下:

相信看到上面的流程后,大家对磁盘缓存机制有所了解,当然也带来了一些疑问,比如:
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:https://img.haomeiwen.com/i949086/5d2c51f1e3a9cddd.png"])
        {
            return nil;
        }
        return [url absoluteString];
    };
清除特定图片缓存

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

    NSURL *imageURL = [NSURL URLWithString:@"http:https://img.haomeiwen.com/i949086/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

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

上一篇下一篇

猜你喜欢

热点阅读