美文网首页
《重读SDWebImage》-Cache部分

《重读SDWebImage》-Cache部分

作者: 我是繁星 | 来源:发表于2019-03-08 16:26 被阅读0次

    最近终于有些时间重读SD的源码了,本篇文章侧重分析SDWebImage缓存部分逻辑,以及其中的一些细节。

    一.SDImageCache提供的功能

    SDImageCache功能

    SDWebImage为整个图片加载逻辑提供缓存支持,包括内存缓存(NSCache实现)和磁盘缓存,且支持同步和异步操作。提供单例对象可进行全局操作。

    SDImageCache提供了两个枚举:

    三种缓存选项SDImageCacheType

    • SDImageCacheTypeNone 不缓存
    • SDImageCacheTypeDisk 磁盘缓存
    • SDImageCacheTypeMemory 内存缓存

    三种查询操作的选项SDImageCacheOptions,这是一个按位枚举可以多选:

    • SDImageCacheQueryDataWhenInMemory 在查询缓存数据时会强制查询磁盘缓存数据
    • SDImageCacheQueryDiskSync 查询磁盘缓存的时候会强制同步查询
    • SDImageCacheScaleDownLargeImages 会根据屏幕比例对图片进行尺寸调整

    至于这两个枚举怎么用看后面的细节里会讲到

    二、细节

    2.1 .命名空间

    每个ImageCache对象在创建的时候都必须提供命名空间,目的是区分磁盘缓存的路径,使每一个SDImageCache对象都有独立的存储空间不至于搞混,默认的命名空间为default。磁盘缓存都会在.../Library/Caches/namespace/com.hackemist.SDWebImageCache.namespace文件夹下。

    磁盘缓存路径
    下面看一下几个重要的方法:
    - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                           diskCacheDirectory:(nonnull NSString *)directory {
        if ((self = [super init])) {
            //命名空间磁盘缓存都会存储在/Users/xingfan/Library/Developer/CoreSimulator/Devices/68BF3925-CD38-4A3C-AFAB-C2660D4D40AF/data/Containers/Data/Application/0889706E-09A2-4DEF-930A-18DE125E859D/Library/Caches/命名空间/com.hackemist.SDWebImageCache.命名空间/这个文件夹下
            NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
            
            // 创建穿行串行队列执行任务
            _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
            
            _config = [[SDImageCacheConfig alloc] init];
            
            // 创建SDMemoryCache继承自NSCache,提供内存缓存功能
            _memCache = [[SDMemoryCache alloc] initWithConfig:_config];
            _memCache.name = fullNamespace;
    
            // 添加disk地址如下:/Users/xingfan/Library/Developer/CoreSimulator/Devices/68BF3925-CD38-4A3C-AFAB-C2660D4D40AF/data/Containers/Data/Application/0889706E-09A2-4DEF-930A-18DE125E859D/Library/Caches/default/com.hackemist.SDWebImageCache.default
            if (directory != nil) {
                _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
            } else {
                NSString *path = [self makeDiskCachePath:ns];
                _diskCachePath = path;
            }
            //最终磁盘缓存的文件夹路径被存储在_diskCachePath这个成员变量里面
            dispatch_sync(_ioQueue, ^{
                self.fileManager = [NSFileManager new];
            });
        }
        return self;
    }
    
    2.2. Cache path

    Cache Path有如下几个功能

    • 添加自定义存储路径
    • 查询命名空间对应的磁盘缓存路径
    • 提供文件名生成方法,并不会用明文key存储,(key.utf8.md5+扩展名)的方式存储在本地

    看下几个重要的方法。

    //添加自定义文件路径,在查找缓存的时候会同时在self.customPaths里面查找
    - (void)addReadOnlyCachePath:(nonnull NSString *)path {
        if (!self.customPaths) {
            self.customPaths = [NSMutableArray new];
        }
    
        if (![self.customPaths containsObject:path]) {
            [self.customPaths addObject:path];
        }
    }
    
    //文件名生成规则,对图片url的utf8String进行md5,链接最后如果有扩展名会拼接上扩展名
    - (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);
        NSURL *keyURL = [NSURL URLWithString:key];
        NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
        // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
        if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
            ext = nil;
        }
        //%02x 格式控制: x意思是以十六进制输出,2为指定的输出字段的宽度.如果位数小于2,则左端补0
        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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
        return filename;
    }
    
    2.3. 存储操作

    根据给出的key存储对应的image数据,主要有两个方法。如下。

    //异步缓存图片
    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        if (!image || !key) {
            if (completionBlock) {
                completionBlock();
            }
            return;
        }
        // 判断配置中是否需要进行内存缓存,如果需要存入memCache
        if (self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = image.sd_memoryCost;
            [self.memCache setObject:image forKey:key cost:cost];
        }
        
        if (toDisk) {
        //异步执行磁盘缓存,因为磁盘缓存耗时
            dispatch_async(self.ioQueue, ^{
        //用自动释放池可以提前释放局部变量,减少内存峰值
                @autoreleasepool {
                    NSData *data = imageData;
                    //如果data为空这里生成imageData
                    if (!data && image) {
                        // 根据是否存在alpha通道判断图片类型
                        SDImageFormat format;
                        //判断image格式
                        if (SDCGImageRefContainsAlpha(image.CGImage)) {
                            format = SDImageFormatPNG;
                        } else {
                            format = SDImageFormatJPEG;
                        }
                        //根据图片类型将图片转成nsdata,过程之后的文章里会讲,这里侧重于缓存逻辑
                        data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                    }
                    //执行磁盘缓存
                    [self _storeImageDataToDisk:data forKey:key];
                }
                
                if (completionBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    
    // 这里确保是从io队列中调用,将imageData存储到磁盘缓存中
    - (void) _storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
        if (!imageData || !key) {
            return;
        }
        //判断目标文件夹是否存在,如果不存在创建
        if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
            [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // 获取缓存文件的文件名并拼接到diskCachePath后面,md5+后缀,.../Library/Caches/命名空间/com.hackemist.SDWebImageCache.命名空间/urlmd5+后缀名
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        // transform to NSUrl
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        //将二进制文件写入目标文件夹
        [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
        
        // iclould操作
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    
    2.4.查询和检索

    对于查询和检索SDImageCache提供了如下功能:

    • 查询当前key在磁盘中是否有对应的缓存数据(提供了同步和异步方法)
    • 查询当前key在内存中是否有对应的缓存数据(同步)
    • 检索出当前key在磁盘中存储的数据,同时提供block返回UIImage和NSData。方法默认情况下会先在内存缓存中查找,再到磁盘缓存查找,如果在磁盘缓存中查找到,会加入到内存缓存中(异步操作)
      看下关键方法:
    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
        if (!key) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return nil;
        }
        
        // 先检测NSCache里面是否有缓存数据
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        //这里就是开头讲到的查询策略,如果SDImageCacheQueryDataWhenInMemory则强制查询磁盘缓存Data数据。跳过此步骤,如果没有这个选项直接返回,但是不会返回Data数据。
        BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
        if (shouldQueryMemoryOnly) {
            if (doneBlock) {
                doneBlock(image, nil, SDImageCacheTypeMemory);
            }
            return nil;
        }
        //这个operation内并不包含存储操作,只是在异步执行磁盘缓存的时候,在外部可以对operation 进行cancel操作,可以中断磁盘缓存的逻辑。
        NSOperation *operation = [NSOperation new];
        void(^queryDiskBlock)(void) =  ^{
            if (operation.isCancelled) {
                // do not call the completion if cancelled
                return;
            }
            //这里跟前面一样,用自动释放池来减少内存峰值。
            @autoreleasepool {
            //此方法会搜索所有路径下的磁盘缓存数据,包括customPath和namespace下。
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage;
                SDImageCacheType cacheType = SDImageCacheTypeNone;
                //判断刚刚内存缓存中是否有image数据。
                if (image) {
                    // 如果有的话,返回内存中获取的image和磁盘中获取的data并且cache类型是SDImageCacheTypeMemory
                    diskImage = image;
                    cacheType = SDImageCacheTypeMemory;
                } else if (diskData) {
                    cacheType = SDImageCacheTypeDisk;
                  //如果没有的话,那么由data转成UIImage,返回这时候返回的数据都是来自于磁盘缓存。
                    diskImage = [self diskImageForKey:key data:diskData options:options];
                    if (diskImage && self.config.shouldCacheImagesInMemory) {
                        NSUInteger cost = diskImage.sd_memoryCost;
                        [self.memCache setObject:diskImage forKey:key cost:cost];
                    }
                }
                if (doneBlock) {
                    if (options & SDImageCacheQueryDiskSync) {
                        doneBlock(diskImage, diskData, cacheType);
                    } else {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            doneBlock(diskImage, diskData, cacheType);
                        });
                    }
                }
            }
        };
        //这里如果选项包括强制同步执行磁盘缓存操作,那么同步执行,否则异步执行
        if (options & SDImageCacheQueryDiskSync) {
            queryDiskBlock();
        } else {
            dispatch_async(self.ioQueue, queryDiskBlock);
        }
        
        return operation;
    }
    
    //该方法作用就是将磁盘取出来的数据转成UIImage
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
        if (data) {
            //由data获取到image,
            UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
            //根据key的名字设置图片的scale。因为这里不会像imageName方法自动添加scale
            image = [self scaledImageForKey:key image:image];
            //看存取策略如果需要返回解压后的image,那么解压image
            if (self.config.shouldDecompressImages) {
                BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
            }
            //返回解压后的image。
            return image;
        } else {
            return nil;
        }
    }
    
    2.4.删除操作

    删除操作相对简单的多,源码就不写了,没啥好说的😂

    • 从内存中删除对应数据
    • 从磁盘中删除对应数据
    2.5.内存清理操作

    这部分是保证内存性能,和磁盘缓存大小的关键

    • NSCache本身就会在内存占用较高的时候自动清理内存,所以这部分不用过多关心,SD也只是在收到内存警告的时候将NSCache清空。
    • 而保证磁盘空间用的是LRU(Least recently used,最近最少使用)缓存策略,有两个选项以最近访问时间为基准,以最近修改时间为基准。
      下面我们看一下LRU缓存淘汰机制是如何实现的。
    //清理旧磁盘文件,这段代码稍微多一点
    - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
          //获取磁盘缓存的文件夹
            NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    
            // 设置时间基准
            NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
            switch (self.config.diskCacheExpireType) {
                    //最近访问时间
                case SDImageCacheConfigExpireTypeAccessDate:
                    cacheContentDateKey = NSURLContentAccessDateKey;
                    break;
                    //最近修改时间
                case SDImageCacheConfigExpireTypeModificationDate:
                    cacheContentDateKey = NSURLContentModificationDateKey;
                    break;
    
                default:
                    break;
            }
            
            NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
    
            // 获取缓存文件夹下所有的文件
            NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
            //设置过期时间,默认是一周
            NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
            NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
            //存储着未过期文件的总大小
            NSUInteger currentCacheSize = 0;
    
            // 枚举缓存目录中的所有文件,有两个目的
            //
            //  1. 筛选出过期文件,添加进urlsToDelete数组中
            //  2. 存储并计算出未过期文件的总大小
            NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
            for (NSURL *fileURL in fileEnumerator) {
                NSError *error;
                NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
    
                // 跳过目录、和错误
                if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // 将过期文件添加进urlsToDelete中
                NSDate *modifiedDate = resourceValues[cacheContentDateKey];
                if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                    [urlsToDelete addObject:fileURL];
                    continue;
                }
                
                // 存储未删除的文件信息,并计算总大小
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
                cacheFiles[fileURL] = resourceValues;
            }
            //删除过期文件
            for (NSURL *fileURL in urlsToDelete) {
                [self.fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // 如果剩余的磁盘文件大小超过了设置的最大值,那么执行LRU淘汰策略,删除最老的文件
            if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
                // 清理的目标为最大缓存值得一般
                const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
    
                // 讲目录下的文件按照时间书序排序,老的排在前面
                NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                         usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                             return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                         }];
    
                // 遍历删除,直到当前大小小于目标大小。
                for (NSURL *fileURL in sortedFiles) {
                    if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                        NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                        currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
    
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    2.6. Cache信息

    提供接口返回当前磁盘缓存的信息

    • 当前磁盘缓存的总大小(同步)
    • 当前磁盘缓存的总数量(同步)
    • 计算磁盘缓存的总大小和数量(异步)

    这个也不上代码了,没什么特殊之处。

    三、Q&A

    Q:SDImageCache中有很多异步操作也没见里面用线程锁,那么是如何保证线程安全的呢?
    A:对于内存缓存NSCache本身就是内存安全的,磁盘缓存使用一个全局的串行队列保证的,串行队列的性质决定了无论同步执行还是异步执行,都会等之前的任务执行完才会执行下一个任务。

    Q:SDImageCache的好处有哪些?
    A:采用二级缓存机制(先从内存中去找,如果没有再到磁盘里去找,如果在没有再去下载,下载过后再存储到内存和磁盘当中),避免了图片的多次下载。有LRU缓存淘汰机制。

    本人能力有限,有问题忘大神们及时指出。

    相关文章

      网友评论

          本文标题:《重读SDWebImage》-Cache部分

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