文章很大一部分参考了这篇博文雷纯锋-谈谈 iOS 中图片的解压缩,感兴趣的可以直接看
我们一般在界面上显示一张图片都先生成UIImage
然后将其赋给UIImageView
,再设置ImageView
的坐标和大小就可以,如下:
UIImage *originalImage = [UIImage imageNamed:@"logo"];
UIImageView *orignalImageView = [[UIImageView alloc]initWithImage:originalImage];
然而,实际上系统帮我们做了很多事,接下来我们浅显的探讨下系统做了哪些工作
理解图片格式
一般我们用的图片大致为.png
、.jpg
和webp
几种格式,这些格式其实只是对图片的压缩,你可以理解成一直图片在电脑里面是个二进制数据的集合,但是这个集合很大也不方便给大众传播,所以人们就想了个办法,把数据进行压缩,这样就有了无损压缩和有损压缩,也就对应生成了各种格式
换一种理解方式就是,我们在传输某个文件的时候,往往会把文件压缩成.zip
,.rar
这些格式,图片的压缩其实和这个同理
这样,我们在显示图片的时候,要先把对应的图片解压缩,好比打开.zip
的文件时,要先把对应的文件解压到某个文件夹一样,那么在iOS系统里面,是什么时候进行解压的呢?
图片加载的流程
概括来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流如下:
- 假设我们使用
+imageWithContentsOfFile:
方法从磁盘中加载一张图片,这个时候的图片并没有解压缩; - 然后将生成的
UIImage
赋值给UIImageView
; - 接着一个隐式的
CATransaction
捕获到了UIImageView
图层树的变化; - 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 使用未压缩的位图数据渲染
UIImageView
的图层。
在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
位图
图片被解压缩后就变成了位图。什么是位图呢?
A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.
从上面的英文中可以看到,位图其实就是一个像素数组,数组中的每一个像素就代表了图片上的一个点
上面这个图片我们看文件简介是:2859B,尺寸是60*60
我们使用下面的代码:
UIImage *image = [UIImage imageNamed:@"check_green"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
NSData *myData = (NSData *)CFBridgingRelease(rawData);
NSLog(@"%ld",myData.length);
就可以获取到这个图片的原始像素数据,大小为 14400B
这样来看,我们图片的原始大小为14400B,这个大小是怎么得到的呢?
事实上,解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:
解压缩后的图片大小 = 图片的像素宽 60 * 图片的像素高 60 * 每个像素所占的字节数 4
所以,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比
强制解压缩的原理
既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响我们应用的响应性,那么是否有比较好的解决方案呢?答案是肯定的。
我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。
而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate
:
/* 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 像素的位图。这个函数的注释比较长,参数也比较难理解,但是先别着急,我们先来了解下相关的知识,然后再回过头来理解这些参数,就会比较简单了。
-
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
:位图的布局信息。
SDWebImage的实现方式
+ (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];
if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
return nil;
}
@autoreleasepool{
// do not decode animated images
if (image.images != nil) {
return image;
}
CGImageRef imageRef = image.CGImage;
//在渲染的时候,把图层按像素叠加, 并且会对每一个像素进行 RGBA 的叠加计算。当某个 layer 的是不透明的,GPU 可以直接忽略掉其下方的图层,这就减少了很多工作量。这也是调用 CGBitmapContextCreate 时 bitmapInfo 参数设置为忽略掉 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);
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.
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
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);
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(imageRefWithoutAlpha));
NSData *myData = (NSData *)CFBridgingRelease(rawData);
NSLog(@"originalImage --- %ld",myData.length);
return imageWithoutAlpha;
}
}
- 使用
CGBitmapContextCreate
函数创建一个位图上下文; - 使用
CGContextDrawImage
函数将原始位图绘制到上下文中; - 使用
CGBitmapContextCreateImage
函数创建一张新的解压缩后的位图; - 最后又创建了一个
UIImage
对象
SDWebImage
中有个不一样的地方在于,如果他检查到你有透明通道,他就会直接返回原图片,而不会进行解压了
下面我们看看其他开源库里面解压缩使用的参数
在上表中,用浅绿色背景标记的参数即为我们在前面的分析中所推荐的参数,用这些参数解压缩后的图片渲染的速度会更快。因此,从理论上说 YYKit 中的解压缩算法是三者之中最优的
最后说一点
UIImage *originalImage = [UIImage imageNamed:@"logo"];
NSLog(@"originalImage --- %ld",UIImageJPEGRepresentation(originalImage, 0).length);
使用上面这种方式来看图片的文件大小很不准确,大家可以试试,如果有好的查看文件大小的方法,可以留言一起探讨下,还是建议大家看我文章开头的那篇文章,比我这篇写的详细