好久没有写文章了这里记录一下把项目代码二进制化提高编译效率的整个过程中碰到的问题和解决方案
先提一下优化编译速度的基本方向基本就是从不同的编译阶段来出主意,比如:
- 预编译阶段的头文件查找:
一款可以让大型iOS工程编译速度提升50%的工具 - 利用cocoapods管理工具生成静态库,减少编译时间。
- 资源文件整合,减少copy所需时间。
等等。
iOS组件二进制方案
现有iOS组件二进制方案:
- Carthage
iOS的另外一个包管理器,Carthage可以将一部分不常变的库打包成framework,再引如到主工程,这样可以减少开发过程中的编译时间。Carthage 可以比较方便地调试源码,不过从目前角度来说对业务侵入太高 - Cocoapods-Binary(Cocoapods 官方推荐的二进制插件)
单私有源,无法实现服务端缓存,在没有对应二进制包版本时,pod install 后会额外去做二进制包的生成,一定程度上会影响 pod install的速度。开发者切回源码调试,二进制缓存会一并清空,需求重新编译。只支持framework,对我们项目现状需要比较大的头文件引用方式改动 - Cocoapods-imy-bin (Cocoapods-bin的升级版)
该插件进行二进制化的策略是采用双私有源,即2个源地址,一个静态服务器保存预先打好包的framework,一个是我们现在保存源码的服务地址,在install的时候去选择使用下载那个。虽然这个插件在使用上确实会有各种各样的问题,但是自己再此基础之上再改改源码还是很不错的,不然全部重新自己写都不知道要写到啥时候去了。在这里感谢美柚老哥们的开源。
基本确定使用双私有源的方案,因为单私有源的方案除了上面的问题以外,还需要对现有所有podspec进行改造,让目前所有组件的podspec同时支持源码和二进制,做法就是新增subspec:Binary,设置podspec中的–default-spec:Binary,然后把原来的source更新成subspec,但是在组件开发的时候需要肯定要切换到源码的方式,然而podspec文件是肯定的要纳入git管理的,这样会导致整个开发流程严重的依赖使用者的手动管理。因为这里面还有一个非常关键的源码和二进制切换的问题。
插件打包工作流程
业务使用流程:
全自动依赖二进制库,只要远端有,如果没有相应的二进制库则使用源码库来源码打包,业务使用无感知。
源码调试工作原理
iOS组件二进制完以后在debug的时候看到的都是arm64汇编代码,非常不利于查找bug的产生原因,因此需要提供一套源码与二进制及时调试的方案。下面提到的方案都是能在debug状态下实时进行源码调试的,至于那种通过pod install来切换PodA.的bin和source的方案,不在此描述了,因为这个插件自带了,但是说实话这个方案很难满足开发需求。
目前我了解到的实时的源码调试方案有两套。
-
一套是美团ZSource背后那些事
重点是在下面这种图,debug生成的macho文件是带有debug信息的,都在_debug_str段展示出来了,只要你在相应的目录下放入特定的源码文件可以在debug的时候无缝切换源码查看了。这个其实和release版本生成的dsym功能是类似的。
-
另外一套就是使用lldb自带的命令把源码的路径和二进制包的路径进行map,这里可能需要写一些脚本。两套方案其实大同小异了。
源码查看方案
以上方案只能解决二进制代码转到源码进行调试,无法解决源码查看的需求,实际应用中我们肯定是有大量的源码查看需求的,甚至比源码调试还多。目前有一套我个人感觉比较完美的方案,就是在工程中同时集成SourcePods,二进制Pods和主工程,但是源码Pods不纳入编译的整体流程(需要改造一下cocoapods),然后使用lldb的map命令将编译流程中的二进制代码地址映射到源码仓,这样就能完美的保留工程二进制化前通过点击代码定义就能跳转到源码仓查看源代码,又能在代码调试断点能跳入sourcepods仓中。
二进制化过程中碰到的问题
问题1:环境宏
iOS中宏是预编译阶段就会被完全替换的,所以目前依赖于特定的环境宏,比如Debug,is_DEV的组件代码在编译成二进制会只包含某个特定环境下的代码,比如archive打出来的包肯定没有debug宏包含的相关代码,所以这样的组件在不同打包环境下是不可以通用的。会报出一些符号undefined的错误。
解决方案:
抽离所有的环境宏到一个特定的组件中,将对环境变量宏的引用改成对组件中的方法调用,比如#is_dev 改成使用[xxx is_dev],但是这个方案改造量比较大,而且有些第三方组件会使用Debug宏,这些也需要同时被改造。
问题2:podspec中的subspec模块怎么处理
目前工程中有好几个组件的podspec都使用了大量的subspec,这就引发了一个问题,是对每个组件的每个子组件生成一个二进制包还是只对一整个podspec生成一个二进制包,对每个子组件生成一个二进制包的好处就是使用podA/subspec的部分引入的时候只会引入一个子组件二进制包,不过其实链接静态库采用的策略本身就是按需引入,所以其实使用组件包和子组件包最终代码大小上不会有区别。但是由于对静态库符号的查找的时候采用的是遍历,所以对链接时间可能会有一点影响。最终我自己选择了生成一个大的二进制包,避免麻烦,并且和xcode目前的编译系统保持了一致。