SDWebImage探究(十九) —— 图像的解码 (一)

版本记录

版本号 时间
V1.0 2018.02.25

前言

我们做APP,文字和图片是绝对不可缺少的元素,特别是图片一般存储在图床里面,一般公司可以委托第三方保存,NB的公司也可以自己存储图片,ios有很多图片加载的第三方框架,其中最优秀的莫过于SDWebImage,它几乎可以满足你所有的需求,用了好几年这个框架,今天想总结一下。感兴趣的可以看其他几篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 图片类型判断深入研究
7. SDWebImage探究(七) —— 深入研究图片下载流程(一)之有关option的位移枚举的说明
8. SDWebImage探究(八) —— 深入研究图片下载流程(二)之开始下载并返回下载结果的总的方法
9. SDWebImage探究(九) —— 深入研究图片下载流程(三)之下载之前的缓存查询操作
10. SDWebImage探究(十) —— 深入研究图片下载流程(四)之查询缓存后的block回调处理
11. SDWebImage探究(十一) —— 深入研究图片下载流程(五)之SDWebImageDownloadToken和操作对象的生成和返回
12. SDWebImage探究(十二) —— 深入研究图片下载流程(六)之下载器到具体下载操作的代理分发实现
13. SDWebImage探究(十三) —— 深入研究图片下载流程(七)之NSURLSession中几个代理的基本用法和关系
14. SDWebImage探究(十四) —— 深入研究图片下载流程(八)之下载完成代理方法的调用
15. SDWebImage探究(十五) —— 深入研究图片下载流程(九)之身份验证质询代理方法调用
16. SDWebImage探究(十六) —— 深入研究图片下载流程(十)之缓存相关代理方法调用
17. SDWebImage探究(十七) —— 深入研究图片下载流程(十一)之收到响应代理方法调用
18. SDWebImage探究(十八) —— 深入研究图片下载流程(十二)之收到图像数据代理方法调用

图像为什么需要解码

一般下载或者从磁盘获取的图片是PNG或者JPG,这是经过编码压缩后的图片数据,不是位图,要把它们渲染到屏幕前就需要进行解码转成位图数据,而这个解码操作比较耗时。你也可以这么理解,图片在远端存储一定都是编码后存储的,这样体积小,一个图像可以看做是一个图像文件,里面包含了文件头,文件体和文件尾,图像的数据就包含在文件体中,而我们的解码就是运用算法将文件体中的图像数据转化为位图数据,方便渲染和展示。

iOS默认是在主线程解码,所以SDWebImage将这个过程放到子线程了。

同时因为位图体积很大,所以磁盘缓存不会直接缓存位图数据,而是编码压缩后的PNG或JPG数据。


图像是否可以解码的判断

这里有几种情况是不进行解码的。

  • 动图,判断image.images != nil,满足这个条件的就是动图,就不解码。
  • 有alpha信息的图片不解码。

1. 动图

这里首先要纠正一个概念,动态图片也不仅仅限于gif,png格式也有动态的,只是常见的是GIF格式的比较多而已。

这里动图为什么不解码呢?

这个我还没找到正确的答案,猜测可能是动图解码后的数据占用内存太大,所以就不对动图进行解码了。关于GIF动图的支持在FLAnimatedImageView分类中。

2. 有alpha信息的图片

(a) 有alpha信息的图片的判断

首先我们需要判断的就是什么图片包含alpha信息,苹果提供了支持。

/* Return the alpha info of `image'. */

CG_EXTERN CGImageAlphaInfo CGImageGetAlphaInfo(CGImageRef cg_nullable image)
    CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
CGImageRef imageRef = image.CGImage;
    
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                     alpha == kCGImageAlphaLast ||
                     alpha == kCGImageAlphaPremultipliedFirst ||
                     alpha == kCGImageAlphaPremultipliedLast);

这里需要说明的就是CGImageAlphaInfo,这个是什么鬼?

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
    kCGImageAlphaNone,               /* For example, RGB. */
    kCGImageAlphaPremultipliedLast,  /* For example, premultiplied RGBA */
    kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
    kCGImageAlphaLast,               /* For example, non-premultiplied RGBA */
    kCGImageAlphaFirst,              /* For example, non-premultiplied ARGB */
    kCGImageAlphaNoneSkipLast,       /* For example, RBGX. */
    kCGImageAlphaNoneSkipFirst,      /* For example, XRGB. */
    kCGImageAlphaOnly                /* No color data, alpha data only */
};

这是CoreGraphic框架中CGImage.h中的一个枚举。


图像的解码实现

SDWebImage中有关于图像解码的部分,我们先看一下实现方式,然后进行详细的解析。

+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    
    // 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];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        size_t bytesPerRow = kBytesPerPixel * width;

        // 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.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
    // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
    if (image == nil) {
        return NO;
    }

    // do not decode animated images
    if (image.images != nil) {
        return NO;
    }
    
    CGImageRef imageRef = image.CGImage;
    
    CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
    BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                     alpha == kCGImageAlphaLast ||
                     alpha == kCGImageAlphaPremultipliedFirst ||
                     alpha == kCGImageAlphaPremultipliedLast);
    // do not decode images with alpha
    if (anyAlpha) {
        return NO;
    }
    
    return YES;
}
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
    // current
    CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
    CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
    
    BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                  imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                  imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                  imageColorSpaceModel == kCGColorSpaceModelIndexed);
    if (unsupportedColorSpace) {
        colorspaceRef = CGColorSpaceCreateDeviceRGB();
        CFAutorelease(colorspaceRef);
    }
    return colorspaceRef;
}

这里做的就是首先判断什么样的图像可以解码,是一个类方法,如果不能就直接返回传入的image参数,如果可以解码就对图像进行解码后返回。

这里有几个点需要说明:

(a) CGBitmapContextCreate上下文的创建

kCGImageAlphaNoneCGBitmapContextCreate中不受支持。由于此处的原始图像没有alpha信息,因此请使用kCGImageAlphaNoneSkipLast创建不带alpha信息的位图图形上下文。

这里我们看一下,这个上下文创建的API

/* Create a bitmap context. The context draws into a bitmap which is `width'
   pixels wide and `height' pixels high. The number of components for each
   pixel is specified by `space', which may also specify a destination color
   profile. The number of bits for each component of a pixel is specified by
   `bitsPerComponent'. The number of bytes per pixel is equal to
   `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
   consists of `bytesPerRow' bytes, which must be at least `width * bytes
   per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
   of the number of bytes per pixel. `data', if non-NULL, points to a block
   of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
   data for context is allocated automatically and freed when the context is
   deallocated. `bitmapInfo' specifies whether the bitmap should contain an
   alpha channel and how it's to be generated, along with whether the
   components are floating-point or integer. */

CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
    CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

创建一个位图上下文。 上下文绘制成宽度为width像素和height像素为高的位图。 每个像素的组件数量由space指定,它也可以指定目标颜色配置文件。 像素的每个分量的位数由bitsPerComponent指定。 每个像素的字节数等于(bitsPerComponent *components+7)/ 8。 位图的每一行都由bytesPerRow字节组成,它们必须至少为每个像素的字节数 * 宽度width。另外,bytesPerRow必须是每个像素字节数的整数倍。 data,如果非NULL,则至少指向bytesPerRow * height字节的内存块。 如果data为NULL,上下文数据将自动分配,并在释放上下文时释放。 bitmapInfo指定位图是否应该包含一个alpha通道以及如何生成,以及组件是浮点还是整数。

(b) 位图信息CGBitmapInfo

最后一个参数,关于CGBitmapInfo的一个枚举。

typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
    kCGBitmapAlphaInfoMask = 0x1F,

    kCGBitmapFloatInfoMask = 0xF00,
    kCGBitmapFloatComponents = (1 << 8),

    kCGBitmapByteOrderMask     = kCGImageByteOrderMask,
    kCGBitmapByteOrderDefault  = (0 << 12),
    kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
    kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
    kCGBitmapByteOrder16Big    = kCGImageByteOrder16Big,
    kCGBitmapByteOrder32Big    = kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

这里存放的就是位图组成部分的信息。

  • kCGBitmapAlphaInfoMask
    • Aplha通道信息遮罩。用这个值来提取alpha信息。这个值明确了位图是否包含了alpha通道和alpha通道是如何生成的。
  • kCGBitmapFloatComponents
    • 位图数据都是浮点值。
  • kCGBitmapByteOrderMask
    • 像素格式的字节顺序
  • kCGBitmapByteOrderDefault
    • 默认的字节顺序
  • kCGBitmapByteOrder16Little
    • 16位小端格式
  • kCGBitmapByteOrder32Little
    • 32位小端格式
  • kCGBitmapByteOrder16Big
    • 16位大端格式
  • kCGBitmapByteOrder32Big
    • 32位大端格式

(c) 生成图像颜色空间CGColorSpaceRef

// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                              imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                              imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                              imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
    colorspaceRef = CGColorSpaceCreateDeviceRGB();
    CFAutorelease(colorspaceRef);
}
return colorspaceRef;

这里,CGColorSpaceRef表示需要使用的色彩标准(为创建CGColor做准备)例如RBG:CGColorSpaceCreateDeviceRGB

Quartz支持由颜色管理系统使用的与设备无关的颜色空间的标准颜色空间,并且还支持通用,索引和模式颜色空间。 设备无关的颜色空间以在设备之间可移植的方式表示颜色。 它们用于将颜色数据从一个设备的本地颜色空间到另一个设备的本地颜色空间的交换。 在不同设备上显示时,设备无关颜色空间中的颜色显示相同,只要设备的功能允许。 因此,与设备无关的颜色空间是表示颜色的最佳选择。

具有精确颜色要求的应用程序应始终使用与设备无关的颜色空间。 常见的设备无关颜色空间是通用颜色空间。 通用颜色空间使操作系统为您的应用程序提供最佳的颜色空间。 绘图到显示看起来像打印相同的内容到打印机。

重点:iOS不支持设备无关或通用颜色空间。 iOS应用程序必须使用设备颜色空间。

下面看一下CGColorSpaceModel,颜色空间模型。

/* The model of a color space. */

typedef CF_ENUM (int32_t,  CGColorSpaceModel) {
    kCGColorSpaceModelUnknown = -1,
    kCGColorSpaceModelMonochrome,
    kCGColorSpaceModelRGB,
    kCGColorSpaceModelCMYK,
    kCGColorSpaceModelLab,
    kCGColorSpaceModelDeviceN,
    kCGColorSpaceModelIndexed,
    kCGColorSpaceModelPattern
};

参考文章

1. SDWebImageDecoder引发的思考
2. CGBitmapInfo
3. 【CoreGraphics】CGColorSpace - 色彩空间
4. Color and Color Spaces

后记

本篇文章主要解析了图像解码相关,包括为什么要进行图像解码、是否可以解码的判断以及解码的代码实现等几个问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容