导读
背景
“木星计划”演变
业务发生改变后,通过拷贝代码迭代一两个版本后,发现当前的人力不足以支撑整个业务线的发展,迭代的速度也会受到影响。
为了从根本上解决这一问题,协同58 无线团队、58 房产团队、前端、后端等多个团队的同学对 App 的架构升级。最终要求一套业务代码开发一次能够在两个 App 上同时运行,即能够“多端复用”。人力不变的情况下,开发效率翻倍,又称「木星计划」。下面简单介绍其演变过程。
1. 安居客 App 与 58 App 现状分析:
图1:安居客 App 与 58 App 差异对比图
通过上图的简单分析发现问题有:
1. 底层库不统一, 三方库版本不一致。
2. 同一业务之间存在差异。
3. 组件化不完善,平行业务依赖,壳工程存在业务代码。
存在的最大的问题在于安居客 App 与 58 App 两个不同的平台差异所带来的影响,为了解决该问题,引入中间层来抹平差异,屏蔽底层细节,同时对外提供统一的接口。每一个中间件执行权利传递给下一个中间件并等待其结束以后又回到当前并做别的事情。
设计如下:
图2:新增中间件设计图
新增中间件最主要的任务是与 58 房产的团队定义规范的接口,以便业务方的调用和后期的扩展。接口定义完成后由对应的开发实现即可,把实现拆分为三个模块:安居客服务、58服务、房产业务中间件。在开发中间件的同时,对项目的架构进行升级,对基础组件进行梳理,避免重复造轮子,取长补短。统一底层的架构,并保证三方库 SDK 版本是一致的。
最终的架构图如下:
图3:当前App 架构图
2. 多端复用
在「木星计划」中,涉及到最多一个名词就是“多端复用”,从一端到多端,开发思维发生了巨大的变化。
所谓的多端复用指的是:
相同的业务有多个入口。像二手房业务线同时存在安居客 App 和 58 App 中。
基础能力保持一致。像二手房、新房、租房、内容几个大的业务线,会依赖很多底层服务,比如 IM 、地图、网络通信、路由等,所涉及到的底层服务都需要保持一致。
多端复用当前的实现思路是:
图4:多端复用设计图
如上图所示,主要突出“多端复用”的整体设计思路。简而言之,就是将原有主工程的代码抽出独立组件(Pods),然后各自工程使用 Podfile 依赖所需的独立组件,独立组件再通过 Podspec 间接依赖其他独立组件。
3. 壳工程分离
对比 58 App 发现安居客 App 中的壳工程已发生异味,壳工程分离主要将原来 project 中的代码全部拆出去,得到一个空壳,仅仅保留一些工程配置选项和依赖库管理文件。
壳工程分离的意义主要有如下几点:
壳工程当前身兼数职,过于繁重,拆分后职能更加明确
脱离成私有 Pods,和主业务的形势保持一致,为后续二进制化做铺垫
单独的 Pods 往往要比 project 代码层面上要更容易管理
安居客 App向58 App 开发环境靠齐,降低适配成本
首先看下壳工程分离的演变历程:
图5:安居客App 壳工程分离演变图
整个演变过程分为三步走:
第一步:联合 PM,先进行业务梳理,评估其影响范围,同QA 进行协调,保证当前的风险可控,确定好测试周期。
第二步:由于该部分内容历史悠久,存在不可控的风险,首先对代码进行分析,删除无用代码、废弃代码、重复代码、资源文件等。对壳工程的代码进行清理,壳工程仅保留一些工程配置选项和依赖库管理文件。涉及到所有代码,整体打包成Pods 独立出去,基于所有的业务层之上(暂定为「共有业务层」)。并修改项目配置,回接原有的代码。
第三步:对「共有业务层」进行拆解。其中主要分为平移 -> 下沉 -> 回接三个步骤。最终「共有业务层」被分解到适合的组件,只剩没有任何业务属性的AppOnly ,与新房、二手房等业务处于平级关系。修改脚本等配置文件,确保独立出去的 Pods 能够正常运行。
平移、下沉、回接具体解释?
1. 平移:是平行业务的移动;当前未细分业务的拆分,比如之前 project 文件中的个人中心模块独立单独的业务模块,以私有 Pods 形式对外提供;已存在业务的拆分,比如之前project文件中的话题移动至内容模块、首页移动至二手房等。
2. 下沉:是将处理后的代码解耦并独立处理,移动到下层的 Pods 或者下层的 SubPods。比如之前 project common 文件中的一些工具类下沉到 AIFFrameworks 中等。
3. 回接:以 Pods 依赖的形势引用之前下沉的模块,引用后删除平移前的代码文件。
需要注意的点:
1. 在做技术项目的同时,业务也在开发,平移代码的时候要考虑到业务开发带来的影响,注意这段时间的 diff。
2. 可以使用OCLint 提前对代码进行扫描、依赖分析。精确的控制时间成本。
4. 构建速度优化
图6:打包机构建速度折线图
编译速度慢,在打包或者编译的时候就会做一些其它的事情,导致工作很容易被打断,打包速度慢的话不仅仅影响开发,还影响整个团队的效率。以下从日常开发和持续集成两个场景中去分析导致打包慢的瓶颈所在,优化前打包机构建一次需要38 分钟,优化后 13 分钟;优化前全量编译一次需 1400s 左右,优化后 370s 左右。(基于 MacBook Pro (13-inch, 2017, TwoThunderbolt 3 ports))。
问题1:执行 pod install \ update 时,Git download 速度慢,大幅度的降低了持续集成的速度。
下图可以看到该仓库 .git 文件大小, .git 文件大小很大程度上决定了 git clone 的速度。
提交代码时,git 会做 diff,除代码之外的资源文件就会导致 diff 失效,很大程度增加了 .git 文件的体积。为了解决这一问题,一开始大家在开发的时候使用 cocoapods-links 进行注入,pod install \ update link 到本地的仓库。为了从根本上解决大家的问题,对集成脚本进行优化,支持多种方式的装载,其中包含自定义Pods 路径。
不仅 weibo_ios_sdk 有这种问题,很多仓库,包括自定义仓库时间久了都会遇到这种问题,通过该方式很大程度解决了全量 clone 的问题。
如图所示,各业务独立成组件后,相应的图片资源也会移动至 Pods 内,每次编译的时候会执行脚本拷贝资源到主工程。但是编译过程中,没有修改的图片资源文件的情况下,并不需要每次都进行拷贝,现在的做法是,添加编译设置,默认是开启的,正常情况下全量编译打开一次,增量编译关闭即可。省去了大量拷贝资源的时间。
具体实现如下:
def no_copy_resources_if_needed(pod_installer)
pod_installer.aggregate_targets.each do |target|
copy_pods_resources_path = "Pods/Target Support Files/#{target.name}/#{target.name}-resources.sh"
copy_sh_text = File.read(copy_pods_resources_path)
# 这里修改下Pods-xxx-resources.sh脚本,当noCopyPodResources选项打开时,并且处于Debug模式,无需重复生成.car 以及编译XIB
no_copy_resources_sh = "#!/bin/sh \nif [[ \"$noCopyPodResources\" == \"YES\" ]] && [[ \"$CONFIGURATION\" == \"Debug\" ]] && [ -f \"${BUILT_PRODUCTS_DIR}/Assets.car\" ]; then \n\t echo \"没有重新拷贝Pods中的图片以及XIB资源,如需拷贝,请在Build Settings -> 最下面 ->noCopyPodResources 去掉 YES\"\n\texit 0 \nfi \necho \"你好,CocoaPods正在为您服务(编译xcassets、XIB等), 请你耐心等待\"\n"
new_contents = copy_sh_text.gsub('#!/bin/sh', no_copy_resources_sh)
File.open(copy_pods_resources_path, "w") {|file| file.puts new_contents }
end
end
问题3:源码编译,部分源码文件编译时间过久,累计在一起带来很多不必要的开支。
在组件化的前提下,对组件进行二进制化,开发根据自己的需求选择源码还是二进制库的方式去装载。由 Jenkins定时自动构建并上传到 gitlab,确保当前的二进制库最新的。按照 iOS 端 pod install 这个过程,cocoapods 为我们预留了钩子:PreInstallHook.rb、PostInstallHook.rb,允许我们在不同的阶段为工程做一些自定义的操作,所以我们在设计时也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:prebuild、build、postbuild。定位好了问题,确定了什么时候做什么事情,接下来就要讨论怎么做才合适。
对整个优化方案和时间减少百分比简单的小结一下:
图7:构建速度优化方案耗时比较
优化方案简述:
1. 将源码 pod link 到本地,git clone 由全量转为增量;
2. 组件提供源码和二进制两种加载方式;
3. 只生成 armv7 这种最老的指令集;
4. 删除无用代码,保留调试信息,函数内联等。
工具篇
1. Git-repo
问题:
随着业务的拆分,项目的 Pods 越来越多,开发一个功能要涉及多个 Pods,并且需要手动提交 merge request 供团队 review。当开发涉及仓库越多的情况下,涉及的时间成本也就会成倍提高。
当前效果:
* [x] 根据用户可生成 PRIVATE_TOKEN、USER_NAME 完成 gitlab config
* [x] 可提交代码并生成 mergerequest,并复制到粘贴板,无需打开网页提交
* [x] 支持一个或者多个仓库同时提交处理
* [x] 可自定义配置本地分支以及远程分支名称
* [x] 支持「58」和「安居客」两个项目提交
* [x] 命令行联想仓库名称后续有待完善
2. 打包平台优化
一套代码两套平台,由于「58」和「安居客」时间集成上存在着将近一周的时间差,一周的时间差将会带来以下的问题:
1. 在开发安居客新的需求同时,需要修改集成到 58 上的问题,导致两个项目相互切换的问题。
2. QA 在进行测试的时候,由于代码未集成,测过安居客后,在 58 上还需在测试一遍。
对于以上问题,对打包机进行优化,开发完新的需求提测时,在原有输出安居客内测包的同时输出 58 内测包,对于提测内容 QA 可同时测试两个平台,进行集成后回归测试即可。
总结与展望
工欲善其事必先利其器,团队不仅开发了以上的工具,而且在大家的努力下,还做了以下工具:
* link map:由于代码结构的重大调整导致之前的代码分析工具已经不能够准备的分析包的大小,对此进行修改实现各个业务线包大小的分析
* fastlane:使用 fastlane 实现 iOS 的持续集成,将 App 发布到App Store,节省了大量的等待时间。
安居客 App 走向平台化后,通过壳工程的分离,模块间进一步解耦,不同模块的职能更加明确。通过对构建速度的优化,节约了同事们开发时的编译时间,把与编译环境搏斗的时间节约出来进行业务的开发与思考,从而进一步提高开发效率。
以提效为第一要义。如果在开发过程中经常做手动重复多次的操作就要考虑有没有自动化替代方案。做技术优化的同时需要兼顾业务的开发。在优化过程中,周期性和正常开发分支进行 diff,以防落后业务代码太多。
充分调动团队的积极性。个人的想法毕竟是有局限性的,方案设计需要在团队内多沟通,集思广益,不断优化设计方案。众人拾柴火焰高,link map 和 fastlane 等其他小工具就是团队内其他小伙伴的优秀成果,大大加快了优化进程。
iOS 走向平台化后,优化的空间很大,目前这个程度也只是刚开始而已。后面有很多的事情可做,比如说,后期随着业务不断发展组件足够多的时候,编译隔离就显得尤为重要,考虑其成本是不是可以支持?目前增量编译的时候是通过开关项来强制关闭资源的拷贝,但是打包机默认是开启的,后期可以尝试资源编译缓存,和源码增量编译一样,仅处理发生变化后的条件,还有包大小问题也同样迫在眉睫。
业务和技术是两个并不矛盾的东西,只是它们就像在天称两端的东西一样,相辅相成。我们在做业务的同时需要不断的去发现问题、解决问题。
参考文献:
1. [CocoaPodsGuides](https://guides.cocoapods.org/using/getting-started.html)
2. [Cocoapods-under-the-hood](https://www.objc.io/issues/6-build-tools/cocoapods-under-the-hood/)
3. [安居客Android APP 走向平台化]
(https://mp.weixin.qq.com/s/71VfmQ5ZyihgTwosMPbSmw)
4. [移动开发已进入 App 工厂时代!]
(http://www.myzaker.com/article/5e15708a8e9f096241601c62/)
5. [百度AppiOS工程化实践: EasyBox破冰之旅]
(https://developer.baidu.com/topic/show/290273)
作者简介:
潘显跃:iOS开发工程师,主要负责安居客和 58 房产业务的开发
推荐阅读: