SDWebImage的缓存可以说是这个框架的一个重大的有点,下面我们就来了解一下这个框架的缓存是如何实现的。
从整体来说,SDWebImage的缓存分为两部分SDImageCache使用NSCache实现,另一部分磁盘缓存,使用NSFileManager实现
下载后的图片缓存 图片下载成功之后的缓存
1 2 - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk
1、图片下载成功之后缓存到Cache中 1 2 3 4 5 6 7 8 @property (strong, nonatomic) NSCache *memCache; // if memory cache is enabled if (self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self.memCache setObject:image forKey:key cost:cost]; }
self.memCache实际上就是一个NSCache,类似于字典的一种存储方式,需要传入图片的消耗
2、图片下载成功之后缓存到磁盘 注意:
这里在图片保存之前有一个图片格式转换的过程(*->PNG)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 // 如果image存在,但是需要重新计算(recalculate)或者data为空 // 那就要根据image重新生成新的data // 不过要是连image也为空的话,那就别存了 if (image && (recalculate || !data)) { //#if TARGET_OS_IPHONE // 我们需要判断image是PNG还是JPEG // PNG的图片很容易检测出来,因为它们有一个特定的标示 (http:// www.w3.org/TR/PNG-Structure.html) // PNG图片的前8个字节不许符合下面这些值(十进制表示) // 137 80 78 71 13 10 26 10 // 如果imageData为空l (举个例子,比如image在下载后需要transform, 那么就imageData就会为空) // 并且image有一个alpha通道, 我们将该image看做PNG以避免透明度 (alpha)的丢失(因为JPEG没有透明色) //通过AlphaInfo获取图片的先关信息 int alphaInfo = CGImageGetAlphaInfo(image.CGImage); // 该image中确实有透明信息,就认为image为PNG BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); //疑问:是否为PNG格式的图片和透明度有什么关系 BOOL imageIsPng = hasAlpha; // 但是如果我们已经有了imageData,我们就可以直接根据data中前几个字节 判断是不是PNG if ([imageData length] >= [kPNGSignatureData length]) { // ImageDataHasPNGPreffix就是为了判断imageData前8个字节 是不是符合PNG标志 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 }
图片个格式处理转换完成之后,将这个图片保存到本地
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // 首先判断disk cache的文件路径是否存在,不存在的话就创建一个 // disk cache的文件路径是存储在_diskCachePath中的 if (![_fileManager fileExistsAtPath:_diskCachePath]) { [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; } // get cache Path for image key 获取图片保存的路径: NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; 图片放在cache目录下 [directory stringByAppendingPathComponent:fullNamespace] 最终地址: 目录+图片名(cachekey的MD5) // 根据image的key(一般情况下理解为image的url)组合成最终的文件路径 // 上面那个生成的文件路径只是一个文件目录,就跟/cache/images/img1.png和 cache/images/的区别一样 // defaultCachePathForKey后面会详解 NSString *cachePathForKey = [self defaultCachePathForKey:key]; // transform to NSUrl NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil]; // disable iCloud backup //iCloud 不备份 if (self.shouldDisableiCloud) { [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil]; }
缓存图片的查找: 根据cachekey 判断这个图片是否缓存过 1 2 3 - (NSOperation *)queryDiskCacheForKey:( NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
从缓存中查找 1 2 3 4 5 6 7 8 9 10 - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } 如果查找到了,直接返回图片,如果没找到请看下一步
从磁盘中查找 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; } @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } });
在磁盘中查找此图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - (UIImage *)diskImageForKey:(NSString *)key { NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; if (data) { UIImage *image = [UIImage sd_imageWithData:data]; image = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } return image; } else { return nil; } }
根据key获取到本地磁盘中关于这个图片的数据,注意这里stringByDeletingPathExtension方法以后可以多借鉴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key { NSString *defaultPath = [self defaultCachePathForKey:key]; NSData *data = [NSData dataWithContentsOfFile:defaultPath]; if (data) { return data; } // checking the key with and without the extension data = [NSData dataWithContentsOfFile: [defaultPath stringByDeletingPathExtension]]; if (data) { return data; } NSArray *customPaths = [self.customPaths copy]; for (NSString *path in customPaths) { NSString *filePath = [self cachePathForKey:key inPath:path]; NSData *imageData = [NSData dataWithContentsOfFile:filePath]; if (imageData) { return imageData; } // checking the key with and without the extension imageData = [NSData dataWithContentsOfFile: [filePath stringByDeletingPathExtension]]; if (imageData) { return imageData; } } return nil; }
根据key,获取这个图片在本地如果存在的话,返回图片的路径,这里的方法和图片下载完成之后图片的存储地址的生成是相同的
1 2 3 4 5 6 7 8 9 - (NSString *)defaultCachePathForKey:(NSString *)key { return [self cachePathForKey:key inPath:self.diskCachePath]; } - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path { NSString *filename = [self cachedFileNameForKey:key]; return [path stringByAppendingPathComponent:filename]; }
对图片名称进行MD5的方法(可以保留做一个工具方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 - (NSString *)cachedFileNameForKey:(NSString *)key { const char *str = [key UTF8String]; if (str == NULL) { str = ""; } // 使用了MD5进行加密处理 // 开辟一个16字节(128位:md5加密出来就是128bit)的空间 unsigned char r[CC_MD5_DIGEST_LENGTH]; // 官方封装好的加密方法 // 把str字符串转换成了32位的16进制数列(这个过程不可逆转) 存储到了r这个空间中 CC_MD5(str, (CC_LONG)strlen(str), r); // 最终生成的文件名就是 "md5码"+".文件类型" NSString *filename = [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x%02x%02x%02x%02x%@", r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]]; return filename; }
计算图片的消耗
cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
通常,精确的 cost 应该是对象占用的字节数
1 2 3 4 5 6 7 FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) { //这里我觉得这样写不是很好,如果这样写就更直观了 // return (height * scale) * (width * scale) return image.size.height * image.size.width * image.scale * image.scale; }
图片找到之后
1 2 3 4 5 6 7 8 9 10 11 dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !strongOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; }