SDWebImage-Cache

SDWebImage在iOS界是一个使用非常广泛的图片加载的库,而现在除了会使用SDWebImage,还会使用YYImage、Kingfisher来请求图片。

SDWebImage主要的三个功能模块:缓存策略(Cache)、图片的编解码(Decoder)、图片下载(Downloader)

这里总结了一些针对缓存策略Cache模块的读后感,Cache模块优雅地实现了数据内存cache和磁盘disk的写、读、删。

缓存流

将image url通过md5加密生成的字符串作为key,先取消下载队列中这个key对应的operation,再查看内存中是否存在,如果不存在,再查看磁盘中是否存在,如果都不存在,就开线程下载图片,图片下载完成后,用key作为标识将image对象存储到内存和磁盘中;在内存警告的时候,销毁没有使用的image。

SDImageCache

主要负责图片缓存(存储、查询、移除)。

图片增删查的实现

SDImageCache在初始化的时候,不仅创建了内存缓存实例、缓存配置实例、磁盘路径、文件管理实例,还创建了串行队列_ioQueue,在实现图片的存储、查询、磁盘的清除存储时候,开一个异步线程,在异步线程中完成所有的操作。

最终调用的方式是:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock

内部实现大致为:

 // 创建一个串行队列
dispatch_queue_t _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache_io", DISPATCH_QUEUE_SERIAL);
    
for (int i =0; i<100; i++) {
        
    dispatch_async(_ioQueue, ^{
    
        // NSLog(@"%d--%@", i,[NSThread currentThread]);
            
        // 将图片编码 ...
        // 将图片存储到磁盘 ...
        // 在主线程中进行回调
        
        // 图片的获取
        // 图片的解码
        
        // 移除指定的图片
        // 移除所有的图片
    });
}

开一个异步线程,串行执行事件

内存存储SDMemoryCache

NSCache衍生类,在图片下载完成后都将存储在NSCache实例中,在接收到内存警时使用removeAllObjects方法清理数据;在将图片存储在NSCache实例的同时,也会存储在weakCache这个属性中,weakCacheNSMapTable类型的实例,使用NSMapTable进行弱缓存(辅助缓存),并使用dispatch_semaphore_t来保证NSMapTable的线程安全,NSCache本身就是线程安全的。

为什么要使用辅助缓存,图片下载后既要保存到NSCache又要被weakCache引用呢?

// Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
// This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
// At this case, we can sync weak cache back and do not need to load from disk cache

// 创建弱引用,weakCache强引用key值,弱引用value值,当value被销毁的时候,此键值对将会被销毁。
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

当内存警告的时候,cache将取消对所有image的引用,此时image如果没有被UIImageView实例强引用的话,由于weakCache对image是弱引用,那对应的键值对也会被销毁,此image将会被系统销毁,而被UIImageView实例强引用的image则会被保留,此时cache空了,weakCache中只会保留被UIImageView强引用的image。

如果图片再次被查询时就会从weakCache中直接获取,并同步到cache中,避免了从磁盘中重复获取,达到了性能的最大化。

磁盘存储

SDImageCache在初始化时候,在磁盘的cache文件夹存储所有图片,并创建了文件管理实例。

SDImageCache将传入的image同时保存到内存cache和磁盘disk中,这以存储的过程都是线程安全的。由于存储、查询、移除操作是比较费时的,所以存储操是在异步线程中进行的,而且内部创建了一个串行队列来顺序执行这一系列操作。

一些零散的点

  1. 设置下载模式为:SDWebImageLowPriority的效果:默认情况下,下载图片是在UI交互期间的,这个标志是在可以使得在UIScrollView在减速滚动的时候下载。
  1. 在SDWebImage中,多个地方使用了NSMapTable类(下载队列、弱缓存)初始化NSMapTable时,对value进行了弱引用,当value值被销毁的时候,会自动将此键值对销毁。

  2. SDWebImage在进入后台、程序即将删除的时候,会申请一段时间来删除数据:

- (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;
    }];
}

官网上说可以在后台维持十分钟,但测试的结果只有三分钟。 beginBackgroundTaskWithExpirationHandler 和endBackgroundTask必须要成对出现

  1. 使用信号量来解决线程安全问题
    SDWebImage使用信号量的方法将线程同步,在初始化SDMemoryCache时将信号量的value值设置成1。
// 信号量,当信号量的value >= 1的时,就可以继续执行下去。

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

,wait使得信号量的value减一,此时会在阻塞当前线程,直至到timeout超时,或使用dispatch_semaphore_signal()使得信号量的value加1,在所处线程中继续执行后面的事件

  1. 计算图片占用内存的空间大小
// 使用内联函数
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}
  1. 文件操作

SDWebImage在文件操作,使用NSFileManager方面也是值得学习的,这里总结下方便以后复习吧。其存储图片在沙盒的Cache文件夹下,其在模拟器上的路径大致是:

/Users/wangwei/Library/Developer/CoreSimulator/Devices/.../data/Containers/Data/Application/.../Library/Caches/default/default/com.hackemist.SDWebImageCache.default/

下面其中一些文件的操作的方法

0. 创建文件夹路径path(文件名都需要在前面拼接这个path)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace];

1. 文件夹是否存在,不存在则创建文件夹 
fileExistsAtPath:  
createDirectoryAtPath:withIntermediateDirectories:attributes:error:  

2. 拼接文件的完整路径(添加/号) 
[path stringByAppendingPathComponent:filename];

3. 查询某个文件path是否存在 
[self.fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];

4. 写入image data,写入到文件到指定的url路径下 
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
5. 读取image data
NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
6. 删除image data
[self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];

7. 删除文件夹,并创建文件夹
[self.fileManager removeItemAtPath:self.diskCachePath error:nil];
[self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];

// 8. 获取有用的缓存文件
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

  1. @autoreleasepool的使用

自动释放池和autorelease方法是objective-c的内存管理话题中的一部分,SDWebImage中在很多地方都是用了,如在图片的读取和写入等。其读取代码具体是:

NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) =  ^{
    if (operation.isCancelled) {
        // do not call the completion if cancelled
        return;
    }
    
    // 使用autoreleasepool
    @autoreleasepool {
        NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        UIImage *diskImage;
        SDImageCacheType cacheType = SDImageCacheTypeDisk;
        if (image) {
            // the image is from in-memory cache
            diskImage = image;
            cacheType = SDImageCacheTypeMemory;
        } else if (diskData) {
            // decode image data only if in-memory cache missed
            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);
                });
            }
        }
    }
};

if (options & SDImageCacheQueryDiskSync) {
    queryDiskBlock();
} else {
    dispatch_async(self.ioQueue, queryDiskBlock);
}

自动释放池的内部源码和机制分析

你可能感兴趣的:(SDWebImage-Cache)