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
这个属性中,weakCache
是NSMapTable
类型的实例,使用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中,这以存储的过程都是线程安全的。由于存储、查询、移除操作是比较费时的,所以存储操是在异步线程中进行的,而且内部创建了一个串行队列来顺序执行这一系列操作。
一些零散的点
- 设置下载模式为:SDWebImageLowPriority的效果:默认情况下,下载图片是在UI交互期间的,这个标志是在可以使得在UIScrollView在减速滚动的时候下载。
在SDWebImage中,多个地方使用了NSMapTable类(下载队列、弱缓存)初始化NSMapTable时,对value进行了弱引用,当value值被销毁的时候,会自动将此键值对销毁。
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必须要成对出现
- 使用信号量来解决线程安全问题
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,在所处线程中继续执行后面的事件
- 计算图片占用内存的空间大小
// 使用内联函数
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
}
- 文件操作
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];
- @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);
}
自动释放池的内部源码和机制分析