接着上一篇,继续分析SDWebImage源码。上一篇主要分析了SDWebImage是怎么把图片下载到本地的,这一篇主要分析它怎么做缓存管理的。
基本思路
1, 下载图片前,先根据图片URL,检查缓存中(内存与磁盘)是否有该URL对应的图片;
2, 下载图片成功后,将图片缓存到内存中,并写到磁盘中;若失败,将该URL列入失败无效URL中
主要相关类
1,SDImageCache :图片缓存器,专门负责图片缓存
2,SDWebImageDownloader:图片下载器,专门负责图片下载
3,SDWebImageManager:图片管理类,负责调度SDWebImageDownloader和SDImageCache,下载图片前,使用SDImageCache判断图片是否缓存,若无缓存则派SDWebImageDownloader上场,去下载图片。
回到SDWebImageManager的主要方法:
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url]; //是否虚假无效的url
}
//url不存在 或者 虚假且标志位为不打算重新尝试下载
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
.......
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
........省略.....
id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
//以下代码,下载结束后执行:
if (weakOperation.isCancelled) {
}
else if (error) { //----下载图片失败后----
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
......下载图片成功后的处理......
else {
if (downloadedImage && finished) { //保存到磁盘
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
.............
}
以上代码可以看到,下载图片前,先判断URL地址是否在failedURLs中,若是虚假无效的URL,则在主线中回调completedBlock;下载图片失败后,如果不是因为网络异常的原因,则判断该URL失败无效,添加到failedURLs的集合中;
另外,可以看到以URL作为key,self.imageCache调用了queryDiskCacheForKey:done:doneBlock 方法进行了缓存查询,跳进去看看SDImageCache中的这个方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先检测内存中是否有对应的图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//内存中无对应图片,检测磁盘中是否有对应的图片
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key]; //获取磁盘中的图片
if (diskImage) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
以上代码可以看到,根据key查询内存中是否有对应图片,若无则异步读取磁盘中的图片。其中,获取内存中缓存的图片,使用了以下方法:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
self.memCache 其实是NSCache,NSCache是一个类似于集合的容器,存储键值对,与NSDictionary有点相似,但是它的特别在于:在系统资源将要耗尽时,它会自动移除缓存,并会优先移除最久未使用的对象。而且,NSCache是线程安全的,所以你看不到对NSCache有加锁的操作。
self.memCache 在 SDImageCache类初始化中被创建:
- (id)init {
return [self initWithNamespace:@"default"];
}
- (id)initWithNamespace:(NSString *)ns {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; //用于目录空间名
// 初始化 PNG 签名数据
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// 创建IO串行队列
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 缓存过期时间,默认是一周
_maxCacheAge = kDefaultCacheMaxCacheAge;
// 初始化缓存
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
// 硬盘缓存路径
_diskCachePath = [self makeDiskCachePath:fullNamespace];
// Set decompression to YES
_shouldDecompressImages = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// 内存不足发出警告时,清除内存中的缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//程序终止时,清理磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
//程序进入后台,后台清理磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
看完初始化方法,再来看看它是如何把图片写到磁盘上去的:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
//获取图片的大小
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
BOOL imageIsPng = YES;
// But if we have an image data, we will look at the preffix
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
}
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) { //缓存目录不存在,则创建
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
//将数据写入文件中
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}
以上代码中,先讲图片保存到内存到缓存中,然后异步将图片数据写到磁盘文件中,使用的是NSFileManager对象中的方法。
至此,基本上了解了SDWebImage的缓存管理机制。
以上个人见解,水平有限,如有错漏,欢迎指出,就酱~~~
相关参考
阅读SDWebImage源码
SDWebImage源码分析
《Objective-C高级编程》一书