【iOS】图片解码

1,549 阅读5分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

在iOS中,大多数的APP都有不可或缺的图片资源,同时也很容易因为对图片的处理不恰当造成性能低下,不要让图片成为你的APP的性能杀手。

一张图片从磁盘中加载出来,同时显示到屏幕上,经过了一系列的复杂处理,其中包括了对图片的解码

图片显示过程

平时我们对于图片的显示,一般都是使用以下的代码:

UIImage *image = [UIImage imageNamed:@"icon"]; self.imageView.image = image; 

简单的两行代码其实里面包含了下面几步:

  1. 首先会调用 image/io从磁盘中加载一张图片,这个时候,图片还没有解码。
  2. 将image复制给imageView的image
  3. 然后一个隐式的CATransaction捕获到图层树的变化
  4. 在主线程runloop下一次迭代到来的时候,Core Animation会提交这个隐式transaction,这个过程会对图片进行copy操作,根据图片的不同,可能会涉及以下的一些甚至全部的步骤。
I. 为文件管理IO和解压缩操作分配内存缓存区域
II. 从磁盘中读取数据到内存中
III. 将压缩的图片数据解码成未压缩的图片数据,这通常是一个非常频繁耗时的CPU操作
IV. CoreAnimation将未压缩位图数据渲染到layer上。

从上面的步骤可以看出,图片的解码主是主要耗时的原因,如果一个APP中只有几张图片是这样设置当然是没问题的,可是如果在一个TableView中有大量的图片在滚动,如果这个时候在不断的解码显示那必然会导致界面卡顿。

为什么需要解码

实际上我们使用的JPEG或者PNG格式的图片,都是一种经过压缩的位图图形格式,只不过PNG是无损压缩并且支持alpha通道,而JEPG是有损压缩,并且可以指定压缩比。下面是iOS中提供的获得上述格式图片的方法:

UIKIT_EXTERN  NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image); 
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format

UIKIT_EXTERN  NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);  
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least) 

接下来我们就要了解一下位图:

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.

其实位图就是一个像素数组,每个像素都代表了图片中独立的一个点,每一个点其实又包含了以下内容:

Bits per component :一个像素中每个独立的颜色分量使用的 bit 数;
Bits per pixel :一个像素使用的总 bit 数;
Bytes per row :位图中的每一行使用的字节数。

这里不多说,具体的可以查看像素格式

我们知道位图是经过压缩后的,我们可以通过

UIImage *image = [UIImage imageNamed:@"icon"]; 
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage)); 

得到原始的数据大小,一般该数据的大小的计算方式是:

图片像素宽 图片像素高 每个像素所占的字节数4

所以图片当前的大小并不等于解码后的大小,所以我们才需要解码之后才能得到原始的数据大小,只有使用原始数据才能正确的显示出图片。

正确的解码姿势

上面已经知道了图片显示是会在主线程解压缩图片之后然后渲染到屏幕上,首先我们可以在子线程中做解码的操作,在子线程中重新绘制图片,得到解码后的图片,然后渲染到屏幕上。

我们先上代码:

- (void)decodeImage:(UIImage *)image completion:(void(^)(UIImage *image))completion{
    if (!image) return;
    //在子线程执行解码操作
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        CGImageRef imageRef = image.CGImage;
        //获取像素宽和像素高
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        if (width == 0 || height == 0) return ;
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        //判断颜色是否含有alpha通道
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        //在iOS中,使用的是小端模式,在mac中使用的是大端模式,为了兼容,我们使用kCGBitmapByteOrder32Host,32位字节顺序,该宏在不同的平台上面会自动组装换成不同的模式。
        /*
         #ifdef __BIG_ENDIAN__
         # define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
         # define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
         #else    //Little endian.
         # define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
         # define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
         #endif
         */
        
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        //根据是否含有alpha通道,如果有则使用kCGImageAlphaPremultipliedFirst,ARGB否则使用kCGImageAlphaNoneSkipFirst,RGB
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        //创建一个位图上下文
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0,  CGColorSpaceCreateDeviceRGB(), bitmapInfo);
        if (!context) return;
        //将原始图片绘制到上下文当中
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        //创建一张新的解压后的位图
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        UIImage *originImage =[UIImage imageWithCGImage:newImage scale:[UIScreen mainScreen].scale orientation:image.imageOrientation];
        //回到主线程回调
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(originImage);
        });
    });
}

性能对比

图片尺寸未解码直接渲染时间(ms)解码后渲染时间(ms)
128x96.jpg0.990.08
128x96.png0.800.08
256x192.jpg3.010.14
256x192.png2.300.17
512x384.jpg4.830.28
512x384.png6.030.28
1024x768.jpg13.831.43
1024x768.png18.311.12
2048x1536.jpg31.723.99
2048x1536.png75.055.16

通过上面的对比我们可以看出,解码后的图片的渲染速度远远高于未解码后的图片的渲染速度,并且由于我们在子线程中进行解码,所以也不会造成主线程的UI卡顿。