FastImageCache 是 Path 团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的。

一、优化点

iOS 从磁盘加载一张图片,使用 UIImageVIew 显示在屏幕上,需要经过以下步骤:

  1. 从磁盘拷贝数据到内核缓冲区
  2. 从内核缓冲区复制数据到用户空间
  3. 生成 UIImageView,把图像数据赋值给 UIImageView
  4. 如果图像数据为未解码的 PNG/JPG,解码为位图数据
  5. CATransaction 捕获到 UIImageView layer 树的变化
  6. 主线程 Runloop 提交 CATransaction,开始进行图像渲染
    • 如果数据没有字节对齐,Core Animation 会再拷贝一份数据,进行字节对齐。
    • GPU 处理位图数据,进行渲染。

FastImageCache 分别优化了 2、4、6(1) 三个步骤:

  1. 使用 mmap 内存映射,省去了上述第 2 步数据从内核空间拷贝到用户空间的操作。
  2. 缓存解码后的位图数据到磁盘,下次从磁盘读取时省去第 4 步解码的操作。
  3. 生成字节对齐的数据,防止上述第 6(1) 步 CoreAnimation 在渲染时再拷贝一份数据。

接下来具体介绍这三个优化点以及它的实现。

二、内存映射

平常我们读取磁盘上的一个文件,上层 API 调用到最后会使用系统方法 read() 读取数据,内核把磁盘数据读入内核缓冲区,用户再从内核缓冲区读取数据复制到用户内存空间,这里有一次内存拷贝的时间消耗,并且读取后整个文件数据就已经存在于用户内存中,占用了进程的内存空间。

FastImageCache 采用了另一种读写文件的方法,就是用 mmap 把文件映射到用户空间里的虚拟内存,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统 VMS 才根据缺页加载的机制从磁盘加载对应的数据块到物理内存,再进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。

三、解码图像

一般我们使用的图像是 JPG/PNG,这些图像数据不是位图,而是是经过编码压缩后的数据,使用它渲染到屏幕之前需要进行解码转成位图数据,这个解码操作是比较耗时的,并且没有 GPU 硬解码,只能通过 CPU,iOS 默认会在主线程对图像进行解码。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage 的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。

FastImageCache 也是在子线程解码图像,不同的是它会缓存解码后的图像到磁盘。因为解码后的图像体积很大,FastImageCache 对这些图像数据做了系列缓存管理,详见下文实现部分。另外缓存的图像体积大也是使用内存映射读取文件的原因,小文件使用内存映射无优势,内存拷贝的量少,拷贝后占用用户内存也不高,文件越大内存映射优势越大。

四、字节对齐

Core Animation 在图像数据非字节对齐的情况下渲染前会先拷贝一份图像数据,官方文档没有对这次拷贝行为作说明,模拟器和 Instrument 里有高亮显示“copied images”的功能,但似乎它有 bug,即使某张图片没有被高亮显示出渲染时被 copy,从调用堆栈上也还是能看到调用了 CA::Render::copy_image 方法:

那什么是字节对齐呢,按我的理解,为了性能,底层渲染图像时不是一个像素一个像素渲染,而是一块一块渲染,数据是一块块地取,就可能遇到这一块连续的内存数据里结尾的数据不是图像的内容,是内存里其他的数据,可能越界读取导致一些奇怪的东西混入,所以在渲染之前 CoreAnimation 要把数据拷贝一份进行处理,确保每一块都是图像数据,对于不足一块的数据置空。大致图示:(pixel 是图像像素数据,data 是内存里其他数据)

块的大小应该是跟 CPU cache line 有关,ARMv7 是 32byte,A9 是 64byte,在 A9 下 CoreAnimation 应该是按 64byte 作为一块数据去读取和渲染,让图像数据对齐 64byte 就可以避免 CoreAnimation 再拷贝一份数据进行修补。FastImageCache 做的字节对齐就是这个事情。

五、实现

FastImageCache 把同个类型和尺寸的图像都放在一个文件里,根据文件偏移取单张图片,类似 web 的 css 雪碧图,这里称为 ImageTable。这样做主要是为了方便统一管理图片缓存,控制缓存的大小,整个 FastImageCache 就是在管理一个个 ImageTable 的数据。整体实现的数据结构如图:

一些补充和说明:

5.1 ImageTable

一个 ImageFormat 对应一个 ImageTable,ImageFormat 指定了 ImageTable 里图像渲染格式/大小等信息,ImageTable 里的图像数据都由 ImageFormat 规定了统一的尺寸,每张图像大小都是一样的。

一个 ImageTable 一个实体文件,并有另一个文件保存这个 ImageTable 的 meta 信息。

图像使用 entityUUID作为唯一标示符,由用户定义,通常是图像url的hash值。ImageTable Meta的indexMap记录了entityUUID->entryIndex的映射,通过indexMap就可以用图像的entityUUID找到缓存数据在ImageTable对应的位置。

5.2 ImageTableEntry

ImageTable的实体数据是ImageTableEntry,每个entry有两部分数据,一部分是对齐后的图像数据,另一部分是meta信息,meta保存这张图像的UUID和原图UUID,用于校验图像数据的正确性。

Entry数据是按内存分页大小对齐的,数据大小是内存分页大小的整数倍,这样可以保证虚拟内存缺页加载时使用最少的内存页加载一张图像。

图像数据做了字节对齐处理,CoreAnimation使用时无需再处理拷贝。具体做法是CGBitmapContextCreate创建位图画布时bytesPerRow参数传64倍数。

5.3 Chunk

ImageTable和实体数据Entry间多了层Chunk,Chunk是逻辑上的数据划分,N个Entry作为一个Chunk,内存映射mmap操作是以chunk为单位的,每一个chunk执行一次mmap把这个chunk的内容映射到虚拟内存。为什么要多一层chunk呢,按我的理解,这样做是为了灵活控制mmap的大小和调用次数,若对整个ImageTable执行mmap,载入虚拟内存的文件过大,若对每个Entry做mmap,调用次数会太多。

5.4 缓存管理

用户可以定义整个ImageTable里最大缓存的图像数量,在有新图像需要缓存时,如果缓存没有超过限制,会以chunk为单位扩展文件大小,顺序写下去。如果已超过最大缓存限制,会把最少使用的缓存替换掉,实现方法是每次使用图像都会把UUID插入到MRUEntries数组的开头,MRUEntries按最近使用顺序排列了图像UUID,数组里最后一个图像就是最少使用的。被替换掉的图片下次需要再使用时,再走一次取原图—解压—存储的流程。

六、使用

FastImageCache 适合用于 tableView 里缓存每个 cell 上同样规格的图像,优点是能极大加快第一次从磁盘加载这些图像的速度。但它有两个明显的缺点:

  1. 占空间大。因为缓存了解码后的位图到磁盘,位图是很大的,宽高 100*100 的图像在 2x 的高清屏设备下就需要 200*200*4byte/pixel = 156KB,这也是为什么 FastImageCache 要大费周章限制缓存大小。
  2. 接口不友好,需预定义好缓存的图像尺寸。FastImageCache 无法像 SDWebImage 那样无缝接入UIImageView,使用它需要配置 ImageTable,定义好尺寸,手动提供的原图,每种实体图像要定义一个 FICEntity 模型,使逻辑变复杂。

FastImageCache 已经属于极限优化,做图像加载/渲染优化时应该优先考虑一些低代价高回报的优化点,例如 CALayer 代替 UIImageVIew,减少 GPU 计算(去透明/像素对齐),图像子线程解码,避免 Offscreen-Render 等。在其他优化都做到位,图像的渲染还是有性能问题的前提下才考虑使用 FastImageCache 进一步提升首次加载的性能,不过字节对齐的优化倒是可以脱离 FastImageCache 直接运用在项目上,只需要在解码图像时 bitmap 画布的 bytesPerRow 设为 64 的倍数即可。

文章

iOS图片加载速度极限优化—FastImageCache解析

iOS 图片加载速度优化的更多相关文章

  1. iOS图片加载速度极限优化—FastImageCache解析

    FastImageCache是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动 优化点 iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步 ...

  2. iOS 图片加载速度极限优化—FastImageCache解析

    FastImageCache是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的.优化点iOS从磁盘加载一张图片,使用UIImageVIew ...

  3. 记一次cocos项目的加载速度优化

    半个月前,我们用cosos creator做了一个简单的小游戏,也许算不上小游戏吧..一边学cocos,一边做,几经波折后终于上线了.然鹅,功能是实现了,但是加载速度十分感人(毕竟没经验嘛,无辜脸). ...

  4. iOS图片加载到内存中占用内存情况

    我的测试结果: 图片占用内存   图片尺寸           .png文件大小 1MB              512*512          316KB 4MB              10 ...

  5. iOS图片加载新框架 - FlyImage

    FlyImage 整合了SDWebImage,FastImageCache,AFNetworking的优点,是一个新的性能高效.接口简单的图片加载框架. 特点 高效 可将多张小图解码后存储到同一张大图 ...

  6. iOS 图片加载和处理

    一.图片显示 图片的显示分为三步:加载.解码.渲染.解码和渲染是由 UIKit 进行,通常我们操作的只有加载. 以 UIImageView 为例.当其显示在屏幕上时,需要 UIImage 作为数据源. ...

  7. 转: web 页面加载速度优化实战-100% 的飞跃提升

    前言 一个网站的加载速度有多重要? 反正我相信之前来 博主网站 的人至少有 50% 在加载完成前关闭了本站. 为啥捏? 看图 首页完整加载时间 8.18s,看来能进来看博主网站的人都是真爱呀,哈哈. ...

  8. iOS图片加载框架-SDWebImage解读

    在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山.它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件.在项目中使用SDWebImage ...

  9. iOS 图片加载框架- SDWebImage 解读

    在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山.它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件.在项目中使用SDWebImage ...

随机推荐

  1. 沙雕与大婶 | Mock调你的外部依赖吧

    故事背景: 沙雕在公司负责API项目的开发,很认真负责,经常加班加点赶进度,却常常被老板吐槽说他开发效率太低,他自己也很委屈,因为他所负责的项目常常依赖大量外部系统,他只好等对方开发完才一个个对接,开 ...

  2. 说一说Vue(2.0)组件的通信方式

    Vue(2.0)是组件化的开发模式,在不借助vuex框架的前提下,组件之间如何通信呢?接下来我在开发中总结了几种情况.1.父组件给子组件传值(props): 父组件给子组件传值的方式主要是用函数pro ...

  3. Object-Oriented Programming Summary Ⅲ

    目录 JML单元作业博客 1.1 梳理JML语言的理论基础 0. 前言 1. 形式 2. 作用域 3. 前置条件 (requires) 4. 后置条件 (ensures) 5. 模型域 (model) ...

  4. 简单的编写java的helloWord

    那么在上一章章节 http://www.cnblogs.com/langjunnan/p/6814641.html 我们简单的俩了解了一下什么是java和配置编写java的环境,本章呢我们学习如何编写 ...

  5. django数据库分库migrate

    最近在研究微服务和分布式,设计到了数据库分库,记录一下 首先,创建多个数据库,如果是已经生成的数据库,可以分库,这里我是新创建的项目,刚好可以用来尝试 我是用docker创建的mysql数据库容器 拉 ...

  6. MATLAB神经网络(4) 神经网络遗传算法函数极值寻优——非线性函数极值寻优

    4.1 案例背景 \[y = {x_1}^2 + {x_2}^2\] 4.2 模型建立 神经网络训练拟合根据寻优函数的特点构建合适的BP神经网络,用非线性函数的输入输出数据训练BP神经网络,训练后的B ...

  7. js 小练习题

    <script> /*1.结论,IIFE中运行顺序3,1,执行test(4),会传递参数*/ /*var a=5; var test = (function(a){ console.log ...

  8. Java对接百度智能云人脸识别

    ------------------------->这篇文章就是自己做个笔记<------------------------- 首先登录or注册自己的百度智能云管理中心:https:// ...

  9. 再刷JVM-JVM运行时数据区域

    前言 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域有各自的用途,以及创建和销毁的时机,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程 ...

  10. sql 语句系列(多表之链)[八百章之第三章]

    新增连接查询而不影响其他连接查询 请看图: 这种情况我们一般会使用左连接的方式. select e.ENAME,d.LOC,eb.RECEIVED from emp e join dept d on( ...