SDWebImage源码之SDImageCache

SDImageCache是SDWebImage库的图片缓存类,其提供了内存和磁盘缓存两种机制,并且设计了一些策略对缓存的图片进行管理。


以下代码和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c这个commit。

SDImageCache

// See https://github.com/rs/SDWebImage/pull/1141 for discussion
@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

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

@end

AutoPurgeCache用作SDImageCache的内存缓存使用。虽然NSCache本身为了保证不引起内存问题,实现了自动剔除数据的策略,但它仍然无法保证在UIApplicationDidReceiveMemoryWarningNotification发生时清空内存,详见这里。
所以AutoPurgeCache在收到UIApplicationDidReceiveMemoryWarningNotification后,调用removeAllObjects清除缓存里所有的数据。


FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
    return image.size.height * image.size.width * image.scale * image.scale;
}

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    ...
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    ...
}

- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost {
    self.memCache.totalCostLimit = maxMemoryCost;
}

NSCache支持设置totalCostLimit,在设置每一个数据的时候都分别设上数据的cost。缓存里的数据到达限制时会触发NSCache的清理策略,来保证数据的总cost低于限制。清理数据的具体时机和策略由NSCache内部实现决定,外部无法控制。
cost一般被设置为value的占用字节数,但是也可以根据需求设计不同的逻辑。

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

最近命中的数据有较大可能会再次被用到,所以在从磁盘读取数据后,把它也存到内存缓存。

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    ...
    [[NSNotificationCenter defaultCenter] addObserver:self                                    
        selector:@selector(backgroundDeleteOldFiles)                                     
        name:UIApplicationDidEnterBackgroundNotification
        object:nil];
   ...
}

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}
                       

在程序进到后台以后,开一个后台任务清理图片缓存,把过期的图片缓存从磁盘上清除。
清除策略保存在SDImageCacheConfig里,包括了过期时间和最大占用磁盘空间两项。
具体的实现是先删除所有过期的图片,再从旧到新删除文件,直到满足磁盘空间要求。详细代码见deleteOldFilesWithCompletionBlock方法。

SDImageCacheTests


// Testing storeImage:forKey:toDisk:YES
- (void)test07InsertionOfImageForcingDiskStorage{
    XCTestExpectation *expectation = [self expectationWithDescription:@"storeImage forKey toDisk=YES"];
    
    UIImage *image = [self imageForTesting];
    [self.sharedImageCache storeImage:image forKey:kImageTestKey toDisk:YES completion:nil];
    expect([self.sharedImageCache imageFromMemoryCacheForKey:kImageTestKey]).to.equal(image);
    [self.sharedImageCache diskImageExistsWithKey:kImageTestKey completion:^(BOOL isInCache) {
        if (isInCache) {
            [expectation fulfill];
        } else {
            XCTFail(@"Image should be in cache");
        }
    }];
    [self waitForExpectationsWithTimeout:kAsyncTestTimeout handler:nil];
}

缓存类的测试代码可以直接使用类本身提供的增删改查接口进行相互验证,这段代码中也包括了XCTest框架中异步接口的测试写法。


SDWebImage源码系列

SDWebImage源码之SDImageCache
SDWebImage源码之SDWebImageDownloader
SDWebImage源码之SDWebImageManager

你可能感兴趣的:(SDWebImage源码之SDImageCache)