4b15bfb2f8820ddabdc3347a5515e838
优化App的持久化策略

这个 session 覆盖了 app 储存文件的方方面面, 对于经常需要写入沙盒的 app来说, 提供了很多好的 guideline, 以及底层原理的分析.

使用HEIC格式图片

苹果建议我们本地的图片切换成使用 HEIC 格式这种更高效的图片格式, HEIC格式本身比jpeg小50%, 因此下载和上传都更快, 在磁盘中存取也更快, 同时也支持透明度和无损压缩, 在单个 HEIC 图片容器中可支持储存多个图片.

将图片放入asset catalog

asset catalog 原生支持了 app slicing, 可以根据下载机型的不同, slice 出对应的图片放入安装包中, 而不是直接将2x, 3x等倍图全部打入安装包, 可以有效减少包体积. 另外image的加载也会更快, 尤其是启动时, apple 声称可以比不使用 asset catalog 的情况提升10%的图片加载速度, 所以使用 heic 加 asset catalog 组合, 会有奇效

文档数据元数据 File system metadata


档 metadata 的数据写入经常会发生, 而且 IO 开销是很大的, 例如你的 app 中有一个 plist 文件记录上一次的启动时间, 每次启动 app, 都读取该 plist 获取上次启动时间, 然后写入当前时间这个简单的操作会发生一次读取 IO, 三次写入 IO, 还有一次 fsync() 的操作, 并且以下行为都会造成 File system metadata 写入

  • 创建文件
  • 删除文件
  • 重命名文件
  • 更新文件

而 File system metadata 包括以下元素

  • 文件名
  • 大小
  • 地理位置
  • 修改时间
  • 等等....

例如当我们写一个240byte的 NSDictionary 到文件时, 首先是 update file system tree

基于写时复制 (copy on write)策略, 不会马上更新 file system tree 的结点, 而是创建一个结点的拷贝

每一次操作, 都会有自己的 transaction id, 这个写操作就会生成一个新的结点, ID也会被更新, 一个简单的写入240byte的数据进入disk的时候, 会同步导致以下数据的更新. 包括:
更新file system node (4k),
更新object map (4k),
metadata总大小: 8k
文件本身: 本身是240byte, 但ios的写入文件最小单位是4k
所以总共是12k
因此每次更新数据到 disk 里都是有代价的, 如果我们只是需要创建一些临时数据, 例如字典, 数组这类数据, 建议不要把这些数据直接写入到磁盘中, 直接在内存中使用并销毁就可以了, 如果这类原始数据有持久化的需求, 应该通过一个中间类来统一管理内存写入到 disk 的逻辑, 尽量减少你的app需要的文件数量

syncing to disk

OS cache: 性能最好的一层, 使用 logical I/O, 由于是储存在内存中, 所以 I/O操作很高效 (使用logical I/O)
Disk cache: 磁盘储存的物理映射. (使用 physical I/O)
permanent storage: 最终用于持久化数据的介质, 对于iOS来说, 就是闪存 (使用 physical I/O)
缓存有以上几个层级, 对于 app 来说, 离 cpu 越近的 cache, 性能就越好, 但同时我们也希望cache能确实地落在磁盘中. 数据在内存当中时对于app而言速度是最快的, 也没有任何的 IO 开销, 但是当我们需要将数据从内存一层一层地注入到闪存时, 就需要注意 IO 开销了.

下面介绍几个将数据从 OS cache 层逐步 flush 到 Permanent Storage 的函数

fsync()

该函数用于将数据从 OS cache 层写入到 Disk cache 层, 但数据可能不是立即写到 permanent storage 层, 如果没有代码的明确指令, 实际上是由设备的固件决定数据什么时候从 disk cache 进入 permanent cache, 并且写入的顺序是没有保障的, 从OS cache写入 Disk cache 的顺序并不决定从 disk cache 进入 permanent cache的顺序 因此fsync()的过度使用是昂贵的, 这个函数是会直接导致 IO 的发生, 其实我们没有必要手动显式地调用这个函数, 因为 OS 本身会周期性的调用fsync()来写入数据, 因为大多数情况下没有必要手动触发fsync()

FULLSYNC

该函数用于将数据从 OS cache 层写入到 permanent cache 层, 并且会触发所有已经存在于 disk cache 上的数据写入到 permanen cache, 并且OS本身会周期性的调用该函数, 因此理由同上, 大多数情况下没有必要手动触发

文件序列化格式的选择

开发者一般会使用 Plist, XML, JSON 这三个常见的格式, 这些都是常见的数据格式, 便于使用而且普适性高, 也易于解析, 适合不是频繁读写的数据, 但是每次改动都是全量的读写, 导致整个文件读取和重新写入, 就会引起上面所说的从OS cache层到 permanent cache 层的IO操作, 即使你写入一个很小的数据, 由于文件本身携带的 meta data 操作, 也可能会产生数据量是写入data本身几倍大的IO开销

举个例子

以下是 file activity instruments 监控我们创建, 读取, 和更新上述这个 plist 时, 引起了12个独立的 IO 操作

top Created with Sketch.