SDWebImage 源码阅读(三)

上一篇文章我们遗留了一些问题:

  • 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 类下载图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值