美文网首页iOS分享的demoiOS/Swift第三方库使用iOS_Skill_Collect
【源码解读】SDWebImage ─── 缓存的设计

【源码解读】SDWebImage ─── 缓存的设计

作者: WellsCai | 来源:发表于2017-12-14 16:32 被阅读46次

    一. 缓存的介绍

    缓存在SDWebImage中是一个不可缺少的部分,它提供了这样一个功能:你传入一个url的key,我(缓存类)从内存中去寻找对应的图片,如果找到则返回给你,如果找不到我到沙盒中去寻找,如果找到再返回给你,并存储在内存中,如果找不到就告诉你找不到(当然SDWebImage这边会由下载类去下载对应图片再来缓存)。

    SDWebImage的缓存功能封装在SDImageCache中,内部包含了内存缓存和磁盘缓存,我们只需要通过url的key去查询就可以了,使用十分简单。通过对SDImageCache的解读,你可以学到如何设计一个缓存类(无论是内存缓存和磁盘缓存),来提高app的性能和用户体验(减少重复的网络请求,减少用户的等待事件)。

    二. 缓存的设计

    缓存的接口和组成

    SDImageCache主要由两部分组成:内存缓存和磁盘缓存。内存缓存由NSCache的子类AutoPurgeCache管理。磁盘缓存是储存在沙盒中,文件夹和缓存文件的创建和删除是由NSFileManager类型的成员变量_fileManager来管理。

    @interface SDImageCache ()
    
    @property (strong, nonatomic) NSCache *memCache;//内存缓存(实际是AutoPurgeCache类型)
    @property (strong, nonatomic) NSString *diskCachePath;//磁盘缓存地址
    @property (strong, nonatomic) NSMutableArray *customPaths;
    @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;//io队列(并行队列)
    
    @end
    @implementation SDImageCache {
        NSFileManager *_fileManager;//文件管理器
    }
    

    作为一个缓存类,应该具备类似于数据库的功能:增删改查。当然改是不需要的,因为每个url对应的图片资源是固定的。所以,在设计上,SDImageCache实现了增(储存图片),删(删除单个图片或清空缓存),查(根据key查询图片)。另外,还提供了一系列查询方法,比如查询缓存的大小,磁盘缓存的数量等等。我们也将根据这几大部分功能来解读。

    //--------------  初始化 ----------------//
    // 单例创建缓存
    + (SDImageCache *)sharedImageCache;
    // 通过文件夹名创建缓存
    - (id)initWithNamespace:(NSString *)ns;
    // 通过文件夹名和文件目录名创建缓存
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
    // 添加只读的缓存目录
    - (void)addReadOnlyCachePath:(NSString *)path;
    
    //--------------  储存 (根据key)----------------//
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key;
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
    - (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;
    
    //--------------  查询 (根据key)----------------//
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
    
    //--------------  删除 (根据key,删除单个)----------------//
    - (void)removeImageForKey:(NSString *)key;
    - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
    
    //--------------  清除或清理 (内存缓存或磁盘缓存)----------------//
    - (void)clearMemory;
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)clearDisk;
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
    - (void)cleanDisk;
    
    //--------------  获取缓存信息 ----------------//
    - (NSUInteger)getSize;
    - (NSUInteger)getDiskCount;
    - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
    - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
    - (BOOL)diskImageExistsWithKey:(NSString *)key;
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
    - (NSString *)defaultCachePathForKey:(NSString *)key;
    // 获取磁盘缓存的位置
    -(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
    
    缓存的初始化

    通过sharedImageCache创建一个SDImageCache的单例对象,主要在初始化做如下操作:

    • 初始化内存缓存对象(AutoPurgeCache类型,主要封装了一个注册通知,当内存警告时,清除缓存)
    • 在IO线程创建管理对象
    • 初始化diskCachePath的值(后面图片储存在这里,如下图)
    .../Library/Caches/default/com.hackemist.SDWebImageCache.default
    
    • 初始化一些属性的设置(shouldDecompressImages,shouldCacheImagesInMemory等)
    • 注册通知(clearMemory,cleanDisk,backgroundCleanDisk)
    磁盘缓存位置.png
    + (SDImageCache *)sharedImageCache {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }
    - (id)init {
        return [self initWithNamespace:@"default"];
    }
    - (id)initWithNamespace:(NSString *)ns {
        NSString *path = [self makeDiskCachePath:ns];
        return [self initWithNamespace:ns diskCacheDirectory:path];
    }
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
        if ((self = [super init])) {
            //最内层文件夹名(com.hackemist.SDWebImageCache.default)
            NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
    
            // 初始化PNG表示的data
            kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
    
            // 创建有一个IO操作的串行队列
            _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    
            // 初始化缓存时间
            _maxCacheAge = kDefaultCacheMaxCacheAge;
    
            // 创建缓存对象
            _memCache = [[AutoPurgeCache alloc] init];
            _memCache.name = fullNamespace;
    
            // 初始化磁盘缓存地址
            if (directory != nil) {
                _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
            } else {
                NSString *path = [self makeDiskCachePath:ns];
                _diskCachePath = path;
            }
    
            // 解压压图片YES
            _shouldDecompressImages = YES;
            // 缓存图片YES
            _shouldCacheImagesInMemory = YES;
            // 不后台上传iCloud
            _shouldDisableiCloud = YES;
    
            //在IO线程创建管理对象
            dispatch_sync(_ioQueue, ^{
                _fileManager = [NSFileManager new];
            });
    
            //注册通知(clearMemory,cleanDisk,backgroundCleanDisk)
            ......
        }
        return self;
    }
    
    查询图片

    查询图片总结起来有三种方式:
    ① 判断是否存在
    ② 直接获取对应图片(单单传递图片信息)
    ③ 通过Block获取图片和在哪里获取的(用于传递多个信息)

    • 检查磁盘缓存是否存在key对应的图片 ------>判断是否存在
      本质都是由NSFileManager对象通过图片地址去查看,看是否存在。这边一个比较疑惑的是一个用的是defaultManager,另一个是_fileManager。个人认为应该是返回BOOL是立即要结果,不适宜在其他线程调用。
    //检查磁盘缓存是否存在图片
    - (BOOL)diskImageExistsWithKey:(NSString *)key {
        BOOL exists = NO;
        
        // 这里使用的是defaultManager,不是_fileManager
        // 从苹果文档可以看出:可以从多个线程安全地调用共享的NSFileManager对象的方法
        exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
    
        // 要确认有带拓展名的和没带拓展名的
        if (!exists) {
            exists = [[NSFileManager defaultManager] fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
        }
        return exists;
    }
    
    //检查磁盘缓存是否存在图片,并在完成后回调block
    - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
        //这里使用的是_fileManager
        dispatch_async(_ioQueue, ^{
            BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];
    
            // 要确认有带拓展名的和没带拓展名的
            if (!exists) {
                exists = [_fileManager fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
            }
    
            //在主线程回调completionBlock
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock(exists);
                });
            }
        });
    }
    
    • 通过key去内存缓存或沙盒获取图片 ------>直接获取对应图片
      通过key去内存缓存或沙盒获取图片主要分成两部分:先去内存缓存找,再去沙盒缓存找。当然你也可以单独在内存或沙盒找。
      ①去内存缓存获取图片,直接调用memCache的objectForKey就可以了。
      ② 去沙盒缓存找要先从路径中获取imageData,然后将imageData进行一系列操作来还原成image(调整方向,转换成正确比例@2x或@3x,是否需要解压)
    //从沙盒缓存和内存缓存中获取图片
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
        // 首先先从内存缓存中获取
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            return image;
        }
        // 没有的话再从沙盒缓存中获取
        UIImage *diskImage = [self diskImageForKey:key];
        //如果沙盒缓存中有 && 设置是需要缓存图片
        if (diskImage && self.shouldCacheImagesInMemory) {
            //将图片缓存在内存中
            NSUInteger cost = SDCacheCostForImage(diskImage);
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }
        return diskImage;
    }
    //从内存缓存中获取图片
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
        return [self.memCache objectForKey:key];
    }
    //通过key从沙盒中获取图片data
    - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
        NSString *defaultPath = [self defaultCachePathForKey:key];
        NSData *data = [NSData dataWithContentsOfFile:defaultPath];
        if (data) {
            return data;
        }
        // 注意要使用有带拓展名的和没带拓展名的地址来获取一遍
        data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
        if (data) {
            return data;
        }
        //从自定义的地址获取图片data
        NSArray *customPaths = [self.customPaths copy];
        for (NSString *path in customPaths) {
            NSString *filePath = [self cachePathForKey:key inPath:path];
            NSData *imageData = [NSData dataWithContentsOfFile:filePath];
            if (imageData) {
                return imageData;
            }
            // 注意要使用有带拓展名的和没带拓展名的地址来获取一遍
            imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
            if (imageData) {
                return imageData;
            }
        }
        return nil;
    }
    //直接通过key去沙盒获取image
    - (UIImage *)diskImageForKey:(NSString *)key {
        //通过key从沙盒中获取图片data
        NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
        if (data) {
            //将data转成image(其中有包括调整方向)
            UIImage *image = [UIImage sd_imageWithData:data];
            //将里面@2x或@3x的转换成正确比例的image
            image = [self scaledImageForKey:key image:image];
            //如果有设置解压属性(即提前解压属性)就去解压
            if (self.shouldDecompressImages) {
                //解压图片
                image = [UIImage decodedImageWithImage:image];
            }
            return image;
        }
        else {
            return nil;
        }
    }
    
    • 查询可以key对应的图片,通过block回调 ,并返回一个新的operation------>通过Block获取图片和在哪里获取的
      对比之前的获取方法,同样也是这么一个步骤:先去内存缓存找,再去沙盒缓存找。只是通过这个方法可以不仅返回一个operation,并且通过block回调返回图片和对应的位置信息。
    //查询可以key对应的图片,通过block回调
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
        //判断输入参数
        if (!doneBlock) {
            return nil;
        }
        if (!key) {
            doneBlock(nil, SDImageCacheTypeNone);
            return nil;
        }
    
        // 首先通过url作为key从内存缓存中去获取
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
    
        //内存缓存没有
        //创建一个operation
        NSOperation *operation = [NSOperation new];
        //在IO队列中去查找沙盒中的图片
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                return;
            }
    
            @autoreleasepool {
                //通过key去沙盒获取image
                UIImage *diskImage = [self diskImageForKey:key];
                //如果沙盒有,并且需要缓存图片则缓存起来
                if (diskImage && self.shouldCacheImagesInMemory) {
                    //获得图片消耗的内存大小
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    //缓存起来
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
    
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
        return operation;
    }
    
    储存图片

    储存图片的一个最主要的方法是:(主要看两个参数,是否计算(计算的意思其实就是将image转成data,因为有的只传image,没有data)和是否要储存到沙盒(没有的话就只储存到内存而已))

    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate 
                                                   imageData:(NSData *)imageData 
                                                      forKey:(NSString *)key 
                                                      toDisk:(BOOL)toDisk;
    

    步骤也分成两部分,如果shouldCacheImagesInMemory = YES是需要缓存在内存,就缓存在内存中。然后根据toDisk来判断是否需要缓存在沙盒中。
    由于缓存在沙盒中的是data,要缓存在沙盒中的话,如果需要重新计算(recalculate)或者data没值,都需要通过image获取图片数据,再将data保存在沙盒中。
    还有一点需要注意的是,储存的文件名是通过MD5将key转码得来的。

    //存储图片(是否重新计算  是否在沙盒中)
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
        if (!image || !key) {
            return;
        }
        // 如果设置是需要缓存在内存,就缓存在内存中
        if (self.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(image);
            [self.memCache setObject:image forKey:key cost:cost];
        }
        //要缓存在沙盒中
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                NSData *data = imageData;
                //如果有图片 && (重新计算或没有data)--->就要通过image获取data
                if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                    //获取图片的透明讯息
                    int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                    //是否有透明度
                    BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                      alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                      alphaInfo == kCGImageAlphaNoneSkipLast);
                    BOOL imageIsPng = hasAlpha;
    
                    // 根据前缀来判断是否是png图片
                    if ([imageData length] >= [kPNGSignatureData length]) {
                        imageIsPng = ImageDataHasPNGPreffix(imageData);
                    }
    
                    if (imageIsPng) {
                        data = UIImagePNGRepresentation(image);
                    }
                    else {
                        data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                    }
    #else
                    data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
                }
                //缓存在沙盒中
                [self storeImageDataToDisk:data forKey:key];
            });
        }
    }
    
    //储存图片(内存缓存和沙盒缓存)(需要将image转成data所以要recalculate)
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key {
        [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES];
    }
    
    //储存图片(是否在沙盒中也缓存)(需要将image转成data所以要recalculate)
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
        [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk];
    }
    
    //单单缓存在沙盒中
    - (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
        
        if (!imageData) {
            return;
        }
        
        //判断_diskCachePath的路径是否存在,没有就创建路径(创建对应的文件夹)
        //.../Library/Caches/default/com.hackemist.SDWebImageCache.default
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
            [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // 获取image key 的缓存路径
        //.../Library/Caches/default/com.hackemist.SDWebImageCache.default/24dd60428e4a8af2a2da3d87a226ab9b.png
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        // 转换成 NSUrl
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        
        //将data储存起来
        [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
        
        // 是否上传iCould
        if (self.shouldDisableiCloud) {
            [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    
    缓存的删除(单个和全部)

    缓存的删除主要分成两种:
    ① 通过key来删除单张图片
    ② 删除所有的图片或删除过期的图片

    • 通过key来删除单张图片
      下面该方法是主要的方法,通过fromDisk的值来决定删除内存缓存还是删除所有的缓存(包括内存和沙盒)。通过completion来完成删除后的事项。
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
        if (key == nil) {
            return;
        }
        
        //删除内存中的缓存
        if (self.shouldCacheImagesInMemory) {
            [self.memCache removeObjectForKey:key];
        }
    
        //是否也要删除沙盒中的缓存
        if (fromDisk) {
            dispatch_async(self.ioQueue, ^{
                [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
                if (completion) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completion();
                    });
                }
            });
        } else if (completion){
            completion();
        }
    }
    
    • 删除所有的图片或删除过期的图片

    方法中带clear的都是直接清除指定的缓存。
    内存缓存就是调用memCache的removeAllObjects,磁盘缓存直接清除diskCachePath文件夹和里面的内容,再创建一个空的文件夹。

    //清除内存缓存
    - (void)clearMemory {
        [self.memCache removeAllObjects];
    }
    //清除磁盘缓存
    - (void)clearDisk {
        [self clearDiskOnCompletion:nil];
    }
    //清除沙盒的缓存,完成后执行传入的block
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
    {
        //在io队列中异步执行清除操作
        dispatch_async(self.ioQueue, ^{
            //清除文件夹
            [_fileManager removeItemAtPath:self.diskCachePath error:nil];
            //再创建个空的文件夹
            [_fileManager createDirectoryAtPath:self.diskCachePath
                    withIntermediateDirectories:YES
                                     attributes:nil
                                          error:NULL];
    
            //主线程回调传入的block(completion)
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    }
    

    方法中带clean的是整理沙盒的缓存,清除一些旧的(内存缓存只有清除)。
    清理缓存主要也有两个步骤,先清除过期的文件,如果缓存大小还是大于设置的最大缓存,就得再清理。再清理的方法是,将剩下的文件按文件的日期从旧到新排序,然后再从旧的开始删除,直到文件缓存大小小于目标缓存的一半。

    //清理沙盒的缓存,完成后执行传入的block
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
            NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
            // 这个枚举器为我们的缓存文件预取有用的属性.
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
    
            //求出过期的时间点
            NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
            NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
            NSUInteger currentCacheSize = 0;
    
            //用来储存将要删除的文件url
            NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
            //枚举器获取所有文件的url
            for (NSURL *fileURL in fileEnumerator) {
                //获取文件的相关属性字典
                /*
                 {
                 NSURLContentModificationDateKey = "2017-12-14 07:04:29 +0000";//日期
                 NSURLIsDirectoryKey = 0;//是否是文件夹
                 NSURLTotalFileAllocatedSizeKey = 8192;//文件大小
                 }
                 */
                NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
                // 跳过文件夹.
                if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // 移除比过期时间点更早的文件;
                NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
                //通过文件创建日期和过期时间点的比较来判断是否过期
                if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                    [urlsToDelete addObject:fileURL];
                    continue;
                }
    
                // 文件大小
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                //当前缓存总大小
                currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
                //将该文件的属性字典保存在cacheFiles中,以fileURL为key
                [cacheFiles setObject:resourceValues forKey:fileURL];
            }
            
            //删除urlsToDelete中包含的文件
            for (NSURL *fileURL in urlsToDelete) {
                [_fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // 如果我们剩下的磁盘缓存大小还是超过配置的容量,就再次进行清理
            // 当前容量大于设置的maxCacheSize
            if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
                // 一个期望的内存大小是配置容量的一半
                const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
                //通过日期来排序,最旧的放在数组前面
                NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                    return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                }];
    
                // 删除文件直到达到我们的目标容量.
                for (NSURL *fileURL in sortedFiles) {
                    if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                        NSDictionary *resourceValues = cacheFiles[fileURL];
                        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                        //计算当前缓存总大小
                        currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
    
                        //如果当前缓存总大小小于目标容量,就不用删除了
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
            //主线程回调completionBlock
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    缓存的其他操作

    缓存的其他操作主要是获取磁盘缓存文件的总大小和总数量。都是通过_fileManager在iO队列中去异步获取的。

    //获取磁盘缓存文件总大小(在ioQueue中)
    - (NSUInteger)getSize {
        __block NSUInteger size = 0;
        dispatch_sync(self.ioQueue, ^{
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
            for (NSString *fileName in fileEnumerator) {
                NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
                NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
                size += [attrs fileSize];
            }
        });
        return size;
    }
    //获取磁盘缓存文件数量(在ioQueue中)
    - (NSUInteger)getDiskCount {
        __block NSUInteger count = 0;
        dispatch_sync(self.ioQueue, ^{
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
            count = [[fileEnumerator allObjects] count];
        });
        return count;
    }
    //计算磁盘缓存文件总大小和数量(在ioQueue中),并通过block传出
    - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock {
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        dispatch_async(self.ioQueue, ^{
            NSUInteger fileCount = 0;
            NSUInteger totalSize = 0;
    
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:@[NSFileSize]
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
    
            for (NSURL *fileURL in fileEnumerator) {
                NSNumber *fileSize;
                [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
                totalSize += [fileSize unsignedIntegerValue];
                fileCount += 1;
            }
    
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock(fileCount, totalSize);
                });
            }
        });
    }
    

    三. 可以学习的知识点

    ① ioQueue(异步队列)的使用
    我们可以在缓存这部分内容中看到很多任务是在异步队列上进行的:

    • 获取沙盒缓存大小
    • 获取沙盒缓存文件数量
    • 将图片储存在沙盒中
    • 去沙盒查找图片
    • 删除沙盒缓存文件

    可以这样总结,跟沙盒缓存有关的操作都放在异步队列中进行。
    ② 通知的使用
    之前SDImageCache的初始化的时候,我们发现会注册三个通知。
    主要用来清理内存缓存和整理沙盒缓存,而不用我们手动去管理。

    //内存警告时会清除内存缓存
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(clearMemory)
                                                         name:UIApplicationDidReceiveMemoryWarningNotification
                                                       object:nil];
    //app终止时,会整理沙盒缓存
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(cleanDisk)
                                                         name:UIApplicationWillTerminateNotification
                                                       object:nil];
    //app进入后台时,会在后台整理沙盒缓存
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(backgroundCleanDisk)
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
    

    ③ 用completionBlock来传递多个参数
    举个例子,当我们在查询任务完成后,想获取到图片以及对应获取的位置,就可以使用Block来传递。而不是像之前一样,直接在返回值中返回一个Image。
    ④ 内联函数和C函数的使用
    其实这个也不太明白这样的一个好处???

    //最大缓存时间(一个星期)
    static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
    // PNG开头是8字节的标识(一个字节包含8位二进制数,2位十六进制)
    static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    static NSData *kPNGSignatureData = nil;
    
    BOOL ImageDataHasPNGPreffix(NSData *data);
    
    //是否是png图片
    BOOL ImageDataHasPNGPreffix(NSData *data) {
        NSUInteger pngSignatureLength = [kPNGSignatureData length];
        if ([data length] >= pngSignatureLength) {
            if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) {
                return YES;
            }
        }
    
        return NO;
    }
    
    //内联函数(类似宏定义)--图片消耗的内存空间
    FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
        return image.size.height * image.size.width * image.scale * image.scale;
    }
    

    相关文章

      网友评论

        本文标题:【源码解读】SDWebImage ─── 缓存的设计

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