@qinyun
2018-03-12T17:48:20.000000Z
字数 5727
阅读 2863
未分类
随着滴滴出行业务的不断拓展,客户端开发团队人数激增,代码量急剧膨胀,同时开发人员存在物理地域的不同(北京、上海、杭州),在如此前提下,如何保证稳定高效的协同开发?如何才能高效的进行代码复用,持续集成和按需集成?工程涵盖业务组件繁多,代码量巨大,单次构建耗时长,一个组件出错就要重新再来,如何避免这种情况?
滴滴出行APP架构组架构师王涛根据滴滴出行乘客端架构演进历程以及实践经验,在2017年ArchSummit全球架构师峰会北京站上分享了滴滴出行对以上问题的解决方案,并介绍了滴滴出行主推的构建平台的设计思路、现状和未来的规划,以下是分享的全部内容。
滴滴出行1.0版本功能比较简单,就是一个供需的匹配工具,业务比较单一,开发者也比较少,所有代码都在一个工程里面。
到了2.0版本,功能上仍然只有出租车一条业务线,但是产品形态发生了一个变化,变成一个交易平台,而代码仍然是以大锅饭的方式进行开发和集成。
到了3.0版本,产品出现多元化,新增了专车业务线,代码出现了初步的膨胀,但是因为这个时期只有两个业务线,集成构建仍然不是什么大问题。
在4.0版本,我们遇到了很多挑战和痛点,这个阶段被定义为平台化整合阶段,由于在这个时期竞争对手的出现导致应用需求需很快上线。时间短、业务重,快速上线造成了工程代码极度膨胀,高度冗余。在这个时间段滴滴出行和快的打车又进行了合并,开发者出现跨地域分布,因此跨地域的团队协作,资源分散在各地,团队间沟通和交流都出现了很大的问题。
对此,在每次发布新版本之前,我们都会预留一周时间进行代码合并。在这个时期我们成立了一个交流群,叫协同发版群,大家都在群里沟通代码要怎么合并的问题,而且这个时期的QA测试遇到了很多的灾难性问题,因为代码是高度耦合的,任何人只要修改了任何一条代码,都要进行一次回归测试,这个时期让我们很头疼,效率很低。
随着业务线增加,代码功能开发时间和代码合并时间的关系如下图的柱状图,功能开发时间的曲线是稳步上升的,但是代码合并所需时间出现了指数级的上升,这是失衡的。因此在架构上我们向组件化迁移,针对每个组件推进了组件工程化,使每一个组件都有一个自己独立的工程,解决了协同合作的问题,提升了开发效率,此时的架构也升级到了5.0阶段。
在平台组件化的阶段,随着后续的业务迭代和功能开发,乘客端里的组件有了高达140多个,同时出现了新问题,我们集成了这么多的组件,如何进行有效管理上升为一个很大的问题。组件越多,原代码越多,编译就需要很长的时间,这对于开发者来说,体验是非常不好的。
然后我们对构建系统进行了再次升级,推出了OneTool工具,标准化壳工程改造,是我们的壳工程经历了合久必分,分久必合的过程,以及基于Cocoapods的预编译优化。
这个是我们目前的基本架构图,大家可以看到下面是基础平台的部分,包括了一些通用的运行时组件,一些基础组件还有业务组件,在平台之上会有不同的APP,每个不同的APP加入自己的个性化组件,包装成一个APP。
由于滴滴出行有十多条业务线,构建系统面临一个最复杂的环境。因此我们首先要做的就是标准化壳工程。
在4.0阶段,我们推进了组件工程化,在当时很好地解决了我们的问题,提升了开发效率,但随着版本迭代,也出现了一些不适应的问题:
由于每个组件都有独立的工程,各自就会加入自己的配置,以及加入自己特异性的脚本,这样就会出现一个问题,组件在自己的工程里面可以正常使用,但是集中到总工程里面就无法使用了,导致无法集成。
组件工程化对新入职的开发者并不是十分友好,任何一个开发者加入一个公司之后可能负责4-5个组件进行开发,每个组件都有独立的工程,他们要同时接触5个不同的工程的创建和管理,对他们来说初始的挑战很大。
还有Debug的问题, Pods 文件夹下的代码是没有版本控制的,因此在我们修改了一些代码之后,这些修改的代码没有办法直接提交到组件仓库里的,如果BUG比较严重的话,修改的代码可能比较多,某一个开发者可能花了一天时间把错误解决了,但是他想把修改的代码往自己的组件库导入的时候,可能已经把修改过的部分给忘了。
因此,我们推翻了组件工程化,开始创建一个标准化的壳工程。
上图是标准壳工程的大概模型图,可以看到它分为两部分:一是本地的部分,这一部分开发者可以进行自由改造,还有一部分是同步的Workspace,这是不允许开发者进行修改的。
如果组件的开发者要想开发组件也很简单,只需把自己的组件克隆到Workspace里面,将自己的组件以LOCAL PODS方式加载进来就可以了。
标准壳工程的创建过程分为六个步骤:创建一个本地的Workspace,克隆同步的Workspace到本地的Workspace里面,创建一个基本的配置文件 ,第四步从Workspace里面拷贝一些文件到本地的Workspace里面,生成 Podfile,执行 pod install,最终生成如图所示的本地的目录结构。
上图中间这一部分就是本地的Workspace,然后选中的右边是同步的Workspace,这其中有一些文件和文件夹是相同的,这就是第四步操作的结果。
以上的步骤还是很烦琐的,因为创建这样一个工程需要六步,每一步都有很小的细节,如果有程序员自己一步一步手工操作的话,非常容易出错,但这却是我们进行工具自动化的基础。
对于工具自动化,我们自研了OneTool,这是一套命令行工具,最初目的是使iOS开发构建更加简单快捷。下图就是基本的命令,其中就包括了前面的创建一个标准壳工程。
OneTool这个工具已经是整个构建系统的桥梁,目前整个构建系统中任何一个环节都离不开OneTool的存在。
OneTool包括两个部分,第一部分是One Commands,这是由开发者主动调用的。还有另外一个部分是OnePods,它是由 Cocoapods 主动调用的。
OneTool很好地解决了开发者使用繁琐步骤创建工程的问题,减少了操作,但它毕竟是一个命令行的工具,所以它并不是很友好,同时每一个开发者的电脑环境是不能保证相同,在使用过程中就会出现各种各样的问题。
所以,我们基于OneTool开发了一个GUI 工具,就是OneApp,它把所有OneTool的命令都集中到这个界面上,降低了使用门槛。OneApp除了基本功能以外还提供了对所有的Workspace统一管理的功能,图中左侧展示出了所有的Workspace,右面的主要区域是展示选则的Workspace的Pods的基本情况,它可以对任何一个pod进行任何信息的修改。
OneApp的工具推出之后,在公司内部将成本基本降低到了零,在最初我们没有工具,仍然是以原始创建工程的方式创建工程时,曾经发生过这样的事情,公司一个新同学入职的时候,第一件事就是配个可以编译的Workspace,由于我们的Workspace非常复杂,他配置了一个星期也没能将工程 run 起来,最后那个同学直接离职了,但是现在这样的问题已经不存在了。使用这个工具之后,新同学在入职十分钟之内就可以创建出一个可以运行的Workspace,目前除了iOS开发者在使用它以外,QA和 PM也在 使用用,PM为什么会用?前一阵iPhoneX出来之后,我们要做适配,PM可以拿出这个工具,通过简单操作配出一个工程,就可以看到适配之后的结果。
如果使用组件化的架构体系,首先尽早建立标准化壳工程,这样会减少很多的负担。其次要统一组件库配置文件规范,也就是podspec文件,还要善于使用xcconfig文件。最后依赖配置文件的权限要控制好,一旦一个开发者误操作将文件进行了修改提交上去,可能就会造成灾难性后果。自动化和工具化是以上工作最好的实现方案,因为手工去做的话很可能会出现一些问题。
滴滴出行的工程初次编译时间有着将近一个小时,假设有300多个开发者,每个人编一次工程,就需要300小时,所以编译时间优化是势在必行的任务。相同的工程在使用预编译优化之后,一次编译时间缩短到5分钟,带来了效率上的极大提升。
预编译优化的实现方案,第一步使用一个纯源码的工程,编译出静态库文件,通过vendored_libraries 加载静态库文件,找到 podspec 文件修改一些参数,以及文件的地址,最后将修改之后的文件与静态库放在一起打包,就制作好了预编译包,将这个包上传到服务器,使用时根据需要将这个预编译包下载下来,集成到工程中,就完成预编译包的创建和使用。
我们的预编译包也有自己的发布流程,每个组件的开发者开发好以及本地测试OK之后他们会将预编译包进行提交触发一下的流程。
当源码中有预编译宏的话就会出现问题,预编译是将代码提前进行编译,最后是使用静态包的,由于我们编译的环境和运行环境很可能是不一样的。比如if_has_include宏,我们在编译的时候有某个头文件,实际运行中没有头文件,这样会直接造成功能的缺失或者crash。
如果组件中定义了subspec,前面的预编译包的创建流程是不ok的,做出来的包是不能使用的。滴滴出行最早是不支持subspec的,一旦遇到subspec这个库只能以源码方式进行集成,后来经过不断的踩坑填坑过程,如今完美的完成了subspec的支持。
使用结构体和联合体,这种在使用预编译的时候也会有一个隐讳的问题,举个例子有两个库:地图库、依赖地图的导航库,地图库里面定义了C语言的结构体,导航库在刚开始使用时,可以完美地运行,当在某个版本地图库将结构体进行修改,新增一个字段,没有通知导航库,这时候在编译阶段因为可以正常编译通过,因为导航库并没有使用这个字段,但是一旦到了线上就会直接造成crash。因为线上运行的时候内存结构不一致,就会造成错误。在这里有一个避免的方式,大家可以参考一下系统库的结构体的实现方式,由库自身提供创建结构体的方法,比如 CGRectMake,使用这些方法创建结构体就可以解决使用时内存结构不一致的问题,还有如果podspec编写不规范,随意写的话,修改出来的包也可能不能正常使用。
最开始的时候用的方法比较低端,人肉merge效率极低、错误后置,之后我们接入了jenkins,在每个开发者提交了一个tag之后,会进行一次编译检查,如果检查失败的话就不提交tag。因为Jenkins每次只能添加一个组件,每个组件都要创建单独的job,最后Jenkins job 数量就爆炸了。如果多个库相互之间有依赖的话,一次需要集成三到四个库,而Jenkins自身一个job只支持修改一个组件库,这时候Jenkins就处理不了,又会回到手工修改这种方式。由于人肉手工修改没有检查的保证,很可能出现错误。
那么有没有更好的方式进行持续构建和集成呢?有!那就是集成构建平台,首先集成构建平台和Jenkins相比有着更加友好的用户界面,另一个是集成构建平台支持各种自定义方式去集成构建,开发者可以想集成几个组件就集成几个,针对任何一个组件库都可以进行自定义的设置,包括预编译包的属性设置等。
另外集成构建平台相比Jenkins会增加一个统一管理所有组件的功能,借助这个功能,在基础平台上通过不同的组件的集成就会包装出不同的APP,即支持多APP的集成,集成构建平台还有一个功能就是会对每次集成构建创建快照,根据需要的时候能进行恢复,如果一个历史线上的包出现问题的话,可以快速恢复那个包的代码状态,进行调试,集成构建平台的投入使用是人肉手工效率的3倍左右。
任何一个开发者提交了代码之后,会触发集成、创建工程、克隆pod、打新tag、更新依赖、集成工程、编译、集成失败,会修改,如果成功:
目前我们持续集成的过程就是在不停地跑这样的循环,完成整个公司所有APP的持续集成过程。
上图是集成构建平台的界面,他有着比Jenkins更友好的界面,左面是支持的不同端和不同的APP,右面是每次集成构建的历史,上面还有一些APP的管理和组件的管理等功能。
我们集成构建平台的设计思路,主要是为了解决Jenkins的一些问题才推出的,首先它支持各种自定义的需求,以及要支撑多 APP 的集成,还要统一管理公司内部所有的SDK,不能只针对iOS一个端使用,一定要多端通用。最后集成构建平台的各个用户权限一定要有明确的划分。
公司内部除了集成构建平台之外还有其他的平台,比如说测试、发版、数据统计平台,这些平台都是相互独立的状态,我们推出集成构建平台之后会将这些平台全部打通,从最初的构建情况,到最后直接上线、数据收集,完全都是打通的。目前,集成构建平台已经上线一个多月,已经有数次稳定发版,已经接入了滴滴出行乘客端和企业端,打通平台的有发版平台和OneTool。
未来要做的就是要流量全切,现在仍是灰度发布状态。一部分使用集成构建平台,另外一部分做老的Jenkins方式,未来要接入公司内所有端,以及和其他平台进行打通,未来开发的重点是要做iOS和安卓的一致性开发,目前在这两个端上,整体开发流程是一致的,但在某些细节上iOS和安卓还有些区别,比如说iOS天生是支持源码的,经过修改之后才支持了预编译的包,而安卓天生是支持预编译的包,在修改了之后才支持了源码的编译,未来我们就要做到一致性开发。
王涛,现任滴滴出行APP架构组架构师,在滴滴之前曾就职于百度等知名互联网公司,有多年移动开发经验。专注于移动开发领域,热爱开源,善于解决技术难题。在 Github 上维护有多个开源项目。
加入滴滴后,参与并完成了 iOS 动态化系统的设计与开发,使滴滴出行成为鲜有动态化能力的公司之一。随后又主导了整个 iOS 构建优化与流程标准化建设,开发了一系列集成构建效能工具。设计并推动整个集成构建平台的开发与上线使用。