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. yii2设置默认控制器

    以Yii2高级模板配置为例

  2. 7-40 jmu-python-统计成绩 (15 分)

    输入一批学生成绩,计算平均成绩,并统计不及格学生人数. 输入格式: 每行输入一个数据,输入数据为负数结束输入 输出格式: 平均分=XX,不及格人数=XX,其中XX表示对应数据.如果没有学生数据,输出没 ...

  3. Typecho 主题制作记录

    模板制作快速入门 模板的制作并非难事,只要你写好了HTML和CSS,嵌套模板就非常简单了,你无需了解标签的内部结构,你只要会使用,模板就能迅速完成.这篇文章只简单的介绍了常用标签的使用方法,希望能带你 ...

  4. 使用 custom element 创建自定义元素

    很早我们就可以在 HTML 文档中写 <custome-element></custom-element> 这样的自定义名称标签.但是浏览器对于不认识的标签一律当成一个普通的行 ...

  5. Python - 超好用的第三方库pathlib,快速获取项目中各种路径

    前言 之前曾介绍过Python的os库详细使用方式,具体可看看这篇博文:https://www.cnblogs.com/poloyy/p/12341231.html 博主在学完os库之后,就开始投入使 ...

  6. Js逆向-滑动验证码图片还原

    本文列举两个例子:某象和某验的滑动验证 一.某验:aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9zbGlkZS1mbG9hdC5odG1s 未还原图像: 还原后的图: ...

  7. java算法--循环队列

    循环队列 我们再用队列得时候不知道发没发现这样一个问题. 这是一个只有三个位置得队列,在进行三次加入(addqueue)操作和三次取出(get)操作之后再进行加入操作时候的样子.明显可以看到,队列已经 ...

  8. JetBrains 第二轮:再为免费全家桶续命三个月

    昨天分享了如何通过参与JetBrains的解密任务来获取正版全家桶的兑换码.今天 JetBrains 一早继续在Twitter推出第二波任务: 下面,我们就继续来一起参与一下,为我们的正版JetBra ...

  9. Spring Boot入门系列(六)如何整合Mybatis实现增删改查

    前面介绍了Spring Boot 中的整合Thymeleaf前端html框架,同时也介绍了Thymeleaf 的用法.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/z ...

  10. 基于RabbitMQ和Swoole实现的一个完整的异步任务系统

    从最开始的使用redis实现的单进程消费的异步任务系统到加入swoole的多进程消费模式,现在,我们的异步任务系统终于又能迈进一步. 因为有了前面两个简单系统的经验,这回基于RabbitMQ的异步任务 ...