App iOS 工程编译优化实践

引言

开发效率的提高,是开发者关注的一个永恒的话题。对于 iOS 而言,编译速度一直是影响 iOS 开发和集成测试效率关键的一环。前端

携程旅行 App iOS 工程编译,经历了从全源码编译到工程组件化,细分 Bundle,再到细分 Bundle 基础上的进一步优化四个阶段。每次的优化改造都是不断结合业务反馈,深刻了解 xcode 编译过程后的成果。python

iOS开发交流技术群:563513413,无论你是大牛仍是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!c++

1、背景

简单回顾一下在作 Bundle 拆分以前的状况,当时整个 iOS 工程的全部代码都在一块儿,并未作工程拆分和解耦,编译时全都是源码编译,数百万行代码所有编译完成要将近一个小时。全部的开发人员都在一个工程里开发,若是由于某我的提交的代码有问题(这是经常会发生的),致使编译了很长时间以后才报错,更是耽误时间,严重影响开发效率。对于测试人员来讲,每次须要验证一个功能时打包测试都须要至少等待几十分钟,这是极大的资源浪费。面试

这个时候的 Build 过程是全源码 complie,几千上万个文件都须要编译、连接,效率可想而知。swift

携程旅行App iOS工程编译优化实践xcode

因此为了提升开发和测试的效率,提升 iOS 工程的编译速度刻不容缓。缓存

2、优化方案

2.1 工程组件化

第一个优化是把整个工程的编译过程打散,把代码按照业务线拆分红一个个独立的子工程,每一个子工程的编译过程都是独立的。每一个子工程只须要保证本身工程的源码可以编译成功,对外输出统一的静态库和资源文件包的产物。这个产物咱们叫作 Bundle。app

单个业务工程(Bundle):框架

携程旅行App iOS工程编译优化实践工具

App Build:

携程旅行App iOS工程编译优化实践

对于单个业务来讲,编译时间大大缩短,整个 Build 过程变成单工程 complie,多工程 link,极大减小了 Build 过程当中的 complie 花费的时间。

这样有两个好处:

1)对于开发人员,每一个业务开发只须要把本身这个子工程切为源码引用,把其余非本身模块的子工程所有用静态库依赖,本地编译也只须要编译本身的子工程,能够大大提高本地开发编译速度。

2)对于测试人员,打包过程就变成了把全部已经编译好的子 Bundle 静态库连接到一个壳工程里,不须要对每一个文件进行编译,能够很快的打包测试验证。

2.2 增量编译

在工程组件化以后,在持续集成平台上单个 Bundle 的打包时间仍是过长。所以框架团队开始研究单个 Bundle 在持续集成平台上增量编译的可能性。

通过调研,最终选定 CCache 作为解决方案。CCache 是一个编译工具,能够将 Xcode 编译文件缓存起来,从而达到编译提速。

针对本地开发该方案具备优点,可是在结合自研的移动发布平台 MCD(Mobile Continuous Delivery)(后面简称【发布平台】)上使用时效果并无达到预期,主要有两点缘由:

1)同一 Bundle 多分支共存 :App 会存在大小版本同时开发的状况,在发布平台中也就会存在不一样版本、不一样分支的状况。

2)缓存管理不便 :发布平台打包机器一般仅有 250G 磁盘空间,当面临磁盘压力时,须要灵活的清理策略。

最终框架团队采用了自管理,能作到缓存物理隔离,同时也就省去了环境配置的步骤。

增量编译具体实现:

1)合并有变更的文件

  • 打包任务会根据新的 commitId 下载一份代码副本,不能直接使用该副本,由于代码文件内容没有变更,仅仅是文件属性的变更也会致使 xcodebuild 缓存不生效。所以须要副本和工做区内的源码作 diff,仅仅合并内容有变更的文件。
  • 使用 python 的 filecmp 实现合并代码逻辑,而且支持配置 ignore。
  • xcodebuild 指定 -derivedDataPath 设置缓存路径,并将该目录配置到 diff ignore 中。

2)提供清除缓存的功能

  • xcodebuild 的缓存有时候会出问题,好比修改了 c++ 文件后有时并不会生效,这种须要提供清除缓存的功能,能够由开发自由选择使用。

截止到以上两步,Native 已经基本实现了增量编译,可是实际使用还不够。由于打包主要是在集成系统平台上面完成的,集成平台打包有多台机器。

携程旅行 App 的打包 Jenkins 采用的是 master-slave 模式,一个 Job 下会有多个节点,Job 是随机抽取的节点。为了提升增量编译的命中率,必需要让 Bundle 和节点关联起来。好比:有 ABCD 四个节点,HotelBundle 每次都落到 A 节点,这样才能保证 A 节点中 HotelBundle 的 xcodebuild 缓存有效,而且代码 diff 差别最小。

具体实现:

1)保留 Jenkins Job 的工做区

该步骤是在 Jenkins Job 的配置中操做,取消勾选下图中的 Delete workspace before build starts

携程旅行App iOS工程编译优化实践

2)使用 Jenkins 插件创建 Bundle 和节点的关联

基于 Jenkins Label Parameter Plugin,并作改造,实现伪随机,以保证关联的节点下线以后,能使用候补节点正常工做。

发布平台前端提供关联配置,业务能够按需选择使用。

携程旅行App iOS工程编译优化实践

经过以上步骤就实现了增量编译,可是该方案针对 swift 不生效。swift 在 Release 模式采用的全量编译(以下图), 作总体优化。不过 swift Bundle 能够采用上述 Bundle 拆分的方案。

携程旅行App iOS工程编译优化实践

以某一个编译源码文件 197 个、资源文件 142 个的 Bundle 为例看下效果。

携程旅行App iOS工程编译优化实践

采用增量编译后,Bundle 编译耗时由 116s 降为 9s。

携程旅行App iOS工程编译优化实践

2.3 Bundle 细分

最初携程旅行 App 的 Bundle 都是按照业务来拆分的,好比:酒店就一个 Hotel Bundle,在当时编译速度已经不慢了。可是随着业务的发展,单个 Bundle 中业务代码愈来愈多,文件愈来愈多,致使编译又会变慢。这时,能够将单个 Bundle 按照功能作更细粒度的拆分,好比酒店拆分出了酒店主工程、酒店基础工程。

更细粒度的 Bundle 拆分还能带来如下其余收益:

  • 加快本地开发编译:某个功能的开发人员只须要将本身这个功能模块切为源码,其余模块全用静态库,提升本地开发编译效率。
  • 为其余独立 app 提供更细粒度的模块功能支持:我厂的不少独立 App 都是共用一套框架和基础组件的,按功能模块细粒度的拆分出独立的模块 Bundle 后,可使独立 app 在选择基础组件时按需选择。

2.4 合理设置头文件搜索路径

业务工程每每会大量依赖基础库代码,在本工程编译过程当中,也须要查找到引用的基础代码的头文件。

由于代码仍是在同一个仓库里,以前的方案是头文件搜索设置仍是指向本地的基础框架代码,使用循环搜索的方式。

这样的好处是任何一个头文件的修改,使用方能够立刻感知到。

缺点就是头文件没有特地为方便调用进行组织,搜索起来特别费时。

通过统计,Hotel 一个文件的编译每每都是秒级别。一整个工程编译下来就是十几分钟。

所以框架团队意识到必需要和第三方库同样,在目前的.a 和资源文件以外,提交 include 目录包含全部会被外部使用的头文件。

同时,考虑到 iOS 开发向 Swift 转型的须要,若是在 include 目录的基础上,还可以提供一份基于 include 里头文件的 module.mapmodule 文件。将方便后期业务方向 Swift 的迁移。

具体方法是:

1)首先框架的 Bundle,在工程设置中点击工程的 Target→Build Phases→Copy Files 点击 +,输入.h 把须要暴露的头文件都添加上。

这样会在输出产物的 Build 目录下,多一个 include 目录,再经过脚本去把这个目录里面的全部文件复制出来,同时生成 module.mapmodule。

2)使用的时候,将头文件搜索路径设置到 include 目录,而且设置为非递归搜索。

携程旅行App iOS工程编译优化实践

验证下来,Hotel 工程修改以后的 Build 时间为 7 分钟,相比修改以前的 19 分钟,时间减小了 63%。

2.5 创建中央缓存

费雷德里克·布鲁克斯说软件工程领域没有银弹。经过以上优化后,减小了编译时间,提高了开发和集成测试的效率,但这也不是解决编译速度问题的银弹。随着业务的不断使用,又出现了新的问题:Bundle 拉取时间过长。

Bundle 化方案各个业务的静态库生成都是在发布平台上编译的,业务在本地开发的时候再使用框架的脚本拉取 bundle 到本地。发布平台上打测试包的时候也是须要拉取全部 Bundle。

发布平台打包过程以下:

1)初始化 Jenkins 工做区,下载代码副本

2)下载 Bundle

3)使用 xcodebuild 生成 ipa

4)上传 ipa 和符号表

5)Job 状态回调

整个过程共耗时 7 分钟,目前携程旅行 App iOS 最新的版本的上线 Bundle 将近 70 个,每一个 Bundle 的静态库支持 arm6四、x84_64 等指令集,全部 Bundle 加起来有 4G 大小,即便在内网全量下载耗时也要 2~3 分钟。

好比酒店某一 Bundle:

携程旅行App iOS工程编译优化实践

全部 Bundle 全量更新一次耗时:

携程旅行App iOS工程编译优化实践

针对这个问题,解决方案是创建中央缓存。

在用户根目录下,创建一个隐藏的目录.iOSBundleRepo,按照 Bundle 的版本号存储,同一 Bundle 可存在多个版本。工具下载 Bundle 时优先判断缓存,未命中时才开始下载而且缓存到 repo 中。

创建中央缓存还能带来其余好处:在发布平台作预缓存,使用定时任务更新中央缓存,进一步节省下载耗时。

该方案实际上采用的是空间换时间的策略,随着时间推移,将会带来磁盘不足的问题,因此必需要实现清理机制。

针对不一样使用场景须要采用不一样的缓存清理策略,具体以下:

  • 本地开发:该模式下,开发能够自由选择更新最新 Bundle 和仅更新配置,缓存使用不频繁。因此将同一 Bundle 版本个数调低,缓存有效期拉长。
  • 持续集成:发布平台打包较为频繁,缓存使用比较频繁,而且 Bundle 版本变更较快,因此将同一 Bundle 个数调高,缓存过时时间设置为一天。

最终,打包耗时由原来的 7 分钟降为 5 分钟。

3、存在的问题和思考

软件开发工程没有银弹,你们都是在焦油坑里挣扎。

Bundle 的方案节省了编译的时间,提升了开发的效率,方便了持续集成和测试。

为了提升单 Bundle 编译速度而导出头文件的方案,牺牲了必定的灵活性换来了编译速度的提升。头文件没有了代码中的直接搜索,框架开发人员从共同开发者真正变成了库提供者,这就要求每一次都接口的修改都要及时更新并导出。

任何一个技术方案确定是在权衡各方面以后作出取舍的结果。框架团队为了提升 iOS Build 速度,经过自研的方案,作了拆分 Bundle,优化头文件搜索路径,增量编译,创建中央缓存等步骤,基本上知足了现有我厂各业务线的平常开发需求。

相关文章
相关标签/搜索