iOS中的图片解码

我们从网络下载或者从本地磁盘加载一张图片到屏幕上显示,要经过图片的解码过程,为什么呢?因为我们一般的图片格式例如 JPEGPNG都是经过压缩后的图片,而显示在屏幕上的图片叫做位图(bitmap),所谓的解码就是把压缩后的图片变成位图。

为什么非要解码成位图才能显示呢?因为位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,就可以在屏幕上渲染整张图片了。

那我们为什么还需要不同格式的各种图片呢?直接全部用位图不就好了?那不就不需要每次解码了?那些JPEG以及PNG等其实都是图像的压缩格式,我们都知道压缩的意思就是减小空间,所以我们可以想到,使用这些格式的原因就是位图实在太大了。举个例子来说,一张位图的宽和高分别都是100个像素,那这个位图的大小是多少呢?我们可以按照下面的公式计算

1
size = width * height * bytesPerPixel

一般 bytesPerPixel = 4,表示每个像素大小为4B,这个下面再具体介绍。

那么把我们的位图代入该公式,可以得到其大小

1
size = 100 * 100 * 4 = 40000B = 39KB

然而这样一张 100*100 的图片,如果是 PNG 或者 JPEG 格式,可能只有几KB。可以想象如果图片的大小更大一点,直接将位图的所有像素信息保存为一张图片的话,将会占据多少的存储空间。所以我们需要 PNG 或者 JPEG 这样的压缩格式图片。PNG 是无所压损,JPEG 可以是有损压缩,即损失部分信息来压缩图片,这样压缩之后的图片大小将会更小。

iOS中图片解码

在 iOS 中,假如我们在本地沙盒下有一张 JPEG 格式的图片,我们想要将它显示在屏幕上,一般可以这么做

1
2
3
UIImageView *imageView = ...;
UIImage *image = [UIImage imageWithContentsOfFile:@"/.../.../path.JPG"];
imageView.image = image;

我们将图片从磁盘加载出来生成 UIImage 对象的时候,实际上图片还没有被解码,实际上完整的图片数据也没有被加载进来。而当 UIImage 对象被赋值给 UIImageView 的 image 属性之后,在图像被真正渲染到屏幕上之前,该图片才会被解码,这里面包括将完整的图片数据从磁盘加载到内存中,然后将数据解码成位图的操作。

这个解码操作默认是发生在主线程上面的,而且非常消耗 CPU,所以我们可以想到如果在 tableView 或者 collectionView 中有相当多的图片需要显示的话,这些图片在主线程的解码操作必然会影响滑动的顺畅度。所以我们是否可以在子线程强制将其解码,然后在主线程让系统渲染解码之后的图片呢?当然可以,现在基本上所有的开源图片库都会实现这个操作。

提前解码图片

要提前解码图片,我们需要使用 CoreGraphics 框架,一般来说要使用下面的 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 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);

这个方法是创建一个图片处理的上下文 CGContext 对象,因为上面方法的返回值 CGContextRef 实际上就是 CGContext *

这个方法看起来很恐怖,首先我们来说一些概念:

  • bitsPerComponent:表示每一个成分有多少位组成,Component 就是指颜色分量,例如 RGB 中,指定 R/G/B 这些颜色分量由多少位来表示
  • bytesPerPixel:表示一个像素点有多少个字节组成,上面的方法注释中提到了一个公式 (bitsPerComponent * number of components + 7)/8,即一个像素点的字节数量与表示当前图像的颜色的颜色分量数量和每个分量的位数有关
  • bytesPerRow:图像一行有多少字节,上面注释中也提到了,它一般是width * bytes per pixel,很好理解,也就是图像像素宽度与每个像素字节大小的乘积。所以我们可以想到最开始我们说的那个计算位图大小的公式,只要 bytesPerRow 再乘上图像的像素高度 height 即可。
  • space:即颜色空间,我们平常一直说的 RGB 就是一种颜色空间,另外还有 CMYK 也都是颜色空间,每个像素的信息必须要在一个颜色空间中才有意义。也就是说,我们通过颜色空间告诉系统一个像素上面的颜色信息都是什么意思。显然同样的一个位图每一个像素的信息,在 RGB 颜色中间中表示的意思与在 CMYK 中是不同的,最后渲染出来的图像一定是不一样的。

我们的手机一般支持 RGB 颜色空间,其 bitsPerComponent = 8。那 bytesPerPixel 是多少呢,根据上面的公式,我们首先要知道 RGB 中有多少颜色分量呢?我们可能认为是 3,因为红、绿、蓝就是 3 个分量,实际上一般是4?因为 RGB 颜色空间中,其实还要包含一个 alpha 通道,也就是透明度。所以实际上应该是 ARGB 或者 RGBA,两者的区别就是 alpha 通道在哪里表示,这个下面还会再说。当然也可能是3,那就说明没有 alpha 通道。现在就是知道了 RGB 颜色空间中,实际上是有 4 个分量,所以我们可以算出 bytesPerPixel = (8 * 4 + 7)/8 = 4B。所以我们现在知道为什么最开始的时候我们计算位图的大小的时候,每个像素的大小我们使用的值是 4B 了。事实上不同的颜色空间下,上面这些值都是不同的,但是一般在手机上,我们使用 RGB 颜色空间,所以差不多就是上面的值。

现在我们回到上面那个方法的参数上面:
第一个参数data,根据注释,我们可以赋值为 NULL,这样系统会自动帮我们分配和释放相应大小的空间。
第二个参数和第三个参数即上下文的宽和高,因为我们根据原始的图片来创建上下文进行渲染,所以这两个值就是原始图片的宽和高。
第四个参数bitsPerComponent我们知道了就是每个成分由多少位组成,因为我们默认实在 RGB 颜色空间中,所以一般都是传入 8。
第五个参数bytesPerRow,这个我之前也专门介绍过了,就是像素宽度与每个像素大小的乘积,因为这些值都是知道的,我们可以这么传入。当然这里我们还可以直接传入0,这样系统会帮我们进行计算,而且这样做系统还会帮我们做一些优化,这样更好。
第六个参数space即颜色空间,我们可以直接使用RGB,直接使用系统提供的 API 获取: CGColorSpaceCreateDeviceRGB()
第七个参数bitmapInfo表示位图的布局信息。这个参数实际上系统提供了一个枚举值:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

从上面我们可以看到它主要指定了 alpha 通道的布局信息、浮点分量的信息、以及字节存储布局信息。

先说 alpha 通道布局信息,实际上也有一个枚举值:

1
2
3
4
5
6
7
8
9
10
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 */
};

上面的注释其实写很清楚,如果没有 alhpa 分量,那就是 kCGImageAlphaNone。带有 skip 的两个 kCGImageAlphaNoneSkipLastkCGImageAlphaNoneSkipFirst即有 alpha 分量,但是忽略该值,相当于透明度不起作用。kCGImageAlphaOnly只有 alpha 值,没有颜色值。另外 4 个都表示带有 alpha 通道。带有 Premultiplied,说明在图片解码压缩的时候,就将 alpha 通道的值分别乘到了颜色分量上,我们知道 alpha 就会影响颜色的透明度,我们如果在压缩的时候就将这步做掉了,那么渲染的时候就不必再处理 alpha 通道了,这样可以提高渲染速度。FirstLast的区别就是 alpha 分量是在像素存储的哪一边。例如一个像素点32位,表示4个分量,那么从左到右,如果是 ARGB,就表示 alpha 分量在 first,RGBA 就表示 alpha 分量在 last。

另外一个比较重要的就是要制定以下字节存储的布局信息,也就是指定一下存储位数,是大端存储还是小端存储。它也有一个枚举值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
kCGImageByteOrderMask = 0x7000,
kCGImageByteOrder16Little = (1 << 12),
kCGImageByteOrder32Little = (2 << 12),
kCGImageByteOrder16Big = (3 << 12),
kCGImageByteOrder32Big = (4 << 12)
} CG_AVAILABLE_STARTING(__MAC_10_12, __IPHONE_10_0);

#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif

iOS 系统使用的是小端存储,所以我们可以使用 kCGImageByteOrder32Little,但是系统给我们提供了一个宏,可以让我们不用考虑大小端问题,所以我们用 kCGBitmapByteOrder32Host 将会更好。

上面介绍了创建上下文的方法,但它只是创建了一个上下文,要解码,还需要使用该上下文,调用一下 draw 方法,draw 了之后,CPU 就会帮我们将其解码出来,然后我们就从以这个上下文中获取到解码后的位图了。只要我们将这个操作放在后台其他线程操作,获取到位图之后再在主线程渲染显示,那么就不会因为解码操作影响滑动性能了。

之前说了基本上图片的第三方开源库都实现了提前解码的代码,所以下面具体的代码还是看那些有名的第三方库好了。我专门去看了 YYImageSDWebImageFLAnimatedImageKingfisher 关于图片解码的部分,发现前三者基本都差不多,只是可能参数上面不太一样,最后一个没有使用上面创建上下文的方法,下面会说到。

YYImage

下面是 YYImage 中解码的代码:

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
47
48
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
if (!imageRef) return NULL;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return NULL;

if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;

} else {
CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
if (bytesPerRow == 0 || width == 0 || height == 0) return NULL;

CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
if (!dataProvider) return NULL;
CFDataRef data = CGDataProviderCopyData(dataProvider); // decode
if (!data) return NULL;

CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
if (!newProvider) return NULL;

CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
CFRelease(newProvider);
return newImage;
}
}

实际上, 这个方法的作用是创建一个图像的拷贝,我们的解码操作,其实也是创建了一个原始图像的拷贝,只不过解码后拷贝出来的就是位图。上面中间那个 if 判断就是判断是否需要解码,如果不需要,那么根据原始的图片重新创建一个拷贝,这个拷贝是未经解码的,所以我们不需要看 else 那部分代码,我们只要看满足 if 条件的代码即可。

YYImage 中关于 alpha 分量的处理:

1
2
3
4
5
6
7
8
9
10
11
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

如果原始图片有 alpha 分量信息,那么统一将其设置为 kCGImageAlphaPremultipliedFirst,如果没有,就设置为 kCGImageAlphaNoneSkipFirst

关于颜色空间,YY 的作者封装了一个方法 YYCGColorSpaceGetDeviceRGB()

1
2
3
4
5
6
7
8
CGColorSpaceRef YYCGColorSpaceGetDeviceRGB() {
static CGColorSpaceRef space;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
space = CGColorSpaceCreateDeviceRGB();
});
return space;
}

可以看到里面使用 dispatch_once 来获取,使得创建该颜色空间的操作只会执行一次,大概这样不用每次都创建,可以提高性能吧。

SDWebImage

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
+ (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;
}

SDWebImage 中和其他不一样的地方,就是如果一张图片有 alpha 分量,那就直接返回原始图片,不再进行解码操作。我猜测作者这样写,是不是因为觉得对于有 alpha 分量的图片,因为上下文创建中那些参数的缘故,解码之后可能会与原始图像有偏差,干脆就不解码了。

SDWebImage 在解码操作外面包了 autoreleasepool,这样在大量图片需要解码的时候,可以使得局部变量尽早释放掉,不会造成内存峰值过高。其他创建上下文,然后调用 CGContextDrawImage,再调用CGBitmapContextCreateImage获取创建后的位图,和 YYImage 基本一样,就是个别参数设置不同。

FLAnimatedImage

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
{
// Always use a device RGB color space for simplicity and predictability what will be going on.
CGColorSpaceRef colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
// Early return on failure!
if (!colorSpaceDeviceRGBRef) {
FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
return imageToPredraw;
}

// Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
// kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
// (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A

// "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
void *data = NULL;
size_t width = imageToPredraw.size.width;
size_t height = imageToPredraw.size.height;
size_t bitsPerComponent = CHAR_BIT;

size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
size_t bytesPerRow = (bytesPerPixel * width);

CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;

CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
alphaInfo = kCGImageAlphaNoneSkipFirst;
} else if (alphaInfo == kCGImageAlphaFirst) {
alphaInfo = kCGImageAlphaPremultipliedFirst;
} else if (alphaInfo == kCGImageAlphaLast) {
alphaInfo = kCGImageAlphaPremultipliedLast;
}
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
bitmapInfo |= alphaInfo;

// Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
// Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
CGContextRef bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
CGColorSpaceRelease(colorSpaceDeviceRGBRef);
// Early return on failure!
if (!bitmapContextRef) {
FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
return imageToPredraw;
}

// Draw image in bitmap context and create image by preserving receiver's properties.
CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
CGImageRef predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
UIImage *predrawnImage = [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation];
CGImageRelease(predrawnImageRef);
CGContextRelease(bitmapContextRef);

// Early return on failure!
if (!predrawnImage) {
FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
return imageToPredraw;
}

return predrawnImage;
}

FLAminatedImage 的代码包含了超详细的注释,看它的源码会很舒服,不会像看其他有些代码时不时的诧异作者为什么要这么写。FLAminatedImage 的作者经常会告诉我们它这边这么写的原因是什么,有什么好处,真是给我们这些菜鸟莫大的帮助。

FLAnimatedImage 的这部分处理和上面两个也差不多,无非也是一部分参数不一样,它是支持 alpha 分量的图片你的,但它对于 alpha 分量的布局和 YYImage 还是有点不一样:

1
2
3
4
5
6
7
8
9
10
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
alphaInfo = kCGImageAlphaNoneSkipFirst;
} else if (alphaInfo == kCGImageAlphaFirst) {
alphaInfo = kCGImageAlphaPremultipliedFirst;
} else if (alphaInfo == kCGImageAlphaLast) {
alphaInfo = kCGImageAlphaPremultipliedLast;
}

如果没有 alpha 分量,那么统一设置为 kCGImageAlphaNoneSkipFirst,而如果有 alpha 分量,那么不改变 alpha 分量的存储位置,而是将没有提前处理的 alpha 分量改为在解码的时候提前计算好,即统一改成了带有Premultiplied的那些选项,提高渲染时候的性能。

FLAnimatedImage 注释很详细,所以它在 CGBitmapContextCreate 方法调用上面写了一段注释:

1
2
3
4
// Create our own graphics context to draw to; 
`UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context
but returns the current one which isn't thread-safe
(e.g. main thread could use it at the same time).

这段话解释了为什么我们需要这么麻烦搞定那么多参数来创建一个上下文环境,因为我们事实上是可以直接调用UIGraphicsGetCurrentContext 让系统来帮我们创建一个上下文环境的,我们在这里面 draw,也一样可以让 CPU 帮我们提前解码图片,获得位图的。作者认为直接在其他线程使用 UIGraphicsGetCurrentContext 并不是线程安全的。

但是关于这个,我专门去查了一番,发现很多人说从 iOS 4.0 之后 UIGraphicsGetCurrentContext 也是线程安全的了。比如这里
在之前 YYImage 的那段代码中,作者其实也有一句注释,不知道大家注意到没:

1
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]

作者的意思应该是使用自己创建上下文的方法,与直接使用 UIGraphicsBeginImageContext(),然后再 draw 其实是一样的。

我之前说 Kingfisher 的解码代码与前面三个不一样,其实说的就是这个,Kingfisher 其实就是直接使用了 UIGraphicsBeginImageContext(),不过它使用的是带有 options 的版本。

Kingfisher

下面是 Kingfisher 中的解码解码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// MARK: - Decode
extension Kingfisher where Base: Image {
var decoded: Image? {
return decoded(scale: scale)
}

func decoded(scale: CGFloat) -> Image {
// prevent animated image (GIF) lose it's images
#if os(iOS)
if imageSource != nil { return base }
#else
if images != nil { return base }
#endif

guard let imageRef = self.cgImage else {
assertionFailure("[Kingfisher] Decoding only works for CG-based image.")
return base
}

guard let context = beginContext(size: CGSize(width: imageRef.width, height: imageRef.height)) else {
assertionFailure("[Kingfisher] Decoding fails to create a valid context.")
return base
}

defer { endContext() }

let rect = CGRect(x: 0, y: 0, width: imageRef.width, height: imageRef.height)
context.draw(imageRef, in: rect)
let decompressedImageRef = context.makeImage()
return Kingfisher<Image>.image(cgImage: decompressedImageRef!, scale: scale, refImage: base)
}
}

extension Kingfisher where Base: Image {

func beginContext(size: CGSize) -> CGContext? {
#if os(macOS)
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(size.width),
pixelsHigh: Int(size.height),
bitsPerSample: cgImage?.bitsPerComponent ?? 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0,
bitsPerPixel: 0) else
{
assertionFailure("[Kingfisher] Image representation cannot be created.")
return nil
}
rep.size = size
NSGraphicsContext.saveGraphicsState()
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
assertionFailure("[Kingfisher] Image contenxt cannot be created.")
return nil
}

NSGraphicsContext.setCurrent(context)
return context.cgContext
#else
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let context = UIGraphicsGetCurrentContext()
context?.scaleBy(x: 1.0, y: -1.0)
context?.translateBy(x: 0, y: -size.height)
return context
#endif
}

func endContext() {
#if os(macOS)
NSGraphicsContext.restoreGraphicsState()
#else
UIGraphicsEndImageContext()
#endif
}

.....

}

作者封装了获取上下文的代码,即beginContext,里面抛开兼容 mac 的代码,赫然就是直接调用系统的 UIGraphicsBeginImageContextWithOptions 方法,只不过因为坐标系的缘故,他在里面多做了一些翻转操作。

Swift 版本解码

虽然 Kingfiser 就是 Swfit 写的,但是他毕竟不是用的自己创建上下文的方式来解码图片,所以我这里给出一份使用 Swift 通过自己创建上下文来提前解码图片的代码。和 OC 很像,就是部分 API 不一样了。

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
47
48
49
extension UIImage {

/**
解码 UIImage 为 bitmap(位图)

- returns: UIImage 位图对象
*/
func decode() -> UIImage? {

// 获取 UIImage 对应的 CGImage 对象
guard let imageRef = self.cgImage else { return self }

// 获取 宽 和 高
let width = imageRef.width
let height = imageRef.height

guard width != 0 || height != 0 else { return self }

// 使用设备的颜色空间 RGB
let colorSpace = CGColorSpaceCreateDeviceRGB()

// 判断是否有 alpha 通道
let alphaInfo = imageRef.alphaInfo
var hasAlpha = false
if alphaInfo == .premultipliedLast ||
alphaInfo == .premultipliedFirst ||
alphaInfo == .last ||
alphaInfo == .first {
hasAlpha = true
}

// 位图布局信息
var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue
bitmapInfo |= hasAlpha ? CGImageAlphaInfo.premultipliedFirst.rawValue : CGImageAlphaInfo.noneSkipFirst.rawValue

// 创建 context
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo) else { return self }

context.draw(imageRef, in: CGRect(x: 0.0, y: 0.0, width: CGFloat(width), height: CGFloat(height)))

// 获取解码后的 CGImage 对象
guard let docededImageRef = context.makeImage() else { return self }

// 返回解码后的 UIImage 对象
return UIImage(cgImage: docededImageRef, scale: self.scale, orientation: self.imageOrientation)

}

}

我对于 alpha 分量的处理与 YYKit 一样,之所以这样做,因为我看了一下苹果官方对于UIGraphicsBeginImageContextWithOptions(_:_:_:)描述文档

Discussion
You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is true, the alpha channel is ignored and the bitmap is treated as fully opaque (noneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (premultipliedFirst | kCGBitmapByteOrder32Host).

这里也是直接使用 premultipliedFirst 或者 noneSkipFirst

另外 Swift 中我没有找到 kCGBitmapByteOrder32Host 这个宏,所以只能直接使用 kCGBitmapByteOrder32Little 了。