源码解读程序员

SDWebImage源码学习笔记 ☞ SDImageCache

2018-12-11  本文已影响19人  RiverSea
SDWebImage 源码学习笔记.png

前言

这是本系列的第 5 篇,也是最后一篇,主要讨论处理缓存的类 SDImageCache 及相关类 SDMemoryCacheSDImageCacheConfig 等。

正文

先介绍 SDImageCache.h 中定义的 2 个枚举:SDImageCacheTypeSDImageCacheOptions

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    // 不缓存,从网络下载数据
    SDImageCacheTypeNone,
    // 磁盘缓存
    SDImageCacheTypeDisk,
    // 内存缓存
    SDImageCacheTypeMemory
};

typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
    // 即使内存中有缓存,也要强制查询磁盘缓存
    SDImageCacheQueryDataWhenInMemory = 1 << 0,
    // 强制同步查询磁盘缓存
    SDImageCacheQueryDiskSync = 1 << 1,
    // 压缩大图
    SDImageCacheScaleDownLargeImages = 1 << 2
};

包含几个重要属性:

/// *** 缓存配置信息 ***
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
/// 内存缓存的最大消耗
@property (assign, nonatomic) NSUInteger maxMemoryCost;
/// 内存缓存的最大缓存数量
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

/// *** 内存缓存 ***
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
/// 磁盘缓存路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
/// 
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
/// 读写操作的串行队列
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
/// 用于操作文件的 fileManager
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;

其中 2 个属性需要重点关注一下:

config 所属类 SDImageCacheConfig 定义了很短属性,只提供了一个init方法,在里边给所有属性付了初值,详见下方法代码:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

@implementation SDImageCacheConfig

- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES;
        _shouldDisableiCloud = YES;
        _shouldCacheImagesInMemory = YES;
        _shouldUseWeakMemoryCache = YES;
        _diskCacheReadingOptions = 0;
        _diskCacheWritingOptions = NSDataWritingAtomic;
        _maxCacheAge = kDefaultCacheMaxCacheAge;
        _maxCacheSize = 0;
        _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
    }
    return self;
}

@end

memCache 它属于一个继承自 NSCache 的缓存类 SDMemoryCache, 他有一个关键属性:

// weakCache 是 NSMapTable 类型
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; 

下面观察一下 SDMemoryCache 的初始化及相关代码:

- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
    self = [super init];
    if (self) {

        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

        // 其他省略 ...

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarning:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
    }
    return self;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    // 注意:此处是调用的 suoper 方法,所以并没有移除 weak cache,如果是调用 self 重写的 removeAllObjects 方法,就会移除 weak cache。
    [super removeAllObjects];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIApplicationDidReceiveMemoryWarningNotification
                                                  object:nil];
}

当遇到内存警告的时候,缓存会被清除,但 weak cache 并不会被移除,如果手动清除的话,weak cache 当然会被移除。

这里 value 设置成 weak 可以避免可能的循环引用,虽然是 weak,不过,image 实例可以被其他对象持有,像 imageView,这种情况下,value 就不是 nil。

似乎扯远了O(∩_∩)O哈哈~,好了,我们还是切回来继续讨论 SDImageCache 提供的方法吧!

首先,是 SDImageCache 的创建方法,它给我们提供了一个单例方法 + (nonnull instancetype)sharedImageCache,下边是他的方法实现:

+ (nonnull instancetype)sharedImageCache {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

然后,看看初始化方法,我们发现所有初始化方法最后均调用了同一个核心方法 - (nonnull instancetype)initWithNamespace: diskCacheDirectory:,具体作用见下方代码注释。

- (instancetype)init {
    return [self initWithNamespace:@"default"];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        
        // 创建一个 IO 串行队列 (依次执行操作)
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        
        // 初始化内存缓存
        _config = [[SDImageCacheConfig alloc] init];
        _memCache = [[SDMemoryCache alloc] initWithConfig:_config];
        _memCache.name = fullNamespace;

        // 初始化磁盘缓存路径
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace]; 
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

        dispatch_sync(_ioQueue, ^{
            self.fileManager = [NSFileManager new];
        });

#if SD_UIKIT
        // App 即将关闭的时候,清除过期缓存
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        // App 即将进入后台的时候,清除过期缓存
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}

记得 上一篇 介绍 SDWebImageManager 的时候,是这样使用 imageCache 的:

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
                                                              options:cacheOptions
                                                                 done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
      // 查询完成后的操作在这里,可能查到了,也可能没查到 ...
}

现在,我们就来揭开这个方法的什么面纱。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
                                            options:(SDImageCacheOptions)options
                                               done:(nullable SDCacheQueryCompletedBlock)doneBlock
{
    // 1.校验参数
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 2.查询内存缓存 (NSCache)
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    NSOperation *operation = [NSOperation new];
    
    // 3.将获取缓存及解压的 ‘耗时’ 操作封装成一个 block
    void(^queryDiskBlock)(void) =  ^{
        
        // 如果已经取消,不作任何处理,直接返回。
        if (operation.isCancelled) {
            return;
        }
        
        @autoreleasepool {
            
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            
            if (image) {
                
                // A > 从 memery 取的

                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
                
            } else if (diskData) {
                
                // B > 如果内存没有,但是从 disc 取到了,需要解压
                
                diskImage = [self diskImageForKey:key data:diskData options:options];
                
                if (diskImage && self.config.shouldCacheImagesInMemory) { // 缓存到内存
                    NSUInteger cost = SDCacheCostForImage(diskImage); // 计算大小
                    [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);
                    });
                }
            }
        }
    };
    
    // 4.执行查询磁盘缓存的 block
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

如上边的代码所示,主要分了这么 4 步:

① 校验参数 --- 如果 key 不存在,直接 doneBlock,返回 nil。

② 查询内存缓存 NSCache --- 如果内存中有,并且没有强制要求必须查询磁盘,则 执行 doneBlock,将 image 返回。

③ 将获取缓存及解压的 ‘耗时’ 操作封装成一个 block --- 这是为了最后执行异步操作的方便。

④ 执行查询磁盘缓存的 block --- 如果设置了 SDImageCacheQueryDiskSync,则同步执行;否则,默认是异步执行。

第 ③ 步中查询磁盘缓存的 queryDiskBlock 里边有两个比较重要的方法:- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:- (nullable UIImage *)diskImageForKey: data: options:,下边分别了解一下这两个方法:

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    // 1.尝试 通过默认路径查询磁盘缓存
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }

    // 2.异常情况的处理:更换了路径再取一次,新路径是将默认路径的后缀去掉 (如果有的话)
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }

    // 3.遍历所有用户自定义的路径,执行类似 1、2 的操作,查询磁盘缓存
    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }

        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }
    }
    // 没查到的话,返回 nil
    return nil;
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
    if (data) {
        
        // 1.解码
        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
        image = [self scaledImageForKey:key image:image];
        
        // 2.解压
        if (self.config.shouldDecompressImages) {
            BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
            
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
        }
        
        return image;
    } else {
        return nil;
    }
}

此方法分别使用了 SDWebImageCodersManager 的 2 个重要方法:

// SDWebImageCodersManager.m

- (UIImage *)decodedImageWithData:(NSData *)data {
    
    LOCK(self.codersLock);
    NSArray<id<SDWebImageCoder>> *coders = self.coders;
    UNLOCK(self.codersLock);
    
    for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
        if ([coder canDecodeFromData:data]) {
            return [coder decodedImageWithData:data];
        }
    }
    return nil;
}
// SDWebImageCodersManager.m

- (UIImage *)decompressedImageWithImage:(UIImage *)image
                                   data:(NSData *__autoreleasing  _Nullable *)data
                                options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
    if (!image) {
        return nil;
    }
    LOCK(self.codersLock);
    NSArray<id<SDWebImageCoder>> *coders = self.coders;
    UNLOCK(self.codersLock);
    for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
        if ([coder canDecodeFromData:*data]) {
            UIImage *decompressedImage = [coder decompressedImageWithImage:image data:data options:optionsDict];
            decompressedImage.sd_imageFormat = image.sd_imageFormat;
            return decompressedImage;
        }
    }
    return nil;
}

那么,这些 coder 是什么时候加进去的,又是怎么添加的呢?其实,这些逻辑都在 SDWebImageCodersManager 的实现代码里。

// SDWebImageCodersManager.m

- (instancetype)init {
    if (self = [super init]) {
        // 初始化 coders
        NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
        [mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
        _coders = [mutableCoders copy];
        _codersLock = dispatch_semaphore_create(1);
    }
    return self;
}

从初始化方法可以看出来,此时只给 coders 添加了一种 coder,即 SDWebImageImageIOCoder,它是用来对普通的 JPG、PNG 等图片解码的。

为了支持对其他类型图片(如 GIF)的解码,manager 给我们提供了下边这个添加 coder 的方法,代码逻辑很简单,就不多做解释了。

// SDWebImageCodersManager.m

- (void)addCoder:(nonnull id<SDWebImageCoder>)coder {
    if (![coder conformsToProtocol:@protocol(SDWebImageCoder)]) {
        return;
    }
    LOCK(self.codersLock);
    NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [self.coders mutableCopy];
    if (!mutableCoders) {
        mutableCoders = [NSMutableArray array];
    }
    [mutableCoders addObject:coder];
    self.coders = [mutableCoders copy];
    UNLOCK(self.codersLock);
}

小结

关于缓存相关的类暂时就先介绍到这里,当然还有很多细节没来得及讨论,不过可以查看 demo 中的注释。

到此,关于 SDWebImage 的源码学习就告一段落了,目前的理解可能有点肤浅,以后随着理解的深入,会不定时的更新。

上一篇下一篇

猜你喜欢

热点阅读