1. 背景
このような問題はどのチームにもあると思います.会社のビジネスが徐々に成長するにつれて、プロジェクトコードも急速に成長しなければなりません. iOS版のリリース以来、数百のバージョンがリリースされました.プロジェクト全体がコンポーネントで設計されており、使用されているプライベートライブラリとサードパーティライブラリは130以上に達しています.この大規模なプロジェクトは多くの問題をもたらしました:
-
プロジェクトのコンパイル速度は遅く、完全なコンパイルには 20 分以上かかります。
-
出力パッケージの速度が遅く、開発が完了してテストが送信されると、パッケージは一度に 20 分以上になります。
多くのビジネス要件と苦情に直面したとき、私たちはこの問題を待つことができず、早急に解決する必要があることを知っていました.
特定のリソース条件の下では、外部ハードウェア条件を使用して改善および改善することは不可能であるため、技術レベルでこの問題を解決する方法は、チームが解決する緊急の必要性になっています。
2.研究
1.Xcode コンパイルの最適化
iOS 開発の公式ツールとして、Xcode 自体に速度を向上させる方法があればそれが最適であることは言うまでもありませんが、調査と試行の結果、いくつかのパラメーターを変更して速度を向上させることは確かに可能ですが、明らかではありませんが、一部のパラメーターは、他のものを犠牲にしても変更されます。ここでは詳しく説明しません。関連情報はすでに非常に豊富です。
2.Cキャッシュなど
このカテゴリでは、コンパイル時間の短縮という目的を達成するために、前回のコンパイルの結果が何らかの方法でキャッシュされ、次回に再利用されます。ただし、現時点ではいくつかの制限があり、フレンドリーで簡単な方法でアクセスすることはできません. 一部のプロジェクトでは、キャッシュの機能を実現するためにいくつかの変更を加える必要がある場合があります. これは詳細な紹介ではありません。関連するコンテンツは、各プロジェクトの公式ウェブサイトで見つけることができます
3.バイナリ
これは業界で最も使用されているソリューションだと思います.現在業界で人気のあるコンポーネント化は、コンパイル時間を短縮するために、各コンポーネントライブラリをバイナリ化して、毎回コンパイルを繰り返さないようにすることです. 全体の詳細な計画については、業界のすべてのチームが、独自のプロジェクトに適したプロジェクトも出力します。おおよその手順は次のとおりです。
-
プラグインを作る
-
コンポーネント ライブラリのバイナリ バージョンをコンパイルする
-
コンポーネントのソース コードとバイナリを同時にリモート エンドにプッシュする
-
プロジェクトを生成する段階で、サブライブラリがバイナリまたはソース コードを参照しているかどうかを判断するため
-
対応するバージョンをプルするには、ソース バイナリ ツールを切り替えます
上記の手順からわかるように、ここにはいくつかの問題があります。
-
プロジェクトはすでにコンポーネント化されている必要があります
-
バイナリ コンポーネント ライブラリに関する問題 (バイナリにする方法、いつバイナリにするか、バイナリに起因するストレージの問題、プル速度の問題、Xcode インデックスの持続時間の問題、デバッグの問題)
从上面可以看出,这一套如果都解决好,肯定是需要一定的人力,时间,资源的,还是需要一定的成本。这一整套部署好,肯定还需要人去持续维护一段时间,直到这一套流程工具能稳定运行。
三、基于Xcode缓存的打包提速方案
综合上面介绍了几种方法,有的工程要改造,有的带来效果不理想,有的需要外部资源。那么有人会说了,我们
不想改造工程
不想部署一系列自动化工具,开发工具
公司iOS就是一个小团队,没有完善的开发环境
能不能有方法去提高打包编译速度呢?
有!一种基于Xcode本身的缓存方案,它来了:
-
完全满足工程0入侵,你无需对现有工程做任何改动
-
无需远端服务器,不必推送,拉取
-
无需额外去开发其他工具辅助,独立或者团队开发没区别
1、想法
我们知道Xcode一次全量编译之后,当你再次编译时,它就会很快编译完成,因为它利用了上次编译的缓存,但是苹果采用的是文件时间戳去判断是否利用缓存,所以当我们pod update等操作之后,对文件有操作,很容易就引起了重复编译,虽然这时候这个文件可能并没有改动,这里不去推测为什么苹果只采用时间戳的方式去判断。所以我们很容易想到,能不能去利用一下苹果的这个缓存呢,当时自己写了个简单的工程去测试,把一次编译的中间产物拷贝出来,然后把工程clean,如果这时候build,理论上来讲这时候是要重新编译的,我在build之前把拷贝出来的上次编译产物给还原进去,这时候点build发现它并没有去重新编译我拷贝出来的库,并且运行结果也没什么问题。好家伙,好像这方法没什么问题!
2、实现
根据上面的想法,很容易想到的思路,我们在编译之前,通过一定的方法去判断当前需要编译的这个库是否有缓存,有缓存就用缓存,没有缓存就去编译,待整个工程的库判断完成后,开始编译,输出结果,然后把当前这次参与编译的库重新缓存起来。整体看起来很熟悉对不对,没错,它的整体思路和所有iOSer都熟悉的图片加载库SDWebImage基本一模一样。方案很熟悉,但是大家可能还是不是很清楚日和去实现,下面介绍一些大家可能关注的几个关键的步骤和问题:
- 是否命中缓存?
因为我们一般是关注文件内容改变与否,所以我们这里采用的是这个库的所有文件,以及其他一些定义的参数,去算出一个MD5,作为这个库的缓存key。这里可能会有疑问,文件那么多,这个计算速度会不会很慢?在我们实际工程中运行,实测并没有什么问题
- 文中说,命中了就用缓存,那我怎么去控制xcode用缓存还是让它编译呢?
这里大家可以去学习了解一下Xcode编译相关的内容,熟悉一下.pbxproj文件,这里简单介绍一下:
一个 Xcode project 文件包含以下这些信息:
- 源代码,包含头文件和实现文件
- 内部和外部的静态库和动态库
- 资源文件
project.pbxproj 文件是 Xcode .xcodeproj 包里面的一个配置文件,我们修改 Project 和 target 里面的配置,实际上就是修改了 project.pbxproj。大家可以去打开这个文件看看,我们可以看到里面这样几个字段:
PBXHeadersBuildPhase:用于framewrok构建的链接阶段
PBXShellScriptPhase: 用于构建阶段复制资源文件的shell脚本
PBXSourceBuildPhase: 构建阶段中编译源文件
PBXResourceBuildPhase: 构建阶段需要复制的资源文件
很明了,这几个字段不就是控制编译的吗,那么我们拿到这个target的pbxproj配置,去里面删除这几个字段,在编译的时候,Xcode读不到这几个字段,不就不重新去编译了吗。不必担心,这里Xcode都是提供了相关的接口让你去操作的,实际过程很简单
- 缓存哪儿取,往哪儿拷贝?
这里还是建议大家了解Xcode编译相关内容,Xcode编译过程有一系列环境变量,这几个字段大家可以了解下,下面几个字段基本囊括了编译过程中产物的一些路径:
CONFIGURATION_BUILD_DIR: "/Xcode/DerivedData/project-dxdgjvgsvvbhowgjqouevhmvgxgf/ArchiveIntermediates/Project Distribution/BuildProductsPath/Distribution-iphoneos"
CONFIGURATION_TEMP_DIR: "/Xcode/DerivedData/project-dxdgjvgsvvbhowgjqouevhmvgxgf/ArchiveIntermediates/Project Distribution/IntermediateBuildFilesPath/project.build/Distribution-iphoneos"
TARGET_BUILD_DIR: "/Xcode/DerivedData/project-dxdgjvgsvvbhowgjqouevhmvgxgf/ArchiveIntermediates/Project Distribution/InstallationBuildProductsLocation/Applications"
TARGET_TEMP_DIR:"/Xcode/DerivedData/project-dxdgjvgsvvbhowgjqouevhmvgxgf/ArchiveIntermediates/Project Distribution/IntermediateBuildFilesPath/project.build/Distribution-iphoneos/Project.build"
CONFIGURATION_BUILD_DIR路径即为你需要取或者拷的位置,在编译过程中读取该字段的值,去把缓存拷贝到该路径,或者编译完成后,去该路径找到缓存取出来保存
- 该如何让Xcode编译过程中如何去执行这些动作呢?
Xcode对外提供了接口,编译过程中可以注入脚本,我们把需要执行的事件脚本编写好,通过Xcode的接口,注入到Xcode编译任务中。那么在后面编译每个target的时候,就会去执行我们自定义的脚本,从而去完成检测target缓存、拷贝等动作
3、运行
经过实测,在2019款MacBook Pro上,我们其中一个工程在完全编译的情况下时间大概850s左右,在使用上述方案后,完整命中缓存的情况下,时间可以低至60s,速度几乎是10倍以上的提升。当然我们实际打包输出,不可能每次都完整命中缓存,方案在线上运行的这段时间,我们统计到平均耗时为200s,提升了70%以上,这对于众多的分支,开发提测阶段,效率也是巨大的提升。
图中为我们现在主要两个工程使用该工具前后的耗时对比
4、可持续优化的地方
-
缓存命中稳定问题,命中逻辑调优
-
目前只能用于输出打包,适配到开发编译阶段
-
目前只用本地缓存,可提供缓存同步
四、未来
目前整体方案以Ruby gem的方式输出,对APP工程完全0入侵,一行命令就可以轻松接入,未来计划将加入其它功能解决iOS开发中的痛点,以持续提升iOS开发的效率和体验
-
cocoapods流程优化
-
分支切换缓存
-
编译缓存