iOS용 컴파일 가속 구축에 대한 또 다른 생각

1. 배경


모든 팀이 그런 고민을 하고 있을 거라 생각하는데, 회사의 비즈니스가 점차 성장하면 프로젝트 코드도 빠르게 성장해야 합니다. iOS 버전 출시 이후 수백 가지 버전이 출시되었습니다.전체 프로젝트는 구성 요소로 설계되었으며 사용 된 개인 라이브러리 및 타사 라이브러리는 130+에 도달했습니다.이 대규모 프로젝트는 많은 문제를 가져 왔습니다.

  1. 프로젝트 컴파일 속도가 느리고 전체 컴파일이 20분 이상입니다.

  2. 출력 패키징 속도가 느리고 개발 완료 및 테스트 제출 시 한 번에 20분 이상 패키징.

수많은 비즈니스 요구 사항과 불만 사항에 직면했을 때 이 문제는 기다릴 수 없고 시급히 해결해야 한다는 것을 알았습니다.

특정 리소스 조건에서는 외부 하드웨어 조건을 사용하여 개선 및 개선할 수 없으므로 기술 수준에서 이 문제를 해결하는 방법이 팀에서 해결해야 할 긴급한 요구 사항이 되었습니다.

2. 연구

1. Xcode 컴파일 최적화


우리 iOS 개발을 위한 공식 도구로서 Xcode 자체에 속도를 향상시킬 수 있는 방법이 있다면 당연히 최고지만 연구와 시도 후에 일부 매개변수를 변경하여 속도를 향상시키는 것이 실제로 가능하지만, 분명하지 않지만 일부 매개변수는 다른 것을 희생하더라도 변경됩니다. 여기서 자세한 내용은 다루지 않겠습니다. 관련 정보는 이미 매우 풍부합니다.

2. CCache 등


이 범주에서 이전 컴파일의 결과는 특정 수단에 의해 캐시되고 컴파일 시간을 줄이는 목적을 달성하기 위해 다음에 재사용됩니다. 그러나 현재로서는 친숙하고 간단한 방식으로 접근할 수 없는 몇 가지 제한 사항이 있으며 일부 프로젝트에서는 캐싱 기능을 구현하기 위해 일부 변경이 필요할 수 있습니다. 자세한 소개는 아니며, 관련 내용은 각 프로젝트의 공식 홈페이지에서 확인하실 수 있습니다.

3. 바이너리


나는 이것이 업계에서 가장 많이 사용되는 솔루션이라고 생각합니다. 현재 업계에서 널리 사용되는 컴포넌트화는 매번 반복되는 컴파일을 피하기 위해 각 컴포넌트 라이브러리를 이진화하여 컴파일 시간을 줄이는 것입니다. 전반적인 세부 계획을 위해 업계의 모든 팀도 자체 프로젝트에 적합한 프로젝트를 출력합니다. 대략적인 단계는 다음과 같습니다.

  1. 플러그인을 만들다

  2. 구성 요소 라이브러리의 바이너리 버전 컴파일

  3. 컴포넌트 소스코드와 바이너리, 그리고 동시에 리모트 엔드로 푸시

  4. 프로젝트 생성 단계에서 서브 라이브러리가 바이너리 또는 소스 코드를 참조하는지 확인하기 위해

  5. 해당 버전을 가져오려면 소스 바이너리 도구를 전환하세요.

위의 단계에서 알 수 있듯이 여기에 몇 가지 문제가 있습니다.

  1. 프로젝트는 이미 구성 요소화되어 있어야 합니다.

  2. 바이너리 구성 요소 라이브러리 문제(바이너리 방법, 바이너리 실행 시기, 바이너리로 인한 스토리지 문제, 풀 속도 문제, 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%以上,这对于众多的分支,开发提测阶段,效率也是巨大的提升。

截屏2021-12-12 20.17.11.png 图中为我们现在主要两个工程使用该工具前后的耗时对比

4、可持续优化的地方

  • 缓存命中稳定问题,命中逻辑调优

  • 目前只能用于输出打包,适配到开发编译阶段

  • 目前只用本地缓存,可提供缓存同步

四、未来


目前整体方案以Ruby gem的方式输出,对APP工程完全0入侵,一行命令就可以轻松接入,未来计划将加入其它功能解决iOS开发中的痛点,以持续提升iOS开发的效率和体验

  • cocoapods流程优化

  • 分支切换缓存

  • 编译缓存

추천

출처juejin.im/post/7040792078489485348