
背景
当前,洋葱学园 iOS 端工程的组件化水平过低,在影响工程师开发效率的同时,又难以兜住持续集成的影响范围,不利于整体工程的高质量建设,已无法满足日益增长的工程预期与精细化控制的需求。
现存问题如下
- 缺乏组件必要的独立运作能力
- 缺乏统一中间件进行调度
- 无法进行单元测试,回归测试成本高
- 组件间依赖链紊乱,维护成本高
- 组件化颗粒度过粗,服务下沉不达标,多端引用成本高
目标收益
我们期望组件化重构完成后,达成以下目标
- 减少开发构建时间和测试回归成本,工程各业务组件
- 可独立运行
- 可独立迭代
- 可独立测试
- 减小维护成本,构建统一中间件,由中间件管理组件之间清晰的依赖关系
- 完成基础服务下沉,各业务组件可横向依赖,重构完善现有组件库其中包括
- 中台
- 中学业务线
- 教师/入校业务线
工程现状
CocoaPods 私有库依赖链
下列所有数据均已脱敏
中台

中学业务线

教师/入校业务线

行业现状
业内主流组件化方案
路由
基于路由URL的UI页面统跳管理
一般用法
1 | // kRouteGoodsDetail = @"/goods/goods_detail" |
传参的情况
1 | //kRouteGoodsDetails = @“//goods/goods_detail?goods_id=%d” |
复杂的参数类型
1 | + (nullable id)handleURL:(nonnull NSString *)urlStr |
从接口上可以看出来,通过@"/goods/goods_detail"
字符串,我们可以拿到需要交互的模块,来进行操作。在需要路由服务的页面,通过bindURL
的形式,完成字符串
与路由
的绑定。
其基本流程是:
- App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册。
- 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。
该方案的缺陷如下:
- URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折。因为在组件化的过程中,注册URL并不是充分必要条件,组件是不需要向组件管理器注册Url的。而且注册了 Url之后,会造成不必要的内存常驻 ,如果只是注册Class,内存常驻量就小一点,如果是注册实例,内存常驻量就大了。而且对路由表的维护也是个不必要成本,还会增加工程师维护时的负担。
- 路由的绑定放到
load
方法中,会对项目的冷启动有一定的影响 - 对业务代码存在侵入性
协议
基于面向协议 (POP)思想的服务注册方案
每个模块提供自己的服务协议,然后将此协议声明注册到中间层。调用方能从中间层看到有哪些服务的接口,然后直接调用即可。
1 | // 在中间件中完成协议的声明,方便所有模块调用和查阅@protocol GoodsModuleService |
面向协议编程,看起来是比较酷的,而且也不需要写反射代码。
该方案缺陷如下:
- 把协议的内容放到公共的地方,一旦发生改变,意味着用到协议的地方都要改一遍
load
方法中进行协议绑定,还是会有老毛病存在- 对业务代码存在侵入性
Runtime
基于 Objective-C Runtime 反射的接口调用封装
大家知道 OC 是支持反射的,Swift 拥有独立的 Runtime 所以也是可以的,比如:
1 | Class className = NSClassFromString(@"Person"); |
然后可以通过- (id)performSelector:(SEL)aSelector
来完成消息的发送。
但是这种方式会存在大量的硬编码,也无法触发编译器的自动补全,同时,只有在运行时才可以发现一些未知的错误。除此之外,无法实现多参数传值和方法返回值的获取的问题。所以这里选择NSInvocation
更合适。
这里的一个思路是业界比较知名的 CTMediator,它是基于Mediator
模式和Target-Action
模式来完成的 。
先说调用方法:(住:本次只讨论本地模块之间的调用,不考虑远程调用)
- 本地组件A在某处调用
[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向CTMediator
发起跨组件调用 CTMediator
根据获得的target和action信息
,通过oc/swift
的runtime
转化生成target
实例以及对应的action
选择项- 然后最终调用到目标业务提供的逻辑,完成需求。
组件仅需要通过Action
暴露可调用的接口即可。所有组件都通过组件自带的Target-Action
来响应,也就是说,模块与模块之间的接口被固化在了Target-Action
这一层,避免了实施组件化的改造过程中,对**Business
**的侵入 ,同时也提高了组件化接口的可维护性。
最后,调用者通过响应者给 CTMediator
做的 category
或者 extension
来发起调用,来避免使用字符串调用出现的不友好。
代码大家可以看下,可以说非常简洁了。考虑的也非常全面。
简化下代码如下:
1 | // Mediator提供基于NSInvocation的接口调用方法的统一入口 |
广播通知
基于通知的广播方案
基于通知的模块间通讯的方案,实现起来是最简单的,直接基于系统提供的NSNotificationCenter
即可。适合一对多的通讯场景。但是劣势也特别明显。复杂的数据传输,同步调用等方式都不太方便。通常用来作为以上几种方案的补充。
我们怎么做
- 梳理职能范围,确定基础库和业务库
- 进行改造抽离,确保职责单一高内聚,保持单向链路依赖
- 重构基本原则:
- 基础服务全部下沉至中台(如网络、AMP、日志)
- 通用服务交由中台维护向下依赖基础服务(如支付、视频播放器、分享、跳转路由等),跨端复用组件必须下沉至中台通用服务层来维护
- 抽离真正可复用的基础业务组件独立成库(如可复用的模板UI、客服等),其余独立业务组件糅合进各业务模块(如各端独立的登录注册体系),所有业务组件统一上提至各业务线维护,不再相互糅合
- 重构基本原则:
- 基础服务下沉,横向业务依赖
- 配套工具开发,统一库的迭代流程和方式(H5 发版工具等)
- Rust 编写双端统一的协议中间件,利用 ffi 抛出给 Kotlin/Java 和 Swift/Objective-C 使用,统一移动端端中间件
基础架构

Action
- 中间件的方案,产出完整调度能力的中间件Demo,包含下沉依赖关系演示
- 列出现有功能的基于中间件改造成本
- 如何梳理职能范围,拆分力度如何确认(依赖链是否保持单一)
- 与 Android 对齐方向和内容,架构思路,双端都需要保证基于组件的独立运行单元测试、运行、迭代
- Jenkins 基于独立工程改造(能设置指定工程路径)
以分支切换的形式