图片加载的工作流
- 1.假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 2.然后将生成的 UIImage 赋值给 UIImageView ;
- 3.接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 4.在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
a.分配内存缓冲区用于管理文件 IO 和解压缩操作;
b.将文件数据从磁盘读到内存中;
c.将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
d.最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层
在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
为什么需要解压缩
位图:位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们应用中经常用的png和jpeg就是位图,都是一种压缩的位图图形格式。
解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4
在磁盘中的图片渲染到屏幕之前,必须得到原始的图片像素数据,这就是为什么要对图片进行解压缩操作。
强制解压缩的原理
/* 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);
看看 CGBitmapContextCreate
函数中每个参数所代表的具体含义:
- data :如果不为 NULL,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL
,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可; - width 和 height:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
- bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
- bytesPerRow:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? ,亲测可用;
- space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
- bitmapInfo :就是我们前面提到的位图的布局信息。
开源库的实现
首先,我们来看看 YYKit 中的相关代码,用于解压缩图片的函数 YYCGImageCreateDecodedCopy
存在于 YYImageCoder 类中,核心代码如下:
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
...
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;
//使用 CGBitmapContextCreate 函数创建一个位图上下文
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
//使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
//使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;
} else {
...
}
}