更新

  • 2021/03/09 Unity 2020.2 新增 IL2CPP 优化
  • 2019/09/12 更新正确方法
  • 2018/09/24 增加 Xcode archive 增量构建引用文章链接
  • 2018/09/16 初次发布

介绍

Unity 通过生成 Xcode 工程以支持 iOS 平台。

IL2CPP在xcode下增量编译问题 - UWA问答 这个问题中提到 C# 代码未发生变化,生成的 C++ 代码也不会发生变化,但是时间戳会发生改变。

大部分编译器之类的工具都是依赖于时间戳信息的,会存储使用到文件的时间戳信息,然后再次编译时会检查,如果内容相同则使用之前的编译缓存。

问题

Unity 的 BuildOptions.AcceptExternalModificationsToPlayer 选项是允许 IL2CPP 增量编译的,但是由于项目内的第三方 SDK 接入并不太规范,不能使用此选项,必须全新构建后修改相关文件配置,否则会重复修改配置。

其次,BuildOptions.AcceptExternalModificationsToPlayer 选项只会正确处理 Unity 生成的 C++ 文件,其他源码文件(如第三方 SDK 中 C++ 文件)并不会处理,这些文件即便是内容未变也会由于时间戳发生变化导致重新编译。

环境

  • Unity 2017.4.2f2
  • Xcode 9.4.1
  • macOS 10.13.6

解决思路

原理非常简单,只需要在生成 Xcode 工程前备份一份,保证所有时间戳不变;然后在生成 Xcode 工程后比较现有文件与备份文件,内容相同则重置时间戳为备份文件时间戳,不同则保留时间戳。

这里需要额外注意一点,文件与目录的时间戳都需要保留,那么就需要对整个目录进行深度遍历保证目录中的子目录与文件会正确影响父目录时间戳。

更新:2019/09/12 这里的思路并不是最优的,从空间与时间的角度看都是。

正确方法是使用 MD5、SHA1 之类的散列算法生成文件的散列值,同时保存文件长度与时间戳;然后在生成 Xcode 工程后比较现有文件与备份文件,长度不同时跳过并保留时间戳;长度相同时计算散列值并比较,相同则重置时间戳为备份文件时间戳,不同则保留时间戳。

这个方法在写完文章几天后就想到了,因为突然想起自己之前使用 C# 写的拷贝 AssetBundle 到 SteamingAssets 目录功能功能就是这么做的,但是考虑到 C# 执行没有 Rust 快,因此准备使用 Rust 重写,奈何拖延症范了一直没有写。为了防止更多人参考到错误的信息,先将正确方法更新放在这儿。

备份时间戳

删除上次备份目录后再使用 cp 命令或 C# 代码拷贝目录备份,这种方法由于未使用之前已复制的目录作为缓存,太过浪费时间。

由于确定平台是 macOS,那么自带的 rsync 可以完美完成任务,rsync 可以增量复制、差异传输、删除多余文件。

1
rsync --archive --delete <source> <destination>

使用 C# 调用外部程序即可,另外需要将标准错误捕获到,以便在返回值非 0 出错时在 Unity 控制台中输出错误。

恢复时间戳

深度遍历目录,文件内容完全相同则恢复时间戳,否则保留时间戳。

比较文件时先比较大小,后比较内容。比较内容就是同时读取两个文件,按字节比较,遇到不同时马上退出。

需要额外处理文件不存在的情况。

注意:代码中使用 Directory.GetDirectories 与 Directory.GetFiles 而不是 DirectoryInfo.GetDirectories 与 DirectoryInfo.GetFiles 是因为前者获取到的路径正好是以指定目录开始的相对路径,而后者只能由 FullName 属性获取绝对路径(也可以替换成相对路径,多一步字符串替换操作)。

解决方案

针对此问题编写的 Unity 编辑器脚本,方便集成在 iOS 构建中使用。

networm/UnityXcodeCache: Simply setup cache for Unity generated Xcode project to speed up build times.

已在前述的环境中测试通过,理论上不会有版本上的依赖。此方案并不要求打包时有特殊指定参数,可以在任意情况下使用,并不会影响打包结果。

使用方法:Unity 构建 Xcode 工程前执行 Tools/Xcode/WriteCache,Unity 构建 Xcode 工程后执行 Tools/Xcode/ReadCache,然后打开 Xcode 构建即可加速。

实测结果

Xcode 工程大概 1.5GiB,并且存放在 SSD 中,在 Unity 中执行恢复时间戳步骤大概耗时 37 秒左右。

实际项目使用 Xcode 构建时间在 3.3GHz Intel Core i5 四核下用时 580 秒左右。

如果完全没有文件改动,再次构建 Xcode 耗时为 0 秒,即直接可以运行;其他视情况而定,大概修改 4 个文件,构建用时会在 90 秒左右,具体还要看脚本依赖关系与 IL2CPP 生成情况。

注意事项

此方案只适用于 Xcode 构建,如果使用命令行工具 xcodebuild 并指定 export 方法,那么缓存并不会生效,因为 export 时并不会使用缓存。

因此使用 fastlane 的 gym 工具就无法享受到缓存加速的好处!

Unity iOS增量编译 | Loading & Learning 这篇文章中提到了利用之前的缓存手工生成 xarchive 文件,然后再 export 为最终的 ipa 文件。

gym 工具本身是将两件事情同时做了,应用上述方案需要替换掉 gym,fastlane 其他部分可以保持不变。

优化方向

  1. 使用性能更强的 C、C++、Rust 重写恢复时间戳部分
  2. 使用多线程处理,最简单的方案是使用一个线程处理一个根目录。

Unity 更新

Unity 2020.2 中新增了 IL2CPP 很重要的优化:

0-Change rebuilds in IL2CPP platforms. If you make changes that don’t involve code changes, for example to materials, shaders or prefabs, IL2CPP conversion from .NET assemblies to C++ will be skipped. This will improve artist workflows and speed up iteration.

IL2CPP player build time improvement. The process of converting .NET assemblies into C++ files has historically been single threaded. In Unity 2020.2, the application that processes the conversion (il2cpp.exe) takes advantage of modern multi-core processors, dramatically decreasing the overall IL2CPP player build times. Find more information in the forum thread.

参考资料