• 首页
        • 更多产品

          客户为中心的产品管理工具

          专业的软件研发项目管理工具

          简单易用的团队知识库管理

          可量化的研发效能度量工具

          测试用例维护与计划执行

          以团队为中心的协作沟通

          研发工作流自动化工具

          账号认证与安全管理工具

          Why PingCode
          为什么选择 PingCode ?

          6000+企业信赖之选,为研发团队降本增效

        • 行业解决方案
          先进制造(即将上线)
        • 解决方案1
        • 解决方案2
  • Jira替代方案
目录

主流图片加载库所使用的预解码究竟干了什么

为了解释这个过程具体的解决问题,需要至少了解苹果的系统解码器的工作流程。Image/IO库是苹果提供的,跨所有Apple平台的系统解码器,支持常见的各种图像格式(JPEG/PNG/TIFF/GIF/HEIF/BMP等)的编码和解码。

一、主流图片加载库所使用的预解码究竟干了什么

为什么需要这个过程,解决了什么问题

为了解释这个过程具体的解决问题,需要至少了解苹果的系统解码器的工作流程。

Image/IO和惰性解码

Image/IO库是苹果提供的,跨所有Apple平台的系统解码器,支持常见的各种图像格式(JPEG/PNG/TIFF/GIF/HEIF/BMP等)的编码和解码。同时,有丰富的接口来和诸如Core Graphics库协作。

常见的网络图像解码,由于拿到的是一个压缩格式,肯定需要想办法转换到对应的UIImage。UIImage可以分为CGImage-based和CIImage-based,后者相对开销大一些,主要是用作滤镜等处理,不推荐使用。所以基本上各种图片库解码,为了解码压缩格式,得到一个CGImage,都是用了Image/IO的这个API:

CGImageSourceCreateImageAtIndex

实际上,Image/IO,除了调用具体的解码器产生图像的Bitmap以外,为了和Core Graphics库协作,也直接以CGImage这个数据结构来传递,但是他采取了一种惰性解码的创建方式。因此这里首先要了解CGImage初始化的接口和对应的行为:

CGImageCreate

这里面其他参数都好理解,具体看一个provider参数,这里面需要传入一个CGDataProviderRef,它是一个关于描述怎么样去获取这个Bitmap Buffer二进制数据的结构。再来看看CGDataProvider的初始化方法,这时候发现它有多种初始化方式,决定了后面的行为。

  • CGDataProviderCreate

这个方法,允许接受一个CGDataProviderCallbacks参数,看说明,可以知道,这个callbacks是一系列函数指针回调,目的是提供一个sequential-access的访问模式,同时Data Buffer会被copy出去。同时,由于传入的是callbacks,可以做到不立即提供Data Buffer,而是在未来需要的时候再触发。

  • CGDataProviderCreateDirect

这个方法,类似于CGDataProviderCreate,但是注明了这个callbacks生成的Data Buffer不会被Copy,Core Graphics只会直接访问返回的Data Buffer指针,需要自己管理好内存。

  • CGDataProviderCreateWithData

这个方法,需要提供一个CFData,同时也不会Copy这个CFData。在Release的同时由Core Graphics自动释放CFData的内存,开发者不需要管理内存。

剩余的具体初始化方法可以看文档说明,总而言之,CGDataProvider提供了各种各样的访问模式,如直接访问,拷贝访问,惰性访问等。而现在问题就来了,前面说到,Image/IO创建CGImage的时候,也需要提供一个DataProvider来指明图像的Bitmap Buffer数据从哪里获取,它是具体用了什么方式呢?

答案是使用了一个私有APICGImageCreateWithImageProvider,经过查看,这个方式实际类似CGDataProviderCreateDirect,也就是通过一组callbacks,提供了一个直接访问,允许惰性提供Data Buffer的方式。换句话说,这也就意味着,Image/IO,其实采取的是一种惰性解码方式。解码器只预先扫描一遍压缩格式的容器,提取元信息,但是不产生最终的Bitmap Buffer,而是通过惰性回调的方式,才生成Bitmap Buffer。

换句话说,通过所有CGImageSourceCreateImageAtIndex这种API生成的CGImage,其实它的backing store(就是Bitmap)还没有立即创建,他只是一个包含了一些元信息的空壳Image。这个CGImage,在最终需要获取它的Bitmap Buffer的时候(即,通过相应的API,如CGDataProviderCopyData,CGDataProviderRetainBytePtr),才会触发最后的Bitmap Buffer的创建和内存分配。

Image/IO和Force Decode

理解到上面Image/IO的惰性解码行为,理解了上面一点,现在说明Force Decode所解决的问题。

众所周知,iOS应用的渲染模式,是完全基于Core Animation和CALayer的(macOS上可选,另说)。因此,当一个UIImageView需要把图片呈现到设备的屏幕上时候,其实它的Pipeline是这样的:

  1. 一次Runloop完结 ->
  2. Core Animation提交渲染树CA::render::commit ->
  3. 遍历所有Layer的contents ->
  4. UIImageView的contents是CGImage ->
  5. 拷贝CGImage的Bitmap Buffer到Surface(Metal或者OpenGL ES Texture)上 ->
  6. Surface(Metal或者OpenGL ES)渲染到硬件管线上

这个流程看起来没有什么问题,但是注意,Core Animation库自身,虽然支持异步线程渲染(在macOS上可以手动开启),但是UIKit的这套内建的pipeline,全部都是发生在主线程的。

因此,当一个CGImage,是采取了惰性解码(通过Image/IO生成出来的),那么将会在主线程触发先前提到的惰性解码callback(实际上Core Animation的调用,触发了一个CGDataProviderRetainBytePtr),这时候Image/IO的具体解码器,会根据先前的图像元信息,去分配内存,创建Bitmap Buffer,这一步骤也发生在主线程。

这个流程带来的问题在于,主线程过多的频繁操作,会造成渲染帧率的下降。实验可以看出,通过原生这一套流程,对于一个1000*1000的PNG图片,名列前茅次滚动帧率大概会降低5-6帧(iPhone 5S上当年有人的测试)。后续帧率不受影响,因为是惰性解码,解码完成后的Bitmap Buffer会复用。

所以,较早不知是哪个团队的人(可能是FastImageCache,不确定)发现,并提出了另一种方案:通过预先调用获取Bitmap,强制Image/IO产生的CGImage解码,这样到最终渲染的时候,主线程就不会触发任何额外操作,带来明显的帧率提升。后面的一系列图片库,都互相效仿,来解决这个问题。

具体到解决方案上,目前主流的方式,是通过CGContext开一个额外的画布,然后通过CGContextDrawImage来画一遍原始的空壳CGImage,由于在CGContextDrawImage的执行中,会触发到CGDataProviderRetainBytePtr,因此这时候Image/IO就会立即解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage,交由UIImageView渲染。

延伸阅读:

二、ForceDecode的优缺点

优点

  • 可以提升,图像名列前茅次渲染到屏幕上时候的性能和滚动帧率

缺点

  • 提前解码会立即分配Bitmap Buffer的内存,增加了内存压力。举例子对于一张大图(2048*2048像素,32位色)来说,就会立即分配32MB的内存。

由此可见,这是一个拿空间换时间的策略。但是实际上,iOS设备早期的内存都是非常有限的,UIKit整套渲染机制很多地方采取的都是时间换空间,因此最终苹果没有使用这套Pipeline,而是依赖于高性能的硬件解码器+其他优化,来保证内存开销稳定。当然,作为图片库和开发者,这就属于仁者见仁的策略了。如大量小图渲染的时候,开启Force Decode能明显提升帧率,同时内存开销也比较稳定。

相关文章