iOS-SDWebImage缓存机制

作者: 路飞_Luck | 来源:发表于2019-02-23 18:27 被阅读14次
    序言

    本文章中讲解的SDWebImage版本为4.4.4

    一 存储

    图片的存储主要是由类SDImageCache实现的,主要方法如下

    - (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;
        }
        // if memory cache is enabled
        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;
                    // 如果image存在,但是需要重新计算(recalculate)或者data为空
                    // 那就要根据image重新生成新的data
                    // 不过要是连image也为空的话,那就别存了
                    if (!data && image) {
                        // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                        SDImageFormat format;
                        if (SDCGImageRefContainsAlpha(image.CGImage)) {
                            format = SDImageFormatPNG;
                        } else {
                            format = SDImageFormatJPEG;
                        }
                        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();
            }
        }
    }
    
    • 存储图片
    // Make sure to call form io queue by caller
    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
        if (!imageData || !key) {
            return;
        }
        
        // 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
        // disk cache的文件路径是存储在_diskCachePath中的
        if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
            [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // get cache Path for image key
        // 根据image的key(一般情况下理解为image的url)组合成最终的文件路径
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        // transform to NSUrl
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        
        // 根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
        [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
        
        // disable iCloud backup
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    
    二 取图片
    1. 内存缓存使用SDImageCacheimageFromMemoryCacheForKey:取数据
    - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
        return [self.memCache objectForKey:key];
    }
    
    1. 从磁盘中读取数据
    - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = diskImage.sd_memoryCost;
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }
    
        return diskImage;
    }
    
    • 读取磁盘
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
        NSData *data = [self diskImageDataForKey:key];
        return [self diskImageForKey:key data:data];
    }
    
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
        return [self diskImageForKey:key data:data options:0];
    }
    
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
        if (data) {
            UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
            image = [self scaledImageForKey:key image:image];
            if (self.config.shouldDecompressImages) {
                BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
            }
            return image;
        } else {
            return nil;
        }
    }
    
    三 删除图片

    删除图片主要用到下面四个方法

    • removeImageForKeyfromDisk:withCompletion: 异步地将image从缓存(内存缓存以及可选的磁盘缓存)中移除
    • clearMemory 清楚内存缓存上的所有image
    • clearDisk 清除磁盘缓存上的所有image
    • cleanDisk 清除磁盘缓存上过期的image

    主要由类SDImageCache实现

    - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            // 记录遍历的文件目录
            NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    
            // Compute content date key to be used for tests
            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];
    
            // This enumerator prefetches useful properties for our cache files.
            // 递归地遍历diskCachePath这个文件夹中的所有目录,此处不是直接使用diskCachePath,而是使用其生成的NSURL
            // 此处使用includingPropertiesForKeys:resourceKeys,这样每个file的resourceKeys对应的属性也会在遍历时预先获取到
            // NSDirectoryEnumerationSkipsHiddenFiles表示不遍历隐藏文件
            NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
    
            // 获取文件的过期时间,SDWebImage中默认是一个星期
            // 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这样。
            // 其实是这样的,比如在2015/12/12/00:00:00最后一次修改文件,对应的过期时间应该是
            // 2015/12/19/00:00:00,不过现在时间是2015/12/27/00:00:00,我先将当前时间减去1个星期,得到
            // 2015/12/20/00:00:00,这个时间才是我们函数中的expirationDate。
            // 用这个expirationDate和最后一次修改时间modificationDate比较看谁更晚就行。
            NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
            // 用来存储对应文件的一些属性,比如文件所需磁盘空间
            NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
            // 记录当前已经使用的磁盘缓存大小
            NSUInteger currentCacheSize = 0;
    
            // Enumerate all of the files in the cache directory.  This loop has two purposes:
            //
            //  1. Removing files that are older than the expiration date.
            //  2. Storing file attributes for the size-based cleanup pass.
            NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
            for (NSURL *fileURL in fileEnumerator) {
                NSError *error;
                NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
    
                // Skip directories and errors. - 当前扫描的是目录,就跳过
                if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // Remove files that are older than the expiration date;  
                // 移除过期文件 - 这里判断过期的方式:对比文件的最后一次修改日期和expirationDate谁更晚,如果expirationDate更晚,就认为该文件已经过期,具体解释见上面
                NSDate *modifiedDate = resourceValues[cacheContentDateKey];
                if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                    [urlsToDelete addObject:fileURL];
                    continue;
                }
                
                // Store a reference to this file and account for its total size.
                // 计算当前已经使用的cache大小, - 并将对应file的属性存到cacheFiles中
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
                cacheFiles[fileURL] = resourceValues;
            }
            
            // 根据需要移除文件的url来移除对应file
            for (NSURL *fileURL in urlsToDelete) {
                [self.fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // If our remaining disk cache exceeds a configured maximum size, perform a second
            // size-based cleanup pass.  We delete the oldest files first.
            // 如果我们当前cache的大小已经超过了允许配置的缓存大小,那就删除已经缓存的文件。
            // 删除策略就是,首先删除修改时间更早的缓存文件
            if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
                // Target half of our maximum cache size for this cleanup pass.
                // 直接将当前cache大小降到允许最大的cache大小的一半
                const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
    
                // Sort the remaining cache files by their last modification time or last access time (oldest first).
                // 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
                NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                         usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                             return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                         }];
    
                // Delete files until we fall below our desired cache size.
                // 每次删除file后,就计算此时的cache的大小
                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;
      
                        // 如果此时的cache大小已经降到期望的大小了,就停止删除文件了
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    四 图片存储路径
    • 磁盘存储图片路径
    - (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
        return [self cachePathForKey:key inPath:self.diskCachePath];
    }
    
    // 将存储的文件路径和文件名绑定在一起,作为最终的存储路径
    - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
        NSString *filename = [self cachedFileNameForKey:key];
        return [path stringByAppendingPathComponent:filename];
    }
    
    - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
        const char *str = key.UTF8String;
        if (str == NULL) {
            str = "";
        }
        // 使用了MD5进行加密处理
        // 开辟一个16字节(128位:md5加密出来就是128bit)的空间
        unsigned char r[CC_MD5_DIGEST_LENGTH];
        // 把str字符串转换成了32位的16进制数列(这个过程不可逆转) 存储到了r这个空间中
        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;
        }
        // 最终生成的文件名就是 "md5码"+".文件类型"
        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;
    }
    
    • 解释

    1.key为图片下载路径,例如 https://imgs3.taobao/banners/201812/82f6bcdeb55191b75f3371056f093388.jpg

    2.self.diskCachePath为存储路径,例如模拟器路径

    /Users/cs/Library/Developer/CoreSimulator/Devices/053EEA1A-F097-409F-A232-51E51A239304/data/Containers/Data/Application/69AA9017-C1CE-4FA4-8A81-253E6AB05CF9/Library/Caches/default/com.hackemist.SDWebImageCache.default
    

    本文部分参考# SDWebImage缓存机制,非常感谢该作者。

    相关文章

      网友评论

        本文标题:iOS-SDWebImage缓存机制

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