在移动开发中,我们经常会和多媒体数据打交道。这些数据的分析往往需要大量的资源,这是一个常见的性能瓶颈。本文着重介绍一种多媒体数据——图片,介绍了图片的常见格式、图片在移动平台上的传输、存储和显示方式,以及一种优化图片显示性能的方法:强制子线程解码。图片是如何在计算机世界中存储和表示的?图片和所有其他资源一样,在内存中本质上是0和1的二进制数据。计算机需要将这些原始内容渲染成人眼可以观察到的图片。相反,图片需要以合适的形式存储在内存中或通过网络传输。下面是一张图片在硬盘中的原始十六进制表示:这种将图片按照一定的规则进行二进制编码的方式就是图片的格式。常见的图片格式图片格式有很多,除了大家熟知的JPG、PNG、GIF,还有Webp、BMP、TIFF、CDR等几十种,应用于不同的场景或平台。这些格式可以分为两大类:有损压缩和无损压缩。有损压缩:与颜色相比,人眼对光亮度信息更为敏感。基于此,通过合并图片中的颜色信息,保留亮度信息,尽可能在不影响图片观感的情况下,减少存储量。顾名思义,此类压缩图像将永久丢失一些细节。最典型的有损压缩格式是jpg。无损压缩:与有损压缩不同,无损压缩不会丢失图像细节。它减小图像尺寸的方式是通过索引为图像中的不同颜色特征建立索引表,减少重复的颜色数据,从而达到压缩的效果。常见的无损压缩格式有png、gif。除了上面提到的格式外,还有必要简单介绍下webp和bitmap这两种格式:Webp:jpg作为主流的网络图片标准,可以追溯到1990年代初期,已经很古老了。因此,谷歌推出了Webp标准来替代旧的jpg,以加快网络图片的加载速度,提高图片压缩质量。webp同时支持有损和无损压缩,压缩率也很高。经过无损压缩后,webp在体积上比png小45%。同样质量的webp和jpg,前者还可以节省一半的流量。同时,webp还支持动画,可谓图像压缩格式的集大成者。webp的缺点是浏览器和移动端支持不是很完善,需要引入谷歌的libwebp框架,编码和解码会消耗相对较多的资源。位图:位图也叫位图文件,是*非压缩*的图像格式,所以体积非常大。所谓非压缩是指图片每个像素的原始信息在内存中按顺序排列,典型的1920*1080像素位图图片,每个像素用RGBA四个字节表示,那么它的体积就是1920*1080*4=1012.5kb。由于位图只是简单的顺序存储了图片的像素信息,所以不需要解码就可以直接渲染到UI上。事实上,其他格式的图片一般都需要先解码成bitmap再渲染到界面上。如何确定图像的格式?在某些场景下,我们需要手动确定图像数据的格式以进行不同的处理。一般来说,只要得到原始的二进制数据,就可以根据不同压缩格式的编码特点进行简单的分类。下面是一些图像帧的常见实现,可以复制使用:+(XRImageFormat)imageFormatForImageData:(nullableNSData*)data{if(!data){returnXRImageFormatUndefined;}uint8_tc;[datagetBytes:&clength:1];switch(c){case0xFF:returnXRImageFormatJPEG;case0x89:returnXRImageFormatPNG;case0x47:returnXRImageFormatGIF;case0x49:case0x4D:returnXRImageFormatTIFF;case0x52:if(data.length<12){returnXRImageFormatUndefined;}NSString*testString=[[NSStringalloc]initWithRange:[datasubdataWithRange:[数据子数据范围0,12)]encoding:NSASCIIStringEncoding];if([testStringhasPrefix:@"RIFF"]&&[testStringhasSuffix:@"WEBP"]){returnXRImageFormatWebP;}}returnXRImageFormatUndefined;}UIImageView的性能瓶颈如前所述,大多数格式的图片,需要先解码成位图,才能渲染到UI上。UIImageView显示图片,流程类似。事实上,一张图片从存入文件系统到显示在UIImageView上,会经历以下几个步骤:分配内存缓冲区和其他资源。从磁盘复制数据到内核缓冲区从内核缓冲区复制数据到用户空间生成UIImageView,将图像数据赋值给UIImageView,将压缩后的图像数据解码成位图数据(bitmap),如果数据不是字节对齐的,CoreAnimation的数据就会再次复制以进行字节对齐。CATransaction捕获UIImageView层树的变化,主线程Runloop提交CATransaction开始图像渲染。GPU处理用于渲染的位图数据。由于UIKit的封装,这些细节并没有直接暴露给开发者。实际上,当我们调用[UIImageimageNamed:@"xxx"]时,未解码的图片存储在UIImage中,调用[UIImageViewsetImage:image]后,图片会被解码显示在主线程的UI上,在这次,解码后的位图数据存储在UIImage中。图片的解压是一个非常消耗CPU资源的工作。如果我们有大量的图片需要在列表中展示,会大大减慢系统的响应速度,降低运行帧率。这是UIImageView的性能瓶颈。解决性能瓶颈:强制解码如果解码后的数据存储在UIImage中,速度会快很多,所以优化的思路是:在子线程中强制解码图片的原始数据,然后抛出解码后的图片返回给主线程继续使用,从而提高主线程的响应速度。我们需要用到的工具是CoreGraphics框架的CGBitmapContextCreate方法和相关的绘图函数。总体步骤是:A.创建一个具有指定大小和格式的位图上下文。B.将未解码的图片写入此上下文,此过程包括*强制解码*。C.从此上下文创建一个新的UIImage对象并返回。下面是SDWebImage实现的核心代码,对应的个数分析如下://1.CGImageRefimageRef=image.CGImage;//2.CGColorSpaceRefcolorspaceRef=[UIImagecolorSpaceForImageRef:imageRef];size_twidth=CGImageGetWidth(imageRef);size_height=CGImageGetHeight(imageRef);//3.size_tbytesPerRow=4*width;//4.CGContextRefcontext=CGBitmapContextCreate(NULL,width,height,kBitsPerComponent,bytesPerRow,colorspaceRef,kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);if(context==NULL){返回/5image;}/.CGContextDrawImage(context,CGRectMake(0,0,width,height),imageRef);//6.CGImageRefnewImageRef=CGBitmapContextCreateImage(context);//7.UIImage*newImage=[UIImageimageWithCGImage:newImageRefscale:image.scaleorientation:image.imageOrientation];CGContextRelease(context);CGImageRelease(newImageRef);returnnewImage;上面代码分析:1.从UIImage对象中获取CGImageRef的引用。这两种结构是苹果在不同层次上表达图像的方式。UIImage属于UIKit,是UI级图片的抽象,用于图片展示;CGImageRef是QuartzCore中的一个结构指针,用C语言编写,用于创建像素位图,它允许您通过操作存储的像素位来编辑图像。这两个结构可以很容易地相互转换://CGImageRef转换为UIImageCGImageRefimageRef=CGBitmapContextCreateImage(context);UIImage*image=[UIImageimageWithCGImage:imageRef];//UIImage转换为CGImageRefUIImage*image=[UIImageimageNamed:@"xxx"];CGImageRefimageRef=loadImage.CGImage;2、调用UIImage的+colorSpaceForImageRef:方法获取原图的色彩空间参数。什么是色彩空间?它是解释相同颜色值的方法。例如,一个像素的数据是(FF0000FF),在RGBA色彩空间中,它会被解释为红色,在BGRA色彩空间中,它会被解释为蓝色。所以我们需要提取这个参数来保证解码前后图片的颜色空间是一致的。CoreGraphic支持的颜色空间类型:3.计算图像解码后每行需要的位数,由两个参数相乘得到:每行的像素数,宽度,需要的位数存储一个像素4。这里的4其实是由每张图片的像素格式和像素组合决定的。下表是苹果平台支持的像素组合方式。表中的bpp表示每个像素需要多少bits;bpc表示颜色的每个分量需要多少位。具体解释可以看下图:解码后的图片默认使用kCGImageAlphaNoneSkipLastRGB像素组合,没有alpha通道,32位,每个像素4个字节,前三个字节代表红绿蓝通道,最后一个丢弃的字节不被解释。4、最关键的函数:调用CGBitmapContextCreate()方法生成空白图像绘制上下文。我们传入上面的一些参数,并指定图像大小、色彩空间、像素排列等属性。5.调用CGContextDrawImage()方法将未解码的imageRef指针内容写入我们创建的context中。这一步完成了隐式解码工作。6.从context上下文创建一个新的imageRef,也就是解码后的图片。7、从imageRef中为UI层生成一个UIImage对象,同时指定图片的scale和orientation参数。scale是指渲染图像时需要压缩的倍数。为什么会有这个参数?因为苹果为了节省安装包的体积,允许开发者为同一张图片上传不同分辨率的版本,也就是我们熟悉的@2x、@3x后缀的图片。不同屏幕素质的设备会获得相应的资源。为了统一绘制图片,这些图片会设置自己的scale属性,比如@2x图片,scale值为2,虽然绘制宽高和1x图片一样,但是实际长度是width*规模。Orientation很好理解,就是图片的旋转属性,它告诉设备使用哪个方向作为图片默认的渲染方向。通过以上步骤,我们成功的在子线程中对图片进行了转码并回调给主线程,从而大大提高了图片的渲染效率。这也是主流应用和大量第三方库的最佳实践。总结一下本文的内容:图片在计算机世界中被压缩成不同的打包格式进行存储和传输。手机会将压缩后的图片解压成位图格式,在主线程中可以渲染。这个过程会消耗大量资源,影响App的性能。我们使用CoreGraphics的绘制方法,强制先在子线程中对UIImage进行转码,减轻主线程的负担,从而提高App的响应速度。与UIImageView类似,UIKit隐藏了很多技术细节,降低了开发者的学习门槛,但另一方面也限制了我们对一些底层技术的探索。文中提到的强制解码方式其实是CGBitmapContextCreate方式的“副作用”,是一种比较hacky的方式。这也是iOS平台的一个局限:苹果太封闭了。用户其实对软件性能(帧率、响应速度、崩溃率等)非常敏感。作为开发者,必须不断探索性能瓶颈背后的原理,并尝试解决它们。移动端开发的性能优化是无止境的。
