iOS编译速度如何稳定提高10倍以上之一
四、双私有源二进制组件整体设计方案
1、制作流程 - 二进制组件
2、使用流程 - 二进制组件
3、分析
如上图所示
server
是一个自建的服务,专门存在二进制文件
二进制repo
是一个专门存储二进制podspec的私有源仓库
两者的设计很好的将二进制组件产物分别的存储起来,同时我们借助cocoapods-imy-bin插件在pod install/update的时机去自动切换源码/二进制组件,巧妙的避开入侵原有组件的问题。即使我们把server
关闭或者二进制repo
删除,清了缓存后,原有的打包流程和开发流程还是能照旧执行,并不受任何影响。
即插即用,无需关心服务是否存在、二进制组件是否已经制作完成了,方便灵活。
同时github/组件源码/源码repo属于源码源
,与二进制repo
组成双私有源
五、制作二进制组件原理
只要项目能编译通过就可以制作,不再需要关注组件是否能通过pod lint,不再需要关注组件是否有push到repo,不再需要耗在的pod lint的等待时间上,完全无门槛。
1、为什么能这么灵活,怎么做到的
大部分的轮子都要求组件能pod lint通过,但是绝大部分的组件并没有这么规范化,梦想总是美好的,现实总是残酷的。
为了能让绝大部分组件使用,我们绕过了pod lint,优化了部分cocoapods流程,直接去取build后的.a编译产物,结合对应的podspec文件,去组装Headers、Resource。很多轮子都只制作当前的组件库,对于依赖的并不提供支持,在最新代码的前提下,利用所有的.a编译产物制作对应二进制组件可以极大的提高效率。
同时制作二进制组件时podspec中dependency依赖组件,不再强制要求去repo拉取,而是优先从podfile拿取依赖值。
2、静态文件.a的制作
制作静态文件分为两种,一种是cocoapods-imy-bin自己制作,一种是利用已经编译完成的编译产物来组装制作二进制组件。
2.1、通过插件自身的xcodebuild 制作
可以直接使用插件的 pod bin auto
命令,在插件初始化配置完成后,目录下只要有包含podspec文件,会自动化执行build、组装二进制组件、制作二进制podspec、上传二进制文件、上传二进制podspec到私有源仓库。
pod bin auto --all-make
带上—all-make
参数会把当前组件所依赖的组件都自动化制作成二进制组件。
//构建真机静态库文件
xcodebuild GCC_PREPROCESSOR_DEFINITIONS='$(inherited)' ARCHS='arm64 armv7' OTHER_CFLAGS='-fembed-bitcode -Qunused-arguments' CONFIGURATION_BUILD_DIR=build clean build -configuration Release -target '目标工程Target' -project ./'目标工程' 2>&1
//构建模拟器静态库文件
xxcodebuild GCC_PREPROCESSOR_DEFINITIONS='$(inherited)' -sdk iphoneos CONFIGURATION_BUILD_DIR=build-simulator clean build -configuration Release Release -target '目标工程Target' -project ./'目标工程' 2>&1
ci打包只需真机模式,开发需要模拟器模式+真机模式。在构建完对应的架构,用lipo对架构.a/.framework进行合并操作
lipo -create 'x86.a' 'arm64.a' 'armv7.a' -output '目标静态库'.a
2.2、结合第三方编译产物
通过Jenkins打包,打包过程会生成中间编译.a文件,再通过cocoapods-imy-bin 的pod bin local
去组装每个二进制组件中的.a/headers/resource,再自动化制作对应的二进制podspec文件,上传对应环境的repo和存储服务。
在我们只有一台iOS打包机,且没有提交代码后还需等几分钟(等GitLab-ci制作完成二进制文件完成)才能打包的习惯,这是一种比较好的选择。
提交代码后等GitLab-ci制作完成二进制文件完成,再打整体app的包,这种方式严重的改变了我们当前的开发习惯,且并在提交代码到出包的过程中没有加快整体流程的速度。
我们同时也用了Gitlab-Ci
触发二进制组件的制作、Jenkins定时构建二进制组件。
2.3、ccache+二进制双层编译缓存
在Jenkins打包上,对未制作二进制的组件,同时也应用了ccache。双编译缓存的机制,在一定的情况下可以很大地加速整体速度。
3、Headers
Headers .h文件在pod install/update过程中,其实已经就组装整理好了,在Pods下Headers/Public/xxx ,这些头文件,我们只需要拷贝就行了,无需再去根据对应的podspec的source去组装。
4、Resource
Resource是通过对应podspec的resource字段去对应的组件库中搜索资源,再组合起来。这里需要特别注意查看下有些中文、带空格路径的文件。
6、开发机器,本地命令
开发机器只需安装了cocopods-imy-bin插件后,可以通过本地命令 pod bin auto
or pod bin local
自动制作二进制组件,自动上传,无需其他配置。
六、二进制PodSpec
1、自动化制作PodSpec
二进制组件现在是一个新的组件库,我们需要为其配置一个podspec配置索引文件。
cocopods-imy-bin制作二进制podspec不需要模板,会自动去提取源码podspec的version去创建,在修改source
、source_files
、vendored_libraries
、public_header_files
这几个字段后,其他的都读取原有字段。对于一个静态组件,其他的修饰字段并不是很重要。
#二进制podspec.json
{
"name": "YYModel",
"version": "1.0.4.1",
"source": {
"http": "http://xxx:10240/frameworks/YYModel/1.0.4.1/zip",
"type": "zip"
},
"source_files": "bin_YYModel_1.0.4.1/Headers/*",
"public_header_files": "bin_YYModel_1.0.4.1/Headers/*.h",
"vendored_libraries": "bin_YYModel_1.0.4.1/*.a"
}
这里有个细节bin_YYModel_1.0.4.1
,制作完二进制组件后,会把目录改成对应的组件库+版本号,用于识别对应的版本。
七、壳工程分离
壳工程顾名思义就是将原来的project中的代码全部拆出去,得到一个空壳,仅仅保留一些工程配置选项和依赖库管理文件。
因为自动集成涉及版本号自增,需要机器修改工程配置类文件。如果在创建二进制的过程中有新业务PR合入,会造成commit树分叉大概率产生冲突导致集成失败。抽出壳工程之后,我们的壳只关心配置选项修改(很少),与依赖版本号的变化。业务代码的正常PR流程转移到了各自的业务组件git中,以此来杜绝人工与机器的冲突。
壳工程分离的意义主要有如下几点:
- 为自动集成铺路,避免业务PR与机器冲突。
- 提升效率,后续Pods往Pods移动代码比proj往Pods移动代码更快。
- 制作二进制组件、加快编译速度、提高研发效率
八、多套完全隔离环境
1、什么是多套完全隔离环境
目前我们提供了三套二进制组件的环境,Dev、Debug_iPhoneos和Release_iPhoneos,分别对应开发人员使用的环境dev、Ci打包使用的Debug、Release,三套环境完全隔离,分别对应不同的私有源仓库、二进制文件存储服务,且互相不干扰。默认是Dev环境。
- Dev 下是 Deubg 设置编译的 x86_64 armv7 arm64。
a. 二进制文件服务器:http://xxx:10240/frameworks/
b. 二进制私有源仓库:https://xxx/binary_spec_dev - Debug_iPhoneos下是 Deubg 设置编译的 armv7 arm64。
a. 二进制文件服务器:http://xxx:9192/frameworks/
b. 二进制私有源仓库:https://xxx/binary_spec_debug_iPhoneos - Release_iPhoneos下是 Release 设置编译的 armv7 arm64。
a. 二进制文件服务器:http://xxx:20480/frameworks/
b. 二进制私有源仓库:https://xxx/binary_spec_release_iPhoneos
2、为什么要多套
我们日常ci打包也分Debug和Release。
Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,以为开发人员提供强大的应用程序调试能力。
而Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利。
Dev是专门提供给研发人员使用,x86_64是针对x86架构的64位处理器,iPhone5s及以上,致力于解决开发人员build的效率问题。
九、配置与开发使用
1、使用二进制组件配置
1.1、本地配置文件 - Podfile_local
本地组件配置文件 Podfile_local,目前已支持Podfile下的大部分功能,可以把一些本地配置的语句放到Podfile_local。
场景:
- 不希望把本地采用的源码/二进制配置、本地库传到远程仓库。
- 避免直接修改Podfile文件,引起更新代码时冲突、或者误提交。
- Pod开启多线程,加快Pod速度
用法:
在与Podfile同级目录下,新增一个Podfile_local
文件
#target 'Seeyou' do 不同的项目注意修改下Seeyou的值
#:path => '../IMYYQHome',根据实际情况自行修改,与之前在podfile写法一致
plugin 'cocoapods-imy-bin'
#是否启用二进制插件,想开启把下面这句注释去掉
# use_binaries!
#需要替换Podfile里面的组件才写到这里
#在这里面的所写的组件库依赖,默认切换为【源码】依赖
target 'Seeyou' do
#本地库引用
#pod 'IMYYQHome', :path => '../IMYYQHome'
#覆盖、自定义组件
#pod 'IMYVendor', :podspec => 'http://覆盖、自定义/'
end
以前的 pod update --no-repo-update 命令加个前缀 `bin` 变成
pod bin update --no-repo-update
or
pod bin install
支持 pod install/update 命令参数
并将其加入 .gitignore ,再也不用担心误提交或者冲突了,Podfile_local 中的配置选项优先级比 Podfile 高,支持和 Podfile 相同的配置语句,同时支持pre_install or post_install。
如果想使用二进制组件,加快编译效果的话
#取消注释这两句代码,具体的命令解释查看后文
plugin 'cocoapods-imy-bin'
use_binaries!
如果您不习惯Podfile_local的使用方式,可以把命令写在Podfile里面,pod时不需要加bin,依旧是 pod update/install。
2、制作二进制配置文件-BinArchive.json
有些组件在不需要被制作成二进制组件,可以通过在Podspec同级目录下,添加BinArchive.json文件。
{
#制作白名单
"archive-white-pod-list" : [
"Seeyou",
"IMYFoundation",
"IMYPublic"
],
#忽略源码存储在git上的组件被制作为二进制组件
"ignore-git-list": [
"git@xxx:Github-iOS"
],
#忽略源码存储在http上的组件被制作为二进制组件
"ignore-http-list": [
"https://xxx/Github-iOS"
]
}
十、GitLab-Ci 配置
1、是什么?
GitLab CI 是GitLab内置的进行持续集成的工具,只需要在仓库根目录下创建.gitlab-ci.yml 文件,并配置GitLab Runner;每次提交的时候,gitlab将自动识别到.gitlab-ci.yml文件,并且使用Gitlab Runner执行该脚本。可以暂时理解为 Jenkins微服务。
目前我们考虑用Gitlab-Ci来为各个组件独立配置'打包功能',做二进制组件或者其他一些自动化任务。
2、能带来给我们什么
- 每次提交代码可以独立检查OC-Lint 或者文件监控等功能
- 及时为每个独立组件提供最新的二进制文件
- 加快且稳定每次Ci打包速度
- 独立提供自动化测试和监控功能。
5、对项目的思考
我们的底层库是相对稳定且规范,目前存在部分上层组件,由于所依赖的底层组件分支变动,导致制作出来的二进制组件不一定是全部最新版本的,所以是到目前为止还没有强力推广的最大因素。这也可能是绝大多数公司项目会遇到的情况。
如果能维护好各自组件下的版本依赖是最好的,或者有更好的解决方案?
目前的实现方案是:
- 目前在CI_3的CI机器上部署了定时构建任务,dev开发和Debug_iPhoneos环境在工作日9点到19点区间,分别每1小时、每半小时构建一次,会生成需要被制作的二进制组件,且不生成ipa/dsym/archive等编译产物。
- 如果Gitlab-ci 部署完成后,可以编写脚本在提交代码时来触发这两个相关Jenkins Job执行构建任务,以此来避免有些业务组件未能独立编译通过和所依赖的代码不是最新的问题。
十一、二进制不切换源码库、程序无需重新运行的调试能力
在看了美团 iOS 工程 zsource 命令背后的那些事儿文章后,身受启发,但是美团没有开源相关的项目,就自己琢磨着怎么实现跟他们一样的功能。经过一番倒腾,终于也有了我们自己的zsource。
1、效果展示
使用二进制,虽然会给工程带来构建速度的提升,但是会带来一个新的问题:在调试工程时,那些使用二进制的组件,无法像源码调试那样看到足够丰富的调试信息。例如,如果程序在二进制组件的代码中崩溃,我们只能看到该组件的堆栈信息和一些不明所以的汇编代码:
和业界大多的组件化方案类似,美柚App 的组件化方案也提供了将一个组件从二进制切换到源码的机制。美柚工程的开发者能够使用一系列配置和命令来切换组件的源码和二进制状态,但每次切换都需要重新执行 pod install
。这种方式在组件化的初期是没有什么问题的。但随着美柚 App 的组件数量不断增长,即便是只切换一个组件的状态,单次 pod install
的时间也增长到了分钟级。而且这种方式每切换一次就必须重新编译运行一次 App,在追查一些偶现崩溃问题时,开发体验非常不友好,也不利于崩溃问题的快速定位分析。
为了解决以上提到的这些问题,我们利用 CocoaPods 的插件机制,为 CocoaPods 的 pod
命令增加了 bin code
子命令,开发者可以在使用二进制构建工程的同时,非常快速地将一个组件调出源码进行调试,具体的使用效果可以看一下如下的屏幕录制:
2、如何使用
在功能根目录下,输入命令:
pod bin code IMYFoundation
IMYFoundation
为需要源码调试的组件库名称。成功之后像平时一样单步调试,控制台打印变量。让我们同时拥有使用二进制的便利和源码调试的能力。
十二、遇到的问题
1、跨组件宏定义
预编译阶段处理的宏定义,在组件进行二进制化后会失效,特别是某些依赖 DEBUG 宏的调试工具,在二进制化之后就不可见了。
个人建议尽量不要跨模块使用宏定义,特别是可以用常量或函数代替的宏。比如有组件 A、B ,B 依赖 A,它们包含如下代码:
// A
#define TDF_THEME_BACKGROUNDCOLOR [[UIColor whiteColor] colorWithAlphaComponent:0.7]
// B
// .m 使用了 TDF_THEME_BACKGROUNDCOLOR
假设 A 和 B 都已二进制化,假设后续我们修改了 A :
// A
#define TDF_THEME_BACKGROUNDCOLOR [[UIColor whiteColor] colorWithAlphaComponent:0.4]
由于 B 中的 TDF_THEME_BACKGROUNDCOLOR 宏已经在二进制化打包预编译时被替换为 [[UIColor whiteColor] colorWithAlphaComponent:0.7]
,所以 B 并不会感知到此次 A 的变更,这时我们就不得不重新打包组件 B 以同步 A 的变更,即使 B 并未做任何更改,当存在较多使用 TDF_THEME_BACKGROUNDCOLOR 宏的组件时,就容易遗漏同步某些组件。
十三、未来计划
- 使cocoapods-imy-bin成为一个工具平台
- 服务内集成代码检查,资源检查等功能
- 服务内集成初步自动化测试、性能监控等功能
- 完善优化GitLab-Ci 各个组件脚本,最大限度降低编译速度曲线幅度
- 实现资源共享,抹去copy pods Resource近34%的耗时,进一步降低编译时间到一分钟内。
- 共创一个更强大的平台工具库Github开源地址
十四、总结与感谢
在自学ruby后写的cocoapods-imy-bin插件,在经过刻骨铭心的努力下终于完成第一阶段的设想,其中涉及到的细节实现非常之多,本文只是讲了个大概,还有一些设想待去实现与完善。总体来说Cocoapods-imy-bin对研发效率提升是非常可观的。
感谢大家的阅读以及对美柚技术的持续关注,同时也感谢各位大佬的信任与支持,才能让这套系统顺利地落地实现。
十五、参考文献
用ccache让Xcode运行、打包飞起来
基于 CocoaPods 的组件二进制化实践
美团 iOS 工程 zsource 命令背后的那些事儿
Github开源地址
作者简介
苏良锦,美柚 iOS 工程师,2019 年加入美柚。