不改一行代码,就极大提高对本地图片加载速度(对 Asset 的探讨)

作者 Tsui YuenHong 日期 2017-08-03
不改一行代码,就极大提高对本地图片加载速度(对 Asset 的探讨)

2017年8月4日更新

根据这个 Session Optimizing I/O for Performance and Battery Life 的描述,使用 Asset 还有对启动优化的好处。

Asset Catalogs

10%的速度提升

如果你是被标题吸引进来的

可以直接跳到最后看结论,接下来是对 Asset 为什么能加快对本地图片加载速度的探讨。

为什么探讨 Asset 这个东西

由于项目中在刚启动的瞬间使用 “-[UIImage imageNamed:]” 会很慢,所以就打算探讨 “-[UIImage imageNamed:]” 的实现,但是过程中发现了使用 Asset 对于图片的加载有极大的提升,就去探讨 Asset 了。

使用 Time Profiler 探索实现

在使用 Time Profiler 调试 “-[UIImage imageNamed:]” 时候,发现它实际是调用 “-[UIImage imageNamed:inBundle:compatibleWithTraitCollection:]” 的,这时候萌发了一个想法是会不会根据指定 Bundle 的范围会加快加载图片的速度(理由是文件夹小了,减少索引次数)。

实验后发现,果然如此。把图片分散到指定 bundle 后的速度大大提升了。

这时候,有同事提示我使用 Asset 会不会加快,因为 WWDC 有提到过。所以我就去看了这集 Session,发现它是这样描述的。

发现使用 Asset 后速度也大大提升。到此为止就产生了探讨 Asset 这个东西的需求了。

符号断点跟踪步骤

这里就用两个图片来简单描述 “-[UIImage imageNamed:]” 发生了什么?

左图 Aseet/ 右图 Folder

上图 Aseet/ 下图 Folder

使用 Asset 底层是使用一个叫 _UIAssetManager 的类去存取图片,而使用 Folder 则是走 imageIO。而且即使你是用 Folder 也会先判断 Asset 中没有这张图片才去走 imageIO 。

这里就不展开说,具体可以根据图中的函数名下符号断点跟踪。(建议使用 chisel 来看各个类的成员变量)

缓存结构和索引的不同

使用 Asset 的缓存结构是: CUIStructuredThemeStore(https://github.com/billinghamj/iPhone6-iOS8-RuntimeHeaders/blob/master/PrivateFrameworks/CoreUI.framework/CUIStructuredThemeStore.h)

// _cache
[
Hash(A.png) -> 缓存
Hash(B.png) -> 缓存
]

使用 Folder 的缓存结构是: CUIMutableStructuredThemeStore(https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/PrivateFrameworks/CoreUI.framework/CUIMutableStructuredThemeStore.h)

// nameIdentifierStore
[
A.png -> 1
B.png -> 2
]

// memoryStore
[
Hash(1) -> 缓存
Hash
(2) -> 缓存
]

因为使用 Folder 去查找缓存,会先遍历 nameIdentifierStore 查找是否有缓存,没有就从 Folder 读取。在 Time Profiler 会有那么多的 “isEqualTo” 比较。

而使用 Asset 则是直接根据图片名称来直接 “objectForKey:” 取出缓存。

两者速度比较是: 前者 O(n)+O(1),后者 O(1)。
(至于为什么是O(n)? 因为我观察有O(n)这个行为,且 “isEqualTo” 的参数刚好字典 keys 和当前需要图片的名字。)

而且看出来建立这个缓存的时候,使用 Asset 会快一点(只需要一个字典就可以)。

文件 IO 的不同

假设都是读取 5 张图片

使用 Asset 的 IO 过程是:
open Asset -> read Asset -> close Asset

使用 Folder 的过程是:
open 1.png -> read 1.png -> close 1.png
open 2.png -> read 2.png -> close 2.png
open 3.png -> read 3.png -> close 3.png
open 4.png -> read 4.png -> close 4.png
open 5.png -> read 5.png -> close 5.png

可以看出这里 IO 次数少了(如果数量大的话,比较更加明显,Asset 一直是 1 次IO,而 Folder 会随读取文件的多少而递增),而且在 IO Usage 可以看出读取 Asset 的速度极快。

asset.car 到底是什么

asset.car 是编译后打包到项目里的文件。从目前的研究来看,asset.car 其实是将资源打包并建立索引的二进制文件,其头部包含资源在二进制对应的位置(类似 seekTable)。

小结

所以使用 Asset 的速度快的原因有以下几点:

  1. 缓存格式较优,从而建立和查找缓存的速度也会加快。
  2. 图片储存方式较优,查找图片位置更快,IO 也更快。

从 Folder 到 Asset,你需要做的只是转移图片资源(Xcode支持将Folder图片一键导入),并且不需要改任何代码就能使图片加载速度大大加快。