美文网首页iOSMore Stronger程序员
关于SDWebImage源码常见问题

关于SDWebImage源码常见问题

作者: 仁伯 | 来源:发表于2017-05-17 20:52 被阅读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类来完成的。

    • 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:https://img.haomeiwen.com/i949086/5d2c51f1e3a9cddd.png"])
            {
                return nil;
            }
            return [url absoluteString];
        };
    
    清除特定图片缓存

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

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

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

    相关文章

      网友评论

      • 三分慢先森:沙盒的存储限制是多少呢
        仁伯:@三分慢先森 系统俺不确定,按理说没限制吧
        三分慢先森:@仁伯安 那系统对单个沙盒有最大缓存的限制吗?
        仁伯:SDImageCacheConfig有个maxCacheSize是控制磁盘缓存大小的,默认为0,即不做大小限制,可以自己改
      • eagleyz:我碰到一个问题,服务器更改图片,但是url不变的话,请求还是下载不下来。总是会读取本地缓存。他有一个选项是可以不读取缓存的,去下载的。设置了,也没用。
        仁伯:@ITzhen 清理是清理特定图片的缓存,根据URL,又不是清理所有
        eagleyz:@仁伯安 清理缓存不行的。这些我也研究过,清理缓存总不能每次清理吧,这样还是浪费流量。如果设置一天清理一次。但是头像频繁更换啊,一分钟更换一次。没效果的
        仁伯:@ITzhen 每次清缓存就可以,文中有代码

      本文标题:关于SDWebImage源码常见问题

      本文链接:https://www.haomeiwen.com/subject/voakxxtx.html