iOS工作流整体规划报告

 

背景

新的一年开始了,随着公司不断的发展壮大,我们的业务线也随之快速横向裂变。之前的三期工程化方案是针对当时公司单一主包、多业务线合并的思路设计而来的,足以满足当时的使用场景。

但是现在的公司已经开始扩展各种海外单包,并且不同的单包之间还出现了业务上的独立发展,并且由于防止因代码雷同而导致的审核风险,我们必须对现有的代码进行混淆处理才能对安心上线。

在这样的背景下,服务端开始尝试通过通用服务平台化的方式来满足业务扩展的需求。而我们客户端也希望通过整体程序架构的改进和工作流的优化,来满足公司未来产品方向的需求。

计划目标

客户端代码

  1. 解决客户端代码在多业务、多需求、多单包、单版本要求下,开发流程容易阻塞的问题,同时提高代码复用率,减少重复开发成本,到Service为止全部复用公共代码
  2. 维持代码规范与整体架构的纯洁性,避免并行开发造成的架构污染,禁止横向依赖与依赖倒置等情况发生
  3. 提高不同开发阶段间转移的便利性,彻底解决代码合并造成代码丢失问题,并且提供完全组件化下的需求管理方案
  4. 提高开发调试效率,大幅度减少开发运行调试包的时间,全二进制组件缩短到3分钟以内完成运行,并提供二进制下直接查看源码信息能力,避免源码与二进制反复切换
  5. 统一所有开发人员的开发环境,解决由于环境配置导致的各类开发问题
  6. 通过以上几点,实现平台化的技术基础

CI/CD

  1. 引入gitlab-ci提高自动化脚本覆盖率,从原来整体流程覆盖率15%(仅包含组件打包与APP发版)提升到70%(组件代码提交等流程自动触发CI、mr时自动触发代码lint与debug模式打包等,除去MR代码合并与定版发包依旧需要手动操作外,其他全部自动化)
  2. 拆分组件打包与app打包流程,组件打包仅与开发有关,QA使用独立的测试App打包平台
  3. 优化APP打包效率,减少测试打包的时间成本,将原有App打包时间缩减至三分之一,单次打包耗时缩减到3分钟以内
  4. 优化组件打包效率,修改为打包多任务并行处理,并隔离编译环境避免发生错误。同时组件打包开启多服务并行设置,支持打包服务器横向扩展加速打包时间

管理后台

  1. 提供业务组件动态配置网页,可以根据配置直接生成需求App,供QA与产品使用
  2. 提供组件版本与相关需求的配置查询功能
  3. 提供查询需求开发进程与状态管理功能
  4. 提供动态配置如统一跳转路由配置、自动化埋点配置、各类三方sdk密钥配置等统一外部配置管理功能

问题分析

由于业务发展方向的变化,导致了之前的工程化工作已经不能完全满足现有的需求,因此我们认为更新现有业务组织迭代方式迫在眉睫。

这里问题的前提是组件化已经完成,所有需求都在业务组件上进行版本开发。

问题一、旧组件难以并行开发

旧的开发模式是通过组件版本在主版本基础上,添加后缀实现的。组件在开发时不断叠加版本号,然后封板时使用最高的版本号。如10.5.0版本的需求,聊天业务的最终组件版本则为10.5.0.15。在开发过程中,这个版本会一直迭代。

旧的开发流程是确定需求–>开发需求–>迭代组件版本–>提交测试–>迭代组件版本–>封板自动合并最高版本组件。这种开发流程的特点是线性,符合做事直觉。缺点是效率相对较低,开发一个需求的时候可能会出现另一个需求更新了组件导致代码不匹配,需要重新适配的问题,这样多任务并行相对困难,且我们的二进制化也是基于组件版本迭代来做的,旧流程无法区分什么时候应该做二进制,什么时候可以不进行二进制。

问题二、旧组件无法区分依赖层级,容易出现依赖倒置、架构污染

旧的组件共用一个依赖管理源,且旧组件的版本管理没有强制使用语义化版本来进行控制,修改一个组件时无法通知到其他依赖了这个组件的上层组件。且无法判断组件间是否有不该出现的依赖倒置问题,这样可能会导致低层级组件依赖高层级组件代码,从而出现架构污染。并且旧组件部分层级组件之间还有横向依赖,会导致组件依赖关系不是树状,而是变成网状拓扑结构,这样在迁移、更新、或者移除组件时会出现拔萝卜带泥的问题。

问题三、旧开发流程自动化程度很低,开发环境无法自更新

旧的开发流程虽然相对简单,但是也更多依赖人工进行操作。由于我们对项目越来越庞大,之前在开发过程中会有大量的精力消耗在代码合并、分支切换、二进制与源码切换进行debug等过程中。并且由于没有自动工具来处理各种资源管理、语法检查、代码混淆等工作,这些事情都需要开发们手动进行,往往不能保持整体结构的统一,很容易出现隔一段时间就需要整体梳理一次的情况。

问题四、单包代码复用率低,硬编码配置过多,平台化准备不足

旧代码中的服务层有很多硬编码,导致服务层代码在不同单包上出现分歧,使得部分逻辑相同但是参数不同的代码无法跨包统一。长期以往会出现不同单包间不能共用的组件越来越多,不利于平台化的发展方向,也不利于后续的代码维护。且由于之前service-module这样的业务拆分架构还处于初期阶段,很多业务划分的没有那么明确,导致有一些层级混杂的情况,这也会造成代码复用率降低的问题。

问题五、iOS缺乏统一管理平台

目前我们的组件都是通过cocoapods直接管理的,没有一个可视化的管理后台进行展示,并且想要查询组件间的层级和完整依赖关系也很不方便。对于QA同学来说,想要找到自己打的包也很费事,经常找不到自己需要测试的包在哪里。且产品对需求验收与跟踪也不够方便,对于自己定的需求也不能及时主动获取需要的开发过程中的包,还得麻烦开发同学一对一的进行打包,设计同学也是同理。

同时目前iOS已经有大量组件通过外部配置来处理初始化依赖关系,而这些配置一直都没有做过统一管理,在发版时如果出现问题极其容易出错,且平时进行修改查询也不够直观,因此这类配置的统一管理也极有必要。

解决方案

方案一、采用新的开发模式和开发流程

新的开发模式采用版本号同步跟随方式,即如果组件需要更新,则同步主版本号并进行跟随,如果无更新则自动采用最新的正式版本号。通过使用RC(预发布版)–>Stable(稳定版)的方式来划分组件持续集成的不同阶段。在RC阶段,使用Debug模式进行二进制打包,保留符号信息并加速打包耗时;在Stable阶段,使用源码进行集成发布。组件如果在某一个版本合并了feature分支的业务需求,则版本号同步为主版本,如果这个版本中没有新需求,则不进行更新。避免了众多组件每次开新版本都需要进行统一升级,且无法直观判断某个版本的组件是否有代码迭代的问题。

为了保证各业务线高效迭代,在开发流程上也会有所更新:确定需求–>创建需求组件分支表–>开发需求–>提交测试–>确定需求需要合入主干–>封板自动合并。这种开发流程通过使用需求列表的方式,不同的需求开发直接切换成不同的列表即可进行需求开发,相当于对多个组件的git仓库进行了更高层级的封装,使测试对需求进行测试时不需要关心具体的组件分支。并且通过RC-Stable的开发方式,开发需求时无需进行二进制化,RC阶段则使用Debug二进制,最终定版则使用源码方式进行集成。

通过更新开发模式和开发流程,使得不同版本的组件需求可以在不同的需求列表中分别开发,在定版时统一合并到对应版本上即可完成版本迭代。同时添加RC阶段用于代码合并阶段的统一测试和后续合并代码造成的debug,这样大幅度减轻了开发迭代版本与测试进行回归测试时的沟通压力。

方案二、拆分组件依赖管理源,拆分组件横向依赖关系

新的组件依赖管理将划分成一层一源,单版本双源的方式。根据我们现在的代码层级划分,会拆分成8个通用源,然后每个单包独有两个业务源。通过划分不同层级源的方式,使当前版本组件在静态检查代码时只允许集成比自己层级更底层的源,通过这样进行自动化依赖关系判断,防止依赖倒置的出现。

对于现有的横向依赖,则在代码上进行拆分。目前我们横向依赖较多的代码主要是弱业务层,因此将旧的WBServicePool组件拆分为WBServiceCore与WBServicePool,并且通过协议编程的方式进行Service间的横向调用,从而避免代码的直接耦合。Module层则继续强化WBLinker的使用,组件之间强制使用WBLinker进行调用。并对一些已经不符合当前层级的组件重新划分层级,从而解决这类问题。

方案三、开发流程全部使用自动化工具进行流程处理

自动化工具将从三个方面来提高开发效率。

  1. 将原来的jenkins触发器+gitlab-webhook这样的流程修改为gitlab-ci+gitlab-runner。gitlab-ci可以与gitlab的代码管理更紧密的集成,通过pipe-stage-job的模式进行定制化处理,具体处理方案可以看workflow设计文档。这里是通过ci-yaml-shell脚本集来处理所有gitlab-ci流程,并且每层组件可以定制单独的脚本策略。
  2. 通过全新的cocoapods-wanba依赖管理工具增强脚本,实现自动处理版本管理、分层组件集成、二进制与源码切换、全局宏注入、打包签名自动切换、单Podfile直接生成App等功能
  3. 使用gitflow统一工作流脚本完成工作流中大部分命令,自动处理Develop–>RC–>Stable开发流程、MR处理、环境配置检查等工作。

这三套脚本互相配合,就可以替代整个工作流中大部分的手动配置工作,提高开发效率。

方案四、将服务层硬编码配置化,使用参数注入解决复用问题

修改基础核心框架,让硬编码参数通过外部配置文件进行注入,如网络库通过读取本地配置文件进行网络请求,分享与三方登陆模块通过插件式架构自动载入不同单包需求。

通过这样的对依赖进行反转控制的方式,将多单包服务层代码完成统一,同时增强代码的弹性扩展能力,降低代码维护成本。

方案五、建设iOS统一管理平台

通过ror等快速建站技术建立统一iOS管理后台,实现组件版本管理、组件依赖管理、组件状态管理、组件开发管理等组件管理功能,实现外部配置管理功能,实现按需组合组件并打包上传功能。将所有需要进行动态配置的事项统一通过管理平台进行管理,完成更加直观可控的iOS开发流程管理。

实施规划

路线图

项目推进路线与依赖关系如下图: -w1114

第一阶段、解除业务层耦合

workflow计划的前提是建立在项目完全组件化的基础之上的,因此在整个计划推进之前,首先需要解决历史遗留问题。因此第一阶段的主要任务是清理之前组件化推进过程中不充分的地方。旧的组件化方案对于业务层组件的横向依赖只做了建议解耦,而在实施过程中还是发生了很多解耦不够充分的问题,同时没有提出一个明确的业务层组件的解耦最佳实践,因此第一阶段主要需要处理这方面的问题。

iOS的业务层很早以前就拆分成了强弱业务,弱业务只负责数据处理,强业务负责具体逻辑,因此从架构上弱业务所包含的耦合逻辑就相对较少,相对更好处理;但同时强业务组件间调用又已经有现成的中间件方案,而弱业务层缺没有这样的方案。所以我们做了一下几点工作来解决现有业务代码横向耦合问题。

弱业务处理方式

第一步通过整理现有弱业务代码,将不符合弱业务层代码范围定义的上下分离:与逻辑相关的代码迁移到对应强业务层,与基础核心组件相关内容下沉至核心层,将弱业务代码纯粹精简化。

第二步修改弱业务的代码组织方式,通过面向协议编程的方式,将所有弱业务接口下沉到弱业务池这样的核心层,通过协议对象的方式进行弱业务服务间的调用。这样设计的原因是因为弱业务接口变动相对来说不频繁,而功能调用相对频繁,因此可以接受协议对象这种弱耦合的方式来进行解耦,以牺牲灵活性换取调用效率与架构的简洁性。 代码调用形式大致如下所示

// 获得协议对象
WBServiceDefine(WBServiceRecorder)
// 通过协议对象方法获取需要信息
@implementation MyClass {
    - (void)myFunction {
        NSString *currentRoomId = WBServiceRecorder().currentRoomId;
    }
}

强业务处理方式

第一步继续推进中间件的覆盖面,整理现有强业务组件中直接耦合的部分,改为通过中间件进行调用。 第二步整理强业务中多个组件同时使用较多的基础性业务代码,将其下沉至弱业务或者核心组件中。 这一步比较依赖业务方的推动,幸好之前的组件化方案相对成熟,且业务开发同学也意识到横向依赖会带来的一系列问题,积极主动的配合,很快就处理完了强业务间不合理的依赖关系。

这两步做完,第一阶段便基本完成,可以开始处理依赖管理相关内容了。

第二阶段、依赖关系重整与组件迁移

之前我们的所有组件的依赖管理是单层设计,所有组件共享同一个依赖源,无法从根本上限制反向依赖导致的架构污染,因此需要设计新的依赖管理模型。

根据之前设计的方案,新的组件依赖架构应如下图所示: -w771

因此新的依赖管理模型需要通过重新组织每层组件的代码路径,对每层代码做源隔离,并且约束每层代码可以接入的组件范围来进行强制约束。

这里我们在代码管理路径上使用gitlab提供的subgroup的方式对每层代码进行分类: -w544

同时给每一层都提供了自己的依赖源,每一层的依赖源都包含二进制与源码一版两份: -w837

通过这样对组件进行路径分割,使其在进行代码检查时禁止添加反向层级组件的依赖关系,保证了架构的纯洁性。

不过组件路径迁移并不适合使用过渡方案进行逐步替换,因此我们采用了整体迁移的方式。关于代码迁移的具体内容与实施方案可以参考iOS workflow 代码迁移实施计划

以上做法解决了依赖关系混乱的问题,但也带来了源仓库过多,组件集成较为困难的问题。因此我们对cocoapods这个iOS组件依赖管理工具的源码进行hook,将其功能扩展,修改成支持多层级源码且支持二进制与源码切换的cocoapods-wanba修改版本。

cocoapods-wanba这个组件依赖管理工具有以下几点功能:

  1. 自动查询组件所属的代码层级,自动切换不同组件的依赖源
  2. 提供组件二进制与源码的切换入口,并支持切换本地开发模式的同时自动下载指定分支代码
  3. 支持App签名注入替换,比如多包签名切换与开发/正式签名切换
  4. 支持版本号注入替换
  5. 支持注入自定义全局宏 -w1730

在cocoapods-wanba投入实用之后,第二阶段已基本完成,接下来就可以处理CI/CD的相关内容了。

第三阶段、CI/CD建设

这一阶段主要是通过设计完整的CI/CD流程,来完善对开发流程的优化与控制。之前我们的CI是通过jenkins与gitlab的webhook相结合的方式来完成的,这种方式的弊端是对git行为的控制不够准确,设计配置长流程较为复杂,且不适合横向扩展编译集群。因此我们将CI/CD平台切换为gitlab-ci来完成功能。这也是最初workflow设计文档内提出的主要功能。相关内容可以参考iOS workflow设计文档

由于在执行过程中发现之前的CI/CD流程,我们并没有将开发关心的问题与QA产品关心的问题进行细分处理,导致整体流程无法精细化自动化。因此我们分别重新设计了面向开发与面向QA产品的处理流程,并通过不同的脚本将功能进行隔离。

面向开发的组件CI/CD流程

对于开发同学,其实只关心组件的开发流程,并不关心App的打包流程,因为开发可以直接在开发机上通过Xcode运行包含所需组件的开发中App。于是我们将组件的CI/CD流程通过gitlab-ci细化为6个环节:check-lint-test-package-publish-report。这五个环节的功能分别如下:

  1. check用于检测当前组件的资源文件等非代码文件是否符合现有规范,如命名规范、文件尺寸规范、组件依赖规范等。如果不符合规范则发送失败报告给该组件的维护者。
  2. lint用于对代码进行静态检查,检查代码是否符合OC语法规范、调用关系是否有欠缺、是否可以正常编译、是否可以集成到App内等静态检查。
  3. test用于检查代码是否可以通过单元测试,并生成单元测试报告。
  4. package用于对组件进行二进制打包。
  5. publish用于发布通过了测试与可以进行二进制化的组件到组件依赖源中。
  6. report用于报告前面5个流程的执行结果给组件维护者,同步组件状态。

组件开发的CI/CD脚本结构如下:

-w361

面向产品&QA的打包流程

对于测试同学,由于他们对具体的组件开发流程并不关心,只关心对应业务需求的App打包是否可以快速顺利的完成。因此暂时将维持使用Jenkins进行打包的形式,后续将该打包流程迁移到统一平台上进行处理。

面向发版的App打包上传流程

对于最终发版时的自动打包上传需求,本身并无其他分支需要选择,直接打正式版App,然后上传至AppConnect即可。因此该流程暂时维持使用Jenkins打包的方式,后续将会迁移到统一平台上通过业务负责人出发监听事件进行自动处理。

CI/CD的基础建设完成后,我们就可以使用新开发流程来规范需求池开发模式,并通过自动化工具简化多需求并行开发测试的操作复杂度了。

第四阶段、实施新开发流程与使用自动化工具

旧开发流程由于由于是建立在单一App开发模式和组件线性开发基础之上的,因此对组件的开发模式并没有进行规定,导致组件内代码管理较为混乱。

新的开发流程将通过三步来解决这一问题。

第一步、App彻底脱壳

旧开发模式的核心是有一个主工程仓库,主工程仓库内集成业务组件形成App。然而在完全组件化的App中,主工程是没有必要长期维护的,反而会限制App组合的灵活性。因此我们将App彻底脱壳,仅通过一个业务组件的集成列表即可直接生成App。同时将不同App的编译参数配置、三方库的调用token等各种配置信息通过外部配置进行记录,然后在生成App时注入进去。这样我们就可以随时生成包含任意业务组件的任意App了。

这一功能是通过增强cocoapods-wanba插件的功能来实现的,实施上仅需一个Podfile文件即可生成App。

第二步、推行新开发流程

之前组件开发并没有具体的流程规范。根据组件开发的特点,我们制定了针对业务组件的开发流程规范。

首先我们将开发流程分为三个阶段:

  • Develop:
    • 需求开发阶段;
    • 每个需求创建一个分支,分支名规则:feature/xxx;
    • 修改的代码,直接提交即可,不需要打 tag
  • RC(Release Candidate):
    • 需求测试通过及内测 TestFlight 阶段;
    • 最终测试版本,正式版的候选版本,着重修改 Bug;
    • 固定分支:release;feature 分支基于此分支创建;
    • 需求测试通过,已确定是当前版本要上线的需求,合并代码到release分支;
    • 此阶段 Bugfix 完成后,需要打 tag ;规则:外部版本号.rc0,eg: 10.6.0.rc0
  • Stable:
    • 稳定正式版本,提交 App Store 的正式版;
    • 固定分支:master;release 分支基于此分支创建,且只能创建release 分支;
    • master 分支只接受来自 release 分支代码合并,不可以直接修改 master 分支的代码
    • 内测结束,在提审正式包前,将release 分支合并到master 分支,并打 tag;规则:外部版本号,eg: 10.6.0

这三个阶段的流程示意图大致如下: alt

第三步、使用统一的本地需求代码管理工具

上述的开发流程中我们会发现,如果一个需求需要同时修改很多业务组件,那操作起来将十分麻烦。无论是代码的提交、mr、bugfix或者封版,都需要同时操作大量的业务仓库。因此我们将这些琐碎的操作逻辑全部打包封装为面向需求的代码管理工具-gitflow脚本。

gitflow脚本是通过建立工作区目录与gitlab-api提供的组件信息查询等功能,组合出一套本地代码管理工具。通过一个可交互式的界面内的各类选项,可以实现不同开发阶段之间顺利迁移。 上述的三个开发阶段都在gitflow脚本内有对应选项,所处阶段不同就直接选择进行操作即可。 -w890

每个阶段都会包含整体需求的组件分支新建、代码的统一提交与推送、以及提MR等功能。通过将多个组件的管理抽象成对需求的管理,隐藏组件管理的具体操作。这样开发同学只需要关心自己的需求是否提交过即可。 -w890

当需求提交到远程仓库后,会生成对应需求的Podfile名称,QA同学通过这个需求名即可打包对应需求的App。

gitflow工具推广使用之后,我们就可以进入下一阶段:对单包与主包的弱业务层及以下层级的代码进行合并了。

第五阶段、参数配置动态化与单包代码整合

根据workflow第二阶段我们整理的组件依赖架构,单包与主包应当只有强业务层是分开实现的,弱业务层及以下层级应该通用一套组件,但是在实际实施过程中经常会出现弱业务等以下层级的组件中包含了只针对主包实现的硬编码,导致单包在使用这些组件时不得不拆分出一个独立的修改后的组件。这一阶段我们就需要将这些有了分歧的组件代码进行合并。

通过对分歧代码进行分析,我们将需要消除硬编码的代码进行统一归纳。可以发现这部分大多集中在网络库等核心组件中。因此这一阶段我们需要先将网络库等核心组件的硬编码配置方式修改为通过注入外部参数来做配置,然后通过控制反转的方式,让强业务对弱业务配置进行初始化注入,将配置信息转移到强业务层,这样我们就可以顺利整合通用组件,完成单包与主包共享所有基础组件的需求了。

第六阶段、统一管理平台的搭建

在完成了以上的工作之后,我们会发现所有的组件管理配置都是通过终端操作去管理的,而gitlab对于我们的需求管理机制无法直接支持。并且iOS现有大量动态配置都是写死一份存在组件内部,另一部分通过后端远程更新。这些配置在本地的设置极其不便,配置内容也不够直观。因此统一管理平台需要将所有这些零散的配置整合起来,并提供直观且可视化的管理页面。

管理后台的设计细节暂时还没有给出具体方案,不过大致需要实现以下功能:

  • 需求提交记录查询,效果类似下图展示:
整体的需求提交信息列表
  • 组件版本管理,及App合成页面,效果类似下图展示:
以蘑菇街的管理后台为例
  • 各种动态配置(json、plist等)参数设置入口
  • App发布后台

这一阶段由于还未开始实施,因此以上介绍均为临时预设,后续可能会有所更改。

收尾、持续完善平台建设

最后的收尾阶段的主要工作就是查缺补漏了,完善CI/CD与管理后台的各个环节的细节,添加各类自动化质量控制脚本以提高代码提交质量,添加更多的可以优化开发流程的小工具。这一阶段可以作为长期优化阶段,与workflow主体任务已经没有必然联系。整套workflow的规划设计是搭建了一套代码开发的框架,内部的细节可以根据之后的实际需求再做补充。

至此,workflow整体规划便全部完成。