iOS之性能优化·提高App的编译速度

一、前言

  • 经过多年的开发和迭代,我相信很多的 iOS 项目代码已经达到几十万行甚至上百万行的规模,所使用的 Pod 库的数量可以达到几十个甚至上百个,App Store 安装包也变得越来越大,在这么大的项目规模下,打包和编译问题逐步成为开发团队一个躲不过的痛,严重影响了研发效率与其他团队之间的协作。
  • 有时间,一台机器同时需要承接七八个项目,多个分支的打包任务,在有多个项目同时打包的情况,尤其显得力不从心。
  • 在硬件资源有限的情况下,并且在无侵入、无影响现有的业务的前提下,如何解决这些成为摆在团队面前的难题和迫在眉睫的需求,怎么加快打包速度成为了一个需要优化处理的重要事项。

二、编译提速探索与尝试

① CCache
  • CCache 是一个编译缓存器,一个能够把编译的中间产物缓存起来的工具。
  • CCache 的原理是通过把项目的源文件用 ccache 编译器编译,然后缓存编译生成的信息,从而在下一次编译时,利用这个缓存加快编译的速度,目前支持的语言有:C、C++、Objective-C、Objective-C++;
  • 如下所示,基本就阐述了 CCache 的工作原理:

iOS之性能优化·提高App的编译速度

  • 在项目中的实际编译流程:

iOS之性能优化·提高App的编译速度

  • 经过在工程的一番尝试,CCache 确实在某些方面上极大的提升了出包的速度。很多时候项目的打包速度的确减少了一半多,能够给带来比较不错的提升,大大加快项目的出包速度。
  • CCache 的优点如下:
    • 能满足追求的无侵入、无影响现有的业务的要求,无入侵、且开发人员无感知。
    • 确实能大幅度地提升编译速度,项目上最快时提高3倍以上的编译速度。
    • 不需要对项目作出大调整,只需部署相关环境和一些脚本支持。
    • 不需要改变开发工具链。
    • 同一个目录下,CCache 的缓存命中率相对稳定。
  • 很多时候开发的过程中,项目可能会存在以下问题:
    • 在未有缓存的情况下,首次打包编译的时间比原来的翻近一倍;
    • 修改一些引用较多的文件(如公共库、底层库改动),容易造成大范围的缓存失效,速度会变得比原来未使用 CCache 时更慢;
    • 多个项目相同的组件不支持缓存共享,有多个分支打包的需求,修改目录名称后,缓存即失效;
    • 机器的 CCache 最大的缓存上限约 18GB,且 Debug/Release 区别缓存,项目会占用几个GB+的缓存,多个项目、多个分支很容易超出上限,一台机器同时支持多个项目会触发 CCache 清缓存;
    • 对机器硬盘读写要求高,如不是全部固态硬盘,速度影响大;
    • CCache 不支持 Clang Modules,系统框架例如 AVFoundation、CoreLocation 等, Xcode 不会再帮你自动引入,会导致编译失败;
    • CCache 不支持 PCH 文件;
    • CCache 目前不支持 Swift。

iOS之性能优化·提高App的编译速度

② 静态库二进制方案的探索
  • 使用二进制编译的自研任务,可以更进一步提高研发效率。
  • 项目使用 CocoaPods 来管理第三方库和私有库的依赖,对大部分项目来说应该是标配。目前还是纯 Objective-C 的项目,有少量 C++,暂没有引入 Swift。
③ 调研过的二进制组件方案
  • Carthage 可以将一部分不常变的库打包成 framework,再引如到主工程,这样可以减少开发过程中的编译时间。Carthage 可以比较方便地调试源码,如果项目大规模使用 CocoaPods,转用 Carthage 来做包管理需要做大量的转换工作,变动太大,不满足的无侵入、无影响现有的业务,这个方案不太适合。
  • cocoapods-packager 可以将任意的 pod 打包成 Static Library,省去重复编译的时间,一定程度上可以加快编译时间,但是也有自身的问题:
    • 优化不彻底,只能优化第三方和私有 Pod 的编译速度,对于其他改动频繁的业务代码无能为力;
    • 私有库和第三方库的后续更新很麻烦,当有源码修改后,需要重新打包上传到内部的 Git 仓库;
    • 过多的二进制文件会拖慢 Git 的操作速度(目前还没部署 Git 的 LFS);
    • 难以调试源码,不共享编译缓存;
    • 打包成 Static Library 过程缓慢,需要通过pod lint,各个组件间又层层嵌套依赖,在现有阶段来说,是难以实现的。
  • Cocoapods-Binary(Cocoapods 官方推荐的二进制插件), 是一个即时生成二进制包并缓存,而非像 CocoaPods-Packager 仅仅针对单个私有库的。原理是通过 CocoaPods 提供的 pre_install hook 在 pod install 的 prepare 阶段拦截到当前的 pod install context,进而 fork 出一份独立的 installer 以完成将预编译源码 clone 至 Pod/_Prebuild 目录下,同时也存在几个不足之处:
    • 单私有源,无法实现服务端缓存,在没有对应二进制包版本时,pod install 后会额外去做二进制包的生成,一定程度上会影响 pod install的速度;
    • 开发者切回源码调试,二进制缓存会一并清空,需求重新编译;
    • 多个项目、不同分支的相同组件依旧无法共享;
    • 只支持 framework,对项目需要比较大的头文件引用方式改动。
  • cocoapods-bin 双私有源:该插件进行二进制化的策略是采用双私有源,即2个源地址,一个静态服务器保存预先打好包的 framework,一个是保存源码的服务地址。它的优点:
    • 源码和二进制文件之间可以来回切换,速度比较快;
    • 不影响未接入二进制化方案的业务团队;
    • 无二进制版本时,自动采用源码版本;
    • 接近原生 CocoaPods 的使用体验。

三、双私有源二进制组件简介

  • 受到 cocoapod-bin 启发后,在借鉴它的部分框架下,可以实现二进制辅助插件cocoapods-imy-bin,新增命令和二进制源码调试能力。
① 只要能编译通过,就制作
  • 在 cocoapods-imy-bin 的辅助下,能无侵入式自动化地制作所有符合条件的组件为二进制,且对于频繁的业务组件也能轻松的应用上二进制组件,无需多余操作,一切交给 cocoapods-imy-bin 自动化运行。
  • 同时对于研发人员,也能提供独立的二进制组件给研发人员使用,解决日常的编译 效率、跑真机效率低下,被墙等各种问题。
  • 只要能编译通过,就制作,一次编译到处使用,无入侵。即使独立的组件库编译不通过,整体项目能编译通过也制作。整套环境下来,可以不让开发人员改变原来的开发习惯,不需要改动业务中相关的代码,基本上做到了使用人员无感知状态。
② Ci 打包效果
  • 如下所示:这是打包几千个的经验得出对单个项目编译时间大致的曲线图,这里假设一台机器只一次只有一次job,Y轴编译时间,X轴某次的编译, 红色线条表示的是原生(未使用 CCache 和二进制组件),黄色线表示使用了CCache,蓝色表示使用了二进制组件:

iOS之性能优化·提高App的编译速度

  • 由图可以看出来在无任何辅助下原生的编译时间曲线(红色)是趋于平缓,在20min上下左右。CCache 和二进制第一次在无任何缓存的情况下,在一定程度上是会比原生的耗时,CCache 主要耗时在边编译边缓存项目的编译产物。二进制主要耗时在编译完成后,对 .a 编译产物的组装和 push 到私有源仓库的时间上(这个跟所采用有关系,如果没有利用Jenkins 编译后的产物制作二进制就不存在)。
  • 在 CCache 完全命中二进制文件完全都存在的情况下,CCache 比原生的提高一倍以上, 二进制会比 CCache 编译时间再提高一倍,且稳定在2分钟左右。二进制在之后的表现更趋于平稳,而 CCache 在修改了某个被引用较多的文件时、如底层的公共文件后,命中率就会大大地降低,有时会比不用 CCache 更耗时,如#4位置。在 ci 有多个 job 同时并发在跑的情况下,由于 CCache 需要对 IO 频繁地读写操作,耗时表现可能会更糟糕些。
  • 二进制的编译时间相对平稳很多(蓝色曲线),在架构强有力的支撑下,划分出110多个独立组件,每次的打包基本上是就耗在某个组件的编译 +archive。如果是某些变更比较频繁的组件,我们还可以考虑对颗粒较大组件配上 CCache,做双层编译缓存。双层编译缓存原理是 Pods 组件库无二进制组件采用源码编译时,源码编译同时应用 CCache 缓存支持,加速源码组件的编译。
  • 同时组件库可以配合 Gitlab-Ci 的 runner 的应用,每次已提交代码就触发独立组件的制作二进制,让每次的编译速度都达到最快,蓝色二进制曲线将会更接近直线。
  • 如果存在有独立组件无法编译问题和版本依赖问题,也可以再跑个定时 Job,或者其他轮询条件 Job,及时提供最新二进制组件。
  • 一台机器上多个项目的 CCache 显得是比较吃力的,且不稳定,超出 CCache 的缓存最大值就会被清掉。使用了二进制后,即使是多个项目编译时间都是趋于比较平稳的。

iOS之性能优化·提高App的编译速度
iOS之性能优化·提高App的编译速度

③ 开发使用效果
  • 在 Podfile 引入插件后,在 pod install/update 后,符合条件的情况下,会自动转换为二进制组件。
  • 源码编译:项目如果上百个 Pods 库,有 20+ 个稳定 Pods 库已经被制作为二进制库,并非全部源码编译,如何全部转换为源码编译,实际数字会比这多出很多。
  • 在环境搭建完后,开发人员在 Podfile 中,加入以下两句,就能享用到自动切换为二进制组件,体验极速编译。
	plugin 'cocoapods-imy-bin' 
	use_binaries!

四、功能点

  • 目前 cocoapods-imy-bin 插件支持的功能如下:
    • 无侵入、无影响现有的业务;
    • 不影响未接入二进制化方案的业务团队,提供配置文件;
    • 只要项目能编译通过就制作,即使独立组件编译失败;
    • 支持无二进制版本时,自动采用源码版本;
    • 支持只需项目能编译通过就能制作二进制组件,无需再关心 pod lint 等;
    • 支持 pod bin local 命令一键自动化制作、上传、存储项目本地已经存在的二进制组件,可配合 ci 打包的编译产物使用;
    • 支持指定依赖分支支持:podspec =>’’, :git 方式的引用;
    • 支持同时 .a、Framework 静态库产出;
    • 支持 archive 时,根据 Podfile 自动获取 podsepc 依赖的库,无需强制去 spec 仓库拉取;
    • 支持多套隔离环境,如 Debug/Release/Dev 配置,方便为 Debug/Release/Dev 各种环境提供专用二进制组件;
    • 支持输出 .a 二进制组件制作 binary.podsepc 无需模板;
    • 支持稳定的二进制组件,在上传二进制组件的 binary.podsepc 跳过 pod lint 验证,加快速度;
    • 支持 pod bin auto 命令一键自动化制作、上传、存储单个二进制组件;
    • 支持 pod bin auto --all-make 命令一键自动化制作、上传、存储该项目下所有组件的二进制组件;
    • 支持是否使用二进制文件、是否制作二进制文件和二进制/源码调试功能的白名单设置;
    • 支持 pod install/update 多线程模式,加快 pod 过程,Pod 速度提升 80%+;
    • 支持 pod bin install/update 命令,实现无入侵修改 Podfile 内容,避免直接修改工程的 Podfile 文件而导致提交冲突、误提交;
    • 支持 pod bin code 命令,实现二进制库不切换源码库,程序无需重新运行的调试能力。
上一篇:高性能MySql学习笔记-第十二章:高可用性


下一篇:记录一些关于Nuitka打包的一些经验