一、概况经过多年发展,美友iOS项目代码已经达到40W+行,使用的Pod库数量达到110+,AppStore安装包有达到210M+,这么大的项目规模下(CI机MAC配置:3GHz8核IntelXeonE5;时间:release20min+),(开发机iMac:Retina5K,27寸,2017融合硬盘;时间:build30min+)packaging,编译问题逐渐成为我们团队无法回避的痛点,严重影响了我们研发效率和其他团队的协作。我们13年的CI机需要同时承担七八个项目、多个分支的打包任务。当有多个项目同时打包时尤其无能为力。在硬件资源有限的情况下,在不侵入、不影响现有业务的前提下,如何在团队之前解决这些问题成为了我们的迫切需求。包速方案。二、加速编译的探索与尝试1、CCacheCCache是??一个编译缓存,一个可以缓存编译后的中间产物的工具。它的原理是用ccache编译器编译工程的源文件,然后将编译产生的信息缓存起来,以便在下次编译时,利用这个缓存来加快编译速度。目前支持的语言有:C、C++、Objective-C、Objective-C++下图基本说明了CCache的工作原理。在项目中实际编译Ccache的过程中,我们在项目中进行了很多尝试,确实在某些方面大大提高了我们外包的速度。美优iOSCi打包从之前最快20min+变成最快10min,确实能给我们带来比较好的提升,大大加快了我们项目的打包速度。我们的项目跑了几个月后,我们也发现我们项目的情况存在一些问题。现在总结一下几点:优点:满足我们对非入侵的追求,对现有业务需求无影响,无入侵,开发者不知情。确实可以大大提高编译速度,美优项目的编译速度在最快的时候提升了3倍以上。不需要对项目做大的调整,只需要部署相关环境和一些脚本支持即可。无需更改开发工具链。同一目录下,CCache的缓存命中率比较稳定。我们的项目存在一些问题:在没有缓存的情况下,第一次打包编译的时间比原来快了将近一倍,原来20+min,第一次接近40+min,在资源紧张,甚至是70min+。修改一些引用较多的文件(比如修改公共库和底层库),很容易造成大范围的缓存失效,速度会比没有使用ccache时变慢。多个项目的相同组件不支持缓存共享。我们有多个分支包装要求。修改目录名后,缓存将失效。我们机器的Ccache最大缓存限制在18GB左右,Debug/Release缓存不一样。美优iOS项目占用缓存5GB+。多个项目和多个分支很容易超过限制。一台词机同时支持多个项目。CCache清除缓存。对机器硬盘的读写要求高。如果不是全固态硬盘,速度会受到很大影响。CCache不支持ClangModules,AVFoundation,CoreLocation等系统框架,Xcode不会再自动为你导入它们,这会导致编译失败。CCache不支持PCH文件。CCache目前不支持Swift2和探索静态库二进制解决方案。虽然我们已经在Ci中应用了Ccache,将包输出速度提高了将近一倍,但是仍然存在明显的问题。在去年的技术周会上,我们的leader提出了使用二进制编译的自研任务,可以进一步提高研发效率。受到老大的启发后,我一直在二进制之路上实践和探索。我们的项目使用CocoaPods来管理第三方库和私有库的依赖,这应该是大部分项目的标配。目前是纯Objective-C项目,有少量C++,还没有引入Swift。3.研究过的二进制组件方案下面列出研究过的一些主流方案以及最终没有采用的原因。这些解决方案都有其局限性,但也给了我很多启发。思考过程与最终解决方案一样有价值。3.1.CarthageCarthage可以将一些不常用的库打包成一个框架,然后导入到主工程中,这样可以减少开发过程中的编译时间。Carthage可以轻松调试源代码。因为我们已经大规模使用了CocoaPods,改用Carthage进行包管理需要做大量的转换工作,改动太大不符合我们的无侵入不影响现有业务,所以不考虑这个方案。3.2.cocoapods-packagercocoapods-packager可以将任意pod打包成一个静态库,节省了重复编译的时间,并且可以在一定程度上加快编译时间,但是它也有自身的问题:优化不彻底,只第三方和私有Pod可以优化编译速度非常快,其他频繁变化的业务代码无能为力。私有库和第三方库的后续更新非常麻烦。修改源码时,需要重新打包上传到内部Git仓库。二进制文件太多会减慢Git的运行速度(Git的LFS还没有部署)源代码调试困难,不共享编译缓存打包成静态库的过程很慢,需要podlint,以及组件之间的嵌套依赖关系。在现阶段,是很难实现的。3.3.cocoapods-binaryCocoapods-Binary(Cocoapods官方推荐的二进制插件),是一个实时生成并缓存的二进制包,不像CocoaPods-Packager那样只针对单个私有库。其原理是通过CocoaPods提供的pre_installhook在podinstall的prepare阶段拦截当前的podinstallcontext,然后fork一个独立的installer完成预编译源码到Pod/_Prebuild目录的clone,但是有还有几个缺点优点:单一私有源无法实现服务端缓存。当没有对应的二进制包版本时,podinstall后会额外生成二进制包,一定程度上会影响podinstall的速度。当开发者切换回源码调试时,二进制缓存会被清空,需要重新编译。多个项目和不同分支的相同组件仍然无法共享。仅支持框架。我们项目的现状需要对头文件的引用方式做比较大的改动。3.4、cocoapods-bindualprivatesources本插件二进制化的策略是使用dualprivatesources,即两个源地址,一个静态服务器存放预打包的框架,一个是我们所在的服务地址目前存放源码,在安装时选择使用,该用的时候下载,是一个很好的项目,深受启发。优点:源码和二进制文件可以来回切换,速度比较快,不影响没有接入二进制方案的业务团队。当没有二进制版本时,会自动使用源代码版本,接近原始的CocoaPods体验。地方:不支持指定分支,并且:podspec=>'',:git引用对于需要支持多分支多业务线的项目来说是致命的。归档二进制文件时,只能去spec仓库下载源码,不能按照指定分支下载依赖库,导致编译失败和混乱。依赖组件需要推送到spec仓库。很多私有库并没有推送到仓库,而对于频繁变化的私有库,推送到仓库的验证很慢,不符合我们的开发习惯。不支持.a静态文件输出,大量工程如#import"IMYPulic.h"需要编译并一一替换为#import。想想那110多个组件库~只支持一个环境,对于Debug/Release/Dev开发环境是无法满足不支持二进制组件的源码调试的。不能流畅支持频繁变化的业务组件,操作起来会异常繁琐。对于我们的项目来说,目前存在较大的障碍,无法使用。4.思考与总结经过一个多月对业界现有轮子的分析和思考,并经过一定的实践,我们最终决定构建一个灵活的、可配置的、简单的、无侵入的、双重私有源的Binary组件助手插件。接下来,撸起袖子加油干~骚年散,双私有源二进制组件简介受到cocoapod-bin的启发,借鉴其部分框架,实现了自己的二进制辅助插件cocoapods-imy-bin,并添加了几个命令和二进制源代码调试功能。1.可以做什么?只要能编译通过,就会在cocoapods-imy-bin的协助下进行生产,所有符合条件的组件都可以自动生产为二进制,无需入侵,可以方便的应用于业务频繁的二进制组件,没有多余的操作,一切交给cocoapods-imy-bin自动运行。同时,对于研发人员,也可以提供独立的二进制组件供研发人员使用,解决日常编译效率高、真机运行效率低、被墙堵等各种问题。我们的口号是:能编译就生产。一次编译,随处使用,无侵入。即使独立组件库编译失败,整体工程也能编译通过。整个环境下来后,我们的开发人员没有改变原有的开发习惯,没有改动业务中的相关代码,基本达到了用户无感知的状态。2.Ci打包效果2.1单个项目——最快编译时间2分钟上图是我们根据打包上千个包的经验得出的单个项目的大概编译时间图。这里假设一台机器一次只有一个工作。Y轴为编译时间,X轴为某次编译,红线表示原生(未使用Ccache和binary组件),黄线表示使用Ccache,蓝线表示使用binary成分。从图中可以看出,在没有任何辅助的情况下,原生编译时间曲线(红色)趋于平坦,在20分钟左右。第一次在没有任何缓存的情况下,Ccache和binary在一定程度上会比native更耗时。ccache主要是在编译时缓存项目的编译产物,比较耗时。binary主要是编译完成后比较耗时,组装.a编译后的产品,推送到私有源码仓库(这个跟采用有关系,如果不用Jenkins编译后的产品做二进制,它不存在。)。当ccache完全命中,所有二进制文件都存在时,ccache比原来快一倍多,二进制编译时间比ccache快一倍,稳定在2分钟左右。binary之后的性能会更稳定,而且当ccache修改一个经常被引用的文件,比如底层public文件,命中率会大大降低,有时会比没有ccache更耗时,比如#4的位置。当CI有多个作业并发运行时,由于ccache需要对IO进行频繁的读写操作,耗时性能可能会更差。我们经常会遇到需要等待70多分钟才能发布包的情况。.二进制编译时间相对稳定(蓝色曲线)。在我们架构的大力支持下,划分了110多个独立的组件,每个包基本都消耗了某个组件的编译+归档。如果是一些变化比较频繁的组件,我们也可以考虑在粒度较大的组件中加入ccache,做成两层的编译缓存。双层编译缓存的原理是,当Pods组件库没有二进制组件,使用源码编译时,源码编译也应用ccache缓存支持,加速源码组件的编译。同时组件库可以配合Gitlab-Ci的runner应用。每次提交代码都会触发独立组件的二进制生成,这样每次编译速度都能达到最快,蓝色二进制曲线会更接近直线。Gitlab-Ci的具体使用教程,请参考下文。如果存在无法编译的独立组件和版本依赖,也可以运行定时作业或其他轮询条件作业,及时提供最新的二进制组件。2.2.在多项目的情况下,一台机器上多个项目的ccache比较困难,不稳定。如果超过了ccache的最大值,就会被清除。使用二进制后,即使是多个项目的编译时间也趋于相对稳定。这里的原理大家可以想象为什么。3、开发使用效果——10倍以上提升Podfile引入插件后,podinstall/update后,满足条件会自动转为二进制组件。在我们的开发机上(iMac:Retina5K,27-inch,2017FusionHardDrive;),在完整代码之前构建需要30min+,但是现在使用所有二进制后,最快编译只需要2min+,效率是增加了10多倍。当你使用独立组件库进行编译开发时,不妨试试这个二进制方案来运行整个项目。可能二进制方案比独立组件库运行得更快。3.1.源码编译ps:110+个Pods库中,有20+个稳定的Pods库被做成了二进制库,不是所有的源码编译,怎么全部转为源码编译,实际数量会比这个多很多。3.2.二进制编译——全卷最快2分钟Ps:源码编译的有2个Pod和5个ActionExtensions,其他所有Pod都是二进制Pod。在二进制构建(arm64和armv7)的127秒中,除了源代码编译时间外,还有大约45秒用于复制pods资源。实际上,编译模拟器x86_64架构只需要不到90秒。全量编译,13496Tasks/727Tasks,1710秒(28.5分钟)/127秒(2分钟),编译速度提升了10倍以上。3.3演示环境搭建完成后,开发者只需在Podfile中添加如下两句即可享受自动切换二进制组件,体验极速编译的乐趣。插件'cocoapods-imy-bin'use_binaries!4、功能点目前cocoapods-imy-bin插件支持的功能如下不侵入,不影响现有业务。在不影响未连接到二进制解决方案的业务团队的情况下提供配置文件。make只要项目编译通过,即使个别组件编译失败。当不支持二进制版本时,将自动采用源版本。支持制作二进制组件只要项目能编译通过即可,无需关心podlint等。支持podbinlocal命令自动创建、上传、存储项目本地已经存在的二进制组件,可与ci打包的编译产物一起使用。支持指定依赖分支,支持:podspec=>'',:git引用,同时支持.a,Framework静态库输出,支持归档,根据Podfile自动获取podsepc依赖库,无需强制从规格仓库中拉取。支持Debug/Release/Dev配置等多套隔离环境,方便为Debug/Release/Dev环境提供专用二进制组件。它支持输出.a二进制组件和制作无模板的binary.podsepc。支持稳定的二进制组件,在binary.podsepc上传二进制组件时跳过podlint验证,以加快速度。支持podbinauto命令自动一键制作、上传、存储单个二进制组件支持podbinauto--all-make命令自动制作、上传、存储项目下所有组件的二进制组件支持是否使用二进制文件和是否制作二进制文件的白名单设置和二进制/源码调试功能支持podinstall/update多线程模式,加快pod进程,Pod速度提升80%+。支持podbininstall/update命令实现对Podfile内容的非侵入式修改,避免直接修改项目Podfile文件可能导致提交冲突和错误提交。支持podbincode命令,实现二进制库无需切换源码库调试能力,程序无需重新运行
