货拉拉货运iOS用户端架构优化实践

7,651 阅读18分钟

作者:Sherwin.Chen

一、背景

在移动应用发展过程中,随着团队人员的扩大、业务复杂化,代码量随之增多,从而带来了团队协作开发中各种各样的问题:

  • 功能代码之间的依赖复杂,可维护性差。
  • 协同开发过程中,并行开发存在相互依赖的情况。
  • 功能界限不清晰,基础模块变动,会导致上层业务受到影响。

一个合理的架构设计可以解决大型项目跨团队协作分工和多业务线并行开发的效率问题,货运iOS用户端当前处理这样一个低效率的问题上,需要成立一个技术专项,解决老的架构带来的问题。 最终达到降本增效,提升多人协作的开发效率。

当前货拉拉货运iOS用户端在开发中存在如下三大痛点:

  • [Pod组件版本管理问题]

    • pod组件版本依赖关系管理混乱:有的pod库版本锁定在podspec里,有的在podfile里。如遇升级某个pod组件,需要改N多个地方。在日常的开发过程中经常性因pod版本依赖关系不一致引起pod install/update 失败,需要各同事排查问题,影响业务迭代的开发效率。
  • [组件路由问题]

    • 当前用户端的模块路由方案为基于Target-Action模式的CTMediator,此方案有如下几个问题:
  1. 调用组件名、方法标识采用字符串传入方式,散落到业务代码的各处,不便于维护。
  2. 组件入口统一使用集中路由接口,通过使用整数标识不同的组件方法名,不便于问题的定位与查找。 业务同学在调用处,无法理解当前调用的数字所代表的业务逻辑。
  3. 路由组件没有对异常进行防护和线上监控,无法监控到模块组件调用的线上运行时情况。
  • [多仓库代码提交问题]

    • 货运iOS用户端有四大仓库,Huolala/User/Move/CommonModule,各仓库内的功能模块划分不明确,业务功能与基础功能混乱,导致某个需求迭代开发,会在3个或4个仓库拉分支开发/提交/MR,在不具备完善的DevOps工具链的情况下,极其的影响开发的效率。

为解决上述问题,由此我们制定了如下优化目标:

  • 目标一:提高研发效率, 包括:业务功能代码开发、编译打包测试
  • 目标二:提高项目框架稳定性和可扩展性,减少线上事故发生的概率
  • 目标三: 解决目前开发中的三大问题

二、相关的预研

  1. iOS货运用户端工程架构现状梳理

从工程架构出发,我们梳理了目前阶段的架构,如下图所示:

image.png

  • 从关系图可以看出,现行的架构很不合理,除了与基础库之间隔离,各业务之间互相依赖,耦合非常严重
  • 业务模块中包含了工具组件,并未下沉到公共模块中,重复性的工具代码冗余在各业务代码中。
  • 公共模块中包含了部分业务相关的模块,未上浮到独立的业务层中,导致上下级互相依赖、调用,破坏了稳定依赖原则
  1. iOS业界构架演进目标

  • 组件化为基础的架构目标
  • 接口与服务分离,对业务分层
  • 单仓多组件,对仓库结构改进

2.1 组件化实现原则

2.1.1 、抽象化原则

所谓"抽象化",就是指从具体问题中,提取出具有共性的模式,再使用通用的解决方法加以处理。越底层的模块,应该越稳定,越抽象,越具有高复用度。

2.1.2、稳定依赖原则

不同组件之间的依赖关系必须要指向更稳定的方向。任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以被修改。即不要让稳定的模块依赖不稳定的模块, 尽可能的减少不稳定库的依赖。

2.1.3、业务模块真正解耦

组件化并不是说你把工程的代码拆分成多少个pod就算完事了,要实现模块之间真正的解耦才算真正的组件化。如果模块之间还是通过头文件引入互相调用代码,循环依赖,那么和原本在一个大工程里用文件夹管理没有区别。要做到真正解耦,需要深入到模块代码里,抽丝剥茧的分离和整合。

2.2 组件化指导方针

2.2.1、组件化第一步,剥离产品公共库和基础库

  • 对基础能力,比如网络请求、UI组件、设备能力、WebView这些,完全的剥离到基础能力层,进行独立迭代,以二方库形式进行开发与发布。
  • 针对三方库,最好再封装一层(适配层),使业务项目不直接依赖三方库,方便后续功能迭代或新方案的随时变更,从而减少对主业务的影响。

2.2.2、组件化第二步,独立业务模块单独成库

  • 拆分粒度可以先粗后细,将相对独立的组件拆分出来。
  • 在开发过程中,对一些独立的模块,如:登录模块、账户模块等等,也可以封装成组件,因为这些组件是项目强依赖的,调用的频次比较多。
  • 另外,在拆分组件化的过程中,拆分的粒度要合适,尽量做到组件的独立性。
  • 同时,组件化是一个渐进的过程,不可能把一个完整的工程一下子全部组件化,要分步进行,通过不停的迭代,来最终实现项目的组件化。

2.2.3、组件化第三步,对外服务最小化

在前两步都完成的情况下,我们可以根据组件被调用的需求来抽象出组件对外的最小化接口。

2.2.4、可独立的工具或二方库使用pod 私有源管理

  • 可借助公司CICD 打包的能力,为我们的端上APP打包、发布提供便利性。
  • 将用户端沉淀的二方以及业务端共治的公共二方库,使用打包平台发布能力进行管理。

三、方案设计与制定

  1. 目标拆解制定

image.png

第一步、组件库依赖管理设计与实施,解决pod组件版本管理问题

第二步、组件路由器方案的设计与实施,解决现行路由器方案低效率不规范问题

第三步、组件化架构分层,解决组件未划分明确层次,互相依赖网状问题。

第四步、单仓多组件方案设计与实施,将业务模块上浮,工具模块下沉。

  1. Pod组件依赖管理方案

image.png

  • 创建一个新的pod组件PodDepeLock,每个子组件分别管理几个大组件库的依赖pod的具体版本号
  • 每个大的组件Example下的podfile依赖PodDepeLock下的对应子组件。
  • 所有货运项目中二方库间的依赖不锁定版本,由PodDepeLock统一处理。
  1. 组件路由方案设计

3.1、方案设计目标

  • 组件调用接口文档化、可视化,方便使用和维护。
  • 保持组件化解的偶特性,调用方无所依赖组件名,只面向组件接口编程。
  • 独立的组件可单独编译开发,保持最小化的业务开发体验。

对于组件化方案的架构设计优劣直接影响到架构体系能否长远地支持未来业务的发展,对App的组件化不只是仅仅的拆代码和跨业务调页面,还要考虑复杂和非常规业务参数参与的调度,非页面的跨组件功能调度,组件调度安全保障,组件间解耦,新旧业务的调用接口修改等问题。模块化解耦需求的更准确的描述应该是“如何在保证开发质量和效率的前提下做到无代码依赖的跨模块通信”。

PS:当前所做的路由方案是为后续的组件化业务拆分做好铺垫。

3.2、SHSpringRouter方案设计思想

基于protocol-class模式下的路由组件。 包含三大模块:

  • 核心层包含路由注册、服务实现创建。
  • 安全协议接口调用,用于保护未实现的协议方法调用
  • 运行时的路由日志输出,包含:日志、监控、异常堆栈。

基于Spring框架Service的理念,通过protocol-class注册绑定关系来实现模块间的API解耦。

  • 各个组件以服务的形式存在,每个都可独立,以达到相互解耦的目的。
  • 各个服务具体实现与接口调用分离。 解决网状依赖的问题。
  • 面向协议编程。 即服务与服务调用都是基于服务协议进行配合,通过SHSpringRouter路由达到调用目的。

3.3、SHSpringRouter路由器架构设计

image.png

  • SHSpringRoute:核心路由处理中台,维护整个protocol-class的关系表
  • SHSafetyService:组件服务实现基类,用于处理方法调用异常的问题。比如未实现某个协议的方法,却被调用了。每个组件服务实现都可继承此类。
  • SHSpringReportLog:当前路由工具在运行时的内部日志输出,用于调试分析以及线上运行情况收集。

3.3.1 核心流程时序图

image.png

3.3.2 工程组件化实施架构图

image.png

  1. 路由接口层成立为独立仓库:做为一个组件调用中台集合,每个API声明为其它组件调用的接口,内部的实现通过protocol-class模式进行调用,完全隔离了对其它组件的依赖。
  2. 组件间的调用不依赖服务实现层,通过Protocol进行黑盒调用,完全解耦。
  3. 业务线下的每条业务线集合,可有一个或多个子分类组件实现,进行细化管理。

3.4 组件路由逐步替换方案

image.png

  • 第一步,确认好改造目标,分为两个方向:组件接口改造,业务方调用改造
  • 第二步,对已有的组件老路由进行改造,创建一个新的重构分支,在多人开发下,在此分支上合并代码冲突
  • 第三步,对正在并行进行迭代需求分支,进行的新增老路由调用进行记录,需要在迭代版本时合并到版本主分支中,其主要思路为进行中的业务需求分支建立对应的业务需求路由替换分支,每个单独的业务需求路由分支可随业务需求发包提测,其中业务同学也参与路由的替换开发工作。
  • 第四步,准备集成测试时,将所有业务路由分支往当前迭代总路由分支提MR,比如biz1-Router向6648-SpringRouter提MR, 确认所有业务路由分支都合并完成后,再向当前的业务迭代分支提MR。最终打包提测。
  1. 组件化架构分层

image.png

一个好的分层,在依赖方向需是由上依赖下,不能形成网状结构。 各业务线能独立开发、发布、上线。为达到这个目标,将整个用户端工程分为5个层次:

  • 壳工程:和所有以cocoapod管理的工程项目一样,壳工程做为当前用户端打包发布,是整个项目工程里最稳定的一层。
  • 业务线层: 用户端的业务主要包含搬家业务、同城/跨城业务以及公共的地图选扯三大业务,分别归属于不同的业务开发团队负责。
  • 接口中台层:将所有业务组件的接口进行集中管理,各组件之前调用,都以协议接口形式进行API的调用,完全切断组件间的联系。这其中包括:业务线接口、基础业务服务接口、基础能力服务接口、SDK适配器接口。
  • 服务层:基于服务协议接口的具体的组件实现,可为独立的pod组件,也可为某个业务线下某个子pod,其中服务包含两大类别:基础能力服务和基础业务服务。
  • 基础层:提供最基础的工具类或模块,包括自研的二方库以及开源的三方库,将一些共性的能力进行组合,设计成高度集群功能包。
  1. 单仓多组件方案

5.1 分仓设计的整体思路

image.png

  • 各业务团队独立负责仓库业务的处理与发布,所做变更不会影响到其它业务团队。交互式协议接口除外,例如搬家团队负责SHMove仓库的管理工作,地图团队负责SHMap仓库的管理工作,同城&跨城团队负责SHUser仓库的管理工作。
  • Huolala仓库用于管理打包相关,无业务相关的代码,可以保证基本无变动,App版本信息更新除外
  • Move仓库用于搬家独立业务,会以pod分支方式依赖到项目中来。
  • Map仓库用于地图独立业务,会以pod tag方式依赖到项目中来。
  • User用于同城、跨城业务,会以pod分支方式依赖到项目中来。
  • 其它的二方、三方 pod库,统一由PodDepeLock进行集中式统一管理。

5.2 单仓多组件设计思路

image.png

  • 主导思想:业务模块上浮,工具模块下沉
  • 业务仓库(User.git)管理所有的业务或业务域的组件,将可抽离的工具属于相关的模块、组件下沉到工具仓(CommonModule.git)中。将User做为一个独立的大组件,用于对外提供组件接口能力,内部将细分为4个子组件,每个子组件并不只是以文件夹形式管理,而是以独立组件模式进行集成,可独立进行编译、调试开发。
  • 工具仓(CommonModule.git)管理所有的工具模块以及App的生命周期任务处理,各区分的模块是子组件形式在工具仓组件中进行划分,并身不再是独立的组件。

四、结项收益

1、专项历程

专项从2022.3月到2022.7月,经历了5个版本的迭代,历时4个月时间,共有6位同事间断性的参与了专项开发。通过互相的配合,分工合作最终完成了重构优化。

共完成34个组件128个组件方法的组件路由重构工作,完成20个组件模块的仓库组件迁移工作。共编写85个测试用例,完成自测工作。

2、结项收益

2.1、Pod库版本冲突问题降低

  • 在实施Pod版本依赖管理方案之前,经常性出现 Pod版本不一致导致失败,需花费时间去排查报错的问题。
  • 在实施此方案之后,基本没有因Pod版本依赖冲突,阻断开发的问题。

2.2、现有路由器运行时效率提升50%

Iphone6机型/Debug模式下的数据输出

  • 原路由器SHMedium基于tager-action模式,在运行时的50次平均耗时 0.846ms
  • 现路由器SHSpringRouter基于Protocol-class模式,在开启组件服务缓存模式下50次平均耗时0.418ms

2.3、组件接口集成效率提升

原基于Medium路由模式下的组件,调用组件名、方法标识采用字符串传入方式,通过使用整数标识不同的组件方法名,不便于问题的定位与查找, 无法理解当前调用的数字所代表的业务逻辑。

集成或排查基本流程:

  • 找到要使用组件名字符串
  • 通过查找,定位到当前组件类名文件。
  • 通过传入方法数字标识符,查找组件类名中相应的方法
  • 观察当前方法需要哪些参数
  • 最终完成组件的调用或业务逻辑的流转。

整个流程下来,需要花费1分钟左右。

重构后的基于SHSpringRouter组件,将接口和实现进行分离,每个集成分只需要关注协议接口API,即可完成代码开发的工作。

集成或调用代码走读流程如下:

  • 排查流程只需要点击方法,即可跳转到实现处。
  • 调用API,只需要使用快速宏SHRouter(),即可完成组件API的调用。而且每个基于协议接口的API都有详细的文档说明。
  • 整个流程下来,基本10s,就完成了。

2.4、业务代码提交效率提升

将业务相关的代码迁移到业务仓库,让开发同学只专注针对业务开发,不需要跨仓库写业务代码,节省了建分支、提MR的时间,提升整体的开发效率。

2.5、无线上问题,各实施方案稳定运行

整个专项共划分为4期,经历了5个迭代版本,完成共15个业务需求分支的代码合并。运行期间无线上相关的问题。实时监控和Carsh平台无相关的异常上报。

五、总结

1、遇到的问题

1.1、组件协议接口参数规范问题

在进行组件重构过程中,组件协议接口的参数直接复用原组件的参数形式,但后续改造过程发现,这种固定的形参方式效率低,协议方式可以做得更简洁明了,于是通过小组同学的讨论和投票,制定了一套针对于组件协议接口的规范,包括接口命名、参数类型一系列的定义。

1.2、单仓多组件模式下图片资源获取问题

进行单仓多组件改造过程中,在单元测试阶段发现SHKit工具库提供的SHImageNamed("") 无法正常的获取到图片,通过对源码的梳理,发现是由于多层级组件路径匹配引起的。

考虑到工程有大量的模块都使用SHImageNamed(),决定对SHImageNamed()进行适配改造,修改正则表达式以及相关逻辑判断后,得以正常获取组件内的图片资源。

为防止后续开发会有图片获取异常,通过设置回调代理,将图片获取时的异常上报到实时监控上,能够发现线上的问题。

1.3、业务迭代,分支合并问题

在对SHUser仓库和SHCommoudle仓库代码迁移时,发现后续与各业务迭代需求进行代码合并是个极其头痛的事,比如一个组件本来是在SHCommoudle仓库,可现在移动到了SHUser仓库里,这种代码合并没法通过git方式进行,需要人工的进行代码的合并,极其容易出错。后续通过分析并整理了业务分支代码合并流程,与各业务同学共同一起合并代码,完成了迭代需求的上线。

1.4、监控体系建设

路由替换与单仓多组件改造是两个风险系数极高的任务,我们通过监控体系以及大量单元测试用例保持上线的质量、减少重构带来的风险。

2、不足与思考

  • 专项前期规划不足,技术预研时间跨度有点长

  • 拆分的组件没有进一步的细化和抽离

    • 考虑到专项时间周期和业务稳定性,组件进一步的抽离将放到后续需求迭代版本中由业务同学进行。
    • 对于细化的目标,粗粒度如何、怎么衡量,还没有统一把控度,后续将根据业务组件本身的复杂度以及分离程度进行划分。
  • 组件间路由通信目前没有解决模型传递的问题

    • 原设计是想通过通用模型,解决不同组件间参数传递因模型引发组件耦合的问题。但实际操作中发现通用模型带来了其它的问题:通用模型臃肿,接口参数传递不明确。
    • 对于此问题还需进一步研究更好的方案。如读者有更好的建议,欢迎评论指导。
  • 项目中还存在一些的Common模块

    • 不要让Common出现以及怎样在业务代码设计中避免陷入到Common陷阱里尚未想好处理方案,如读者有更好的建议,欢迎评论指导。

六、参考资料

iOS 组件化方案选型

iOS应用架构谈 组件化方案

阿里组件化框架BeeHive解析

iOS 组件化/模块化架构设计实践

蜂鸟商家版 iOS 组件化 / 模块化实践总结

ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP