上一篇文章我们遗留了一些问题:
- SDImageCache : 根据 key 去缓存中查找对应的图片
- SDWebImageDownloader : 下载图片
- SDImageCache : 将图片存入缓存
前面的代码并没有特意去解析 SDImageCache 的缓存机制,这一篇我们主要带着问题来讲解 SDImageCache 类。
1. 根据 key 去缓存中查找对应的图片
SDImageCache 类中这么多函数,我们先从哪里看起呢?和 SDImageCache 相关的遗留问题有两个:根据 key 去缓存中查找对应的图片、将图片存入缓存。那我们就先从第一个遗留问题开始。
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
...
}];
上面的代码是 SDWebImageManager 中调用 SDImageCache 的方法,根据key去查找缓存中对应的图片,进入该方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
// 首先检查内存缓存(详情见 1.1)
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果内存中有,直接返回image
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
// 否则,说明图片在磁盘cache中
// 采用new的方式只能采用默认的init方法完成初始化,采用alloc的方式可以用其他定制的初始化方法
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
// 从硬盘缓存中取得图片(详情见 1.2)
UIImage *diskImage = [self diskImageForKey:key];
// 如果磁盘中得到了该image,并且还需要缓存到内存中,为了同步最新数据
if (diskImage && self.shouldCacheImagesInMemory) {
// 精确的cost应该是对象占用的字节数
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
1.1. 从内存缓存中获取图片
获取内存缓存中的图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
点击进入 imageFromMemoryCacheForKey: 方法
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
通过 key-value 从内存缓存中获取图片
1.2. 从硬盘缓存中获取图片
获取硬盘缓存中的图片
UIImage *diskImage = [self diskImageForKey:key];
点击进入 diskImageForKey: 方法
- (UIImage *)diskImageForKey:(NSString *)key {
// 获取硬盘缓存的 image data(详情见 1.3)
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
// data 转换成 image 格式(详情见 1.4)
UIImage *image = [UIImage sd_imageWithData:data];
// 设置是否动图和scale大小(详情见 1.5)
image = [self scaledImageForKey:key image:image];
// 判断是否要压缩图片,默认要压缩图片
if (self.shouldDecompressImages) {
// 解压缩图片(详情见 1.6)
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
1.3. 获取硬盘缓存的 image data
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
// 根据图片的key获取到全路径,这个路径是MD5加密路径
// 这里嵌套了2、3层,最后的MD5加密需要注意一下
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// 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;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
MD5 加密
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
// MD5进行加密处理
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
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;
}
1.4. data 转换成 image
+ (UIImage *)sd_imageWithData:(NSData *)data {
if (!data) {
return nil;
}
UIImage *image;
// 根据data的前面几个字节,判断出图片类型,是jepg、png、gif...
NSString *imageContentType = [NSData sd_contentTypeForImageData:data];
// 如果是gif图片或webp图片,需要单独处理
if ([imageContentType isEqualToString:@"image/gif"]) {
image = [UIImage sd_animatedGIFWithData:data];
}
#ifdef SD_WEBP
else if ([imageContentType isEqualToString:@"image/webp"])
{
image = [UIImage sd_imageWithWebPData:data];
}
#endif
else {
image = [[UIImage alloc] initWithData:data];
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
}
return image;
}
1.5. 设置动图和 scale 大小
进入 scaledImageForKey: image: 方法后,又调用一个方法
- (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image {
return SDScaledImageForKey(key, image);
}
继续进入,发现这是一个 c++ 函数
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
if (!image) {
return nil;
}
// 如果 image.images 存在,可以理解为 gif 图
if ([image.images count] > 0) {
NSMutableArray *scaledImages = [NSMutableArray array];
// 使用递归,构建一组动图
for (UIImage *tempImage in image.images) {
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
// 根据这些 images 构成我们所需的 animated image
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
}
else {
// 屏幕为 320x480, scale 为 1
// 屏幕为 640x960, scale 为 2
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
CGFloat scale = 1;
// "@2x.png"的长度是7,所以此处添加了这个判断
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
}
1.6. 解压缩图片
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
// while downloading huge amount of images
// autorelease the bitmap context
// and all vars to help system to free memory
// when there are memory warning.
// on iOS7, do not forget to call
// [[SDImageCache sharedImageCache] clearMemory];
// 当下载大量图片,产生内存警告时
// 自动释放bitmap上下文环境和所有变量来释放系统内存空间
// 在iOS 7中不要忘记添加
// [[SDImageCache sharedImageCache] clearMemory];
if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
return nil;
}
@autoreleasepool{
// do not decode animated images
// 对于 animated images 不需要解压缩
if (image.images != nil) {
return image;
}
CGImageRef imageRef = image.CGImage;
// 图片如果有alpha通道,就返回原始image,因为jpg图片有alpha的话,就不压缩
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
if (anyAlpha) {
return image;
}
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
// 如果属于上述不支持的ColorSpace,ColorSpace就使用RGB
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
}
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitsPerComponent = 8;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
// 当你调用这个函数的时候,Quartz创建一个位图绘制环境,也就是位图上下文。
// 当你向上下文中绘制信息时,Quartz把你要绘制的信息作为位图数据绘制到指定的内存块。
// 一个新的位图上下文的像素格式由三个参数决定:
// 每个组件的位数,颜色空间,alpha选项。alpha值决定了绘制像素的透明性。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
bitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
// Draw the image into the context and retrieve the new bitmap image without alpha
// 在上面创建的context绘制image,并以此获取image,而该image也将拥有alpha通道
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
// 开始释放资源
if (unsupportedColorSpace) {
CGColorSpaceRelease(colorspaceRef);
}
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
2. 将图片存入磁盘缓存
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// if memory cache is enabled
// 如果可以使用内存缓存
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
// 构建一个data,用来存储到disk中,默认为imageData
NSData *data = imageData;
// 如果image存在,但是需要重新计算(recalculate)或者data为空
// 那就要根据image重新生成新的data
// 不过要是连image也为空的话,那就别存了
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// We need to determine if the image is a PNG or a JPEG
// PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
// The first eight bytes of a PNG file always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
// If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
// and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
// 我们需要判断image是PNG还是JPEG
// PNG的图片很容易检测出来,因为它们有一个特定的标示 (http://www.w3.org/TR/PNG-Structure.html)
// PNG图片的前8个字节不许符合下面这些值(十进制表示)
// 137 80 78 71 13 10 26 10
// 如果imageData为空 (举个例子,比如image在下载后需要transform,那么就imageData就会为空)
// 并且image有一个alpha通道, 我们将该image看做PNG以避免透明度(alpha)的丢失(因为JPEG没有透明色)
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
// 该image中有透明信息,就认为image为PNG
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
// 但是如果我们已经有了imageData,我们就可以直接根据data中前几个字节判断是不是PNG
if ([imageData length] >= [kPNGSignatureData length]) {
// ImageDataHasPNGPreffix 就是为了判断 imageData 前8个字节是不是符合PNG标志
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 如果image是PNG格式,就是用UIImagePNGRepresentation将其转化为NSData,否则按照JPEG格式转化,并且压缩质量为1,即无压缩
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
// 当然,如果不是在iPhone平台上,就使用下面这个方法。不过不在我们研究范围之内
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
[self storeImageDataToDisk:data forKey:key];
});
}
}
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
if (!imageData) {
return;
}
// 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
// disk cache的文件路径是存储在_diskCachePath中的
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
// 根据image的key(一般情况下理解为image的url)组合成最终的文件路径,之前文章已经讲过了
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
// 根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup
// 如果不使用iCloud进行备份,就使用NSURLIsExcludedFromBackupKey
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
3. 总结
查找图片和存储图片运用了好多 CGImage 相关知识,这方法不足之处还差好多,虽然基本理解了图片存储与查找,但是细节还是没有好好的研究,日后把这里作为一个重点拿出来详细的研究一下。
遗留问题:使用 SDWebImageDownloader 类下载图片