作者|Razor
作者介绍
Razor,爱奇艺iOS基础架构组资深工程师,2013年加入爱奇艺后一直从事爱奇艺视频App的研发及架构,目前主要负责组件化、性能优化及降低崩溃率等工作。
请输入标题 abcdefg
导 读
爱奇艺的愿景做一家以科技创新为驱动的伟大娱乐公司,为了达到这个愿景,我们有了泡泡、阅读、秀场、电影票、电商、漫画、游戏等各种业务。而随着业务以及模块间交互的增多,如何解决业务间的耦合问题以及方便模块的相互调用就变得越来越重要。
这些模块都有调起登录、分享、播放器等需求,各模块间也存在着相互调用。如果直接引用模块进行调起,那模块的独立开发及测试将变得困难(需要把别的模块也加到自己的测试工程中,如果别的模块又引用了其他模块,那么最后会把所有的模块都带进来),而且一旦模块接口发生变化,则所有使用的地方都要进行修改
01
出发点
我们要解决的问题主要有两点:
1、多业务线复杂架构情况下的独立开发测试,互不干扰。
2、统一调用接口,方便业务间的调用。
为了解决这两个问题,我们决定让各业务以组件化的形式加入到主工程中。
一般来说,组件是对业务、逻辑或图形界面的封装,具备可移植性,并应做到集成后即可使用。
组件又分为基础组件、业务组件,基础组件一般提供上层模块需要用到的功能,比如网络交互、数据处理等等;业务组件一般处理某种特定的业务,比如电影票相关的功能(展示、购买)可以组成电影票业务组件。
组件化有很多优点,比如易于复用、相对独立并且方便集成和使用,这样一来就解决了前文提出的问题。
为了独立开发及测试,业务组件间是不能耦合彼此的,但是业务组件间是有相互调用的,为了解决耦合及方便业务件相互调用,我们采用了Mediator模式,各个模块都耦合Mediator,这样模块间就不需要相互引用了,并且模块间调起可以通过统一接口进行,降低了学习成本。
02
第一次组件化
iOS组件化业界有很多种形式,我们也考虑过url路由的形式,类似这样:
[QYRouter openURL:@”iqiyi://ModuleA?xx=xx” params:xx completion:xx]
使用这种形式时,如果只用query string传参,那么oc的对象无法传递。
如果query string加oc对象的形式,又会增加接口的复杂度,因为调用者会在参数到底是放在query string中还是放在oc接口中产生迷惑,而实现方又会在从哪里取这个参数产生迷惑。
(有人可能会说,实现的时候,我两个地方的参数都兼容下就可以了,但是这样并不是一个“干净”的实现方式,并且可能发生参数名冲突)
基于以上考虑,我们决定通过发送消息的形式来进行模块间通信,并使用枚举类型指定模块及子页面,这样不会出现人为的错误(如果是字符串,ModuleA可能写成ModlueA)。
这样我们就创建了一个库来处理模块间通信,我们给这个库起名为QYEngine,所有的模块间交互都通过Engine,Engine负责消息的发送与回调。
Engine主要由以下几部分组成:
1.QYEControlCenter
控制中心,整个Engine中最核心的部分,
负责模块任务的调度与分发
2.QYEChecker
用于检验模块是否合法(是否注册等)
3.QYEParser
将一次调用转换为task
4.QYETask
每一次的模块调用都被抽象为一个task
5.QYETaskStack
需要回调的task会保存在task stack中,
用于保存上下文信息
6.QYEActionHandler
将每一个task转为一次事件调用,
调用注入的
7.QYEInterfaceManager
对象的对应方法
我们提供了统一的接口EngineSend来调用其他模块,
调用EngineSend后,
数据会发送到Engine中的控制中心,
控制中心会调用Checker进行数据检验,
然后调用Parser进行数据处理,
生成一个Task,
Task会被传给ActionHandler进行处理,
需要回调的Task会存在TaskStack内,
最后ActionHandler会调用注入的Manager对象
进行实际的业务逻辑处理,
调用流程如下图:
Engine内部调用流程图
每个模块都需要耦合Engine,
调用其他模块及接收回调也是通过Engine。
例如
当模块A调用模块B时,
会将调用的模块类型及参数发给Engine,
剩下的事情就交给Engine了;
当模块B的调用完成时,
会回调Engine,
再由Engine通知模块A刚才的调用已完成。
各模块与Engine的关系如下图所示:
各模块与Engine调用关系图
比如想要调起登录模块的注册页面,代码类似这样:
EngineObj *obj = [EngineObj engineObjWithModule:ModuleLogin type:LoginRegisterType andParams:@{@”info”:@”请先注册”}];
EngineCallback *cb = [EngineCallback engineCallbackWithTarget:self action:@selector(registerDone:)];
EngineSend(obj, cb)
这样调起者就可以不用关心调起注册页面的细节,不论用户注册成功或失败,调用者都会通过registerDone:方法得到结果
Engine会对相应模块维护一个协议,
比如上面代码中,
打开的module是ModuleA,
那就路由到openModuleA:方法,
具体的调起逻辑由主工程的一个Engine Manager实现,
这个Manager对象在程序启动时动态注入到Engine中,
并且实现了Engine中的模块协议,
Engine与Manager及模块关系如下图所示:
Engine、Manager及模块关系图
Manager注入可以在程序启动时,也可以写在自己的load方法中:
EngineManager *mgr = [[EngineManager alloc] init];
[QYEApi setInterfaceMgr:mgr];
manager需要实现调起各模块的协议,具体业务都在这个类中,比如上面的例子,就需要他实现调起登录的协议:
- (void)openLoginModuleByType:(Type)type andParams:(NSDictionary *)params {
}
第一次组件化的问题
在实际使用中,发现第一次组件化有一些问题:
1
模块type需要在Engine中维护,新增时需要修改Engine的头文件(新增一种module type),新增调起协议(在interface中新增一个方法)。这样一来就违反了开闭原则,虽然修改不多,但还是需要去维护。
2
在新增模块或模块接口变更时,主工程的Manager需要去维护,增加对应模块的调起接口,耗费人力。
3
如果调起哪个模块是通过服务器数据下发的,想调起新增的模块必须新增调用代码,无法做到直接通过服务器数据进行动态调起。
03
第二次组件化
第二次组件化主要为了解决了上一次组件化的痛点。此外,模块的如何创建模块自己最清楚,所以这一次我们改成了注册制。
所谓注册制,就是每个模块集成时,都要在Engine中进行注册,并实现一个协议。
程序启动时,需要将对应模块的ID及处理消息的类注册到Engine中。为了使模块接入及移除时对主工程的入侵性达到最小,我们约定在load中进行注册。
代码类似这样:
+ (void)load
{
[Engine registerID:@"2" withClass:[self Class]];
}
+ (void)launchWithObj(Obj *)obj
{
//模块内部进行创建;
}
当模块注册时,Engine会根据其注册的ID及类名维护一个Map,用于后续消息分发时使用。协议方法中的Obj类包含了3个参数:
//服务器原始数据,包含了模块id,及模块所需参数
@property (nonatomic, strong) NSDictionary *serverParams;
//客户端提供的数据,比如新模块承载的ViewController
@property (nonatomic, strong) NSDictionary *clientParams;
//模块退出时的回调
@property (nonatomic, copy) EngineClose close;
进行调用时,只需要传递服务器的模块数据及加在哪个view controller上即可,调起代码:
Obj *obj = [Obj objWithSP:sp andCP:cp closeBlock:close];
EngineOpen(obj);
调用EngineOpen后,
首先这个Obj会被发送到控制中心,
控制中心会跟进server params中的数据解出registerID,
再从Engine的Map中找到registerID对应的类名,
调用这个类的launchWithObj:方法,
具体的调用逻辑在模块内部实现,
展示到传入的view controller上就完成了这次调用,
模块注册及调用流程如下图所示:
注册制流程图
Engine与各模块关系如下图所示,
各模块实现Engine Protocol,
通过Engine相互调用:
注册制Engine与模块关系图
我们在实际的调用时,并没有直接发给对应类原始的Obj对象,而是复制了一份,这样在模块退出调用close的时候就可以进行拦截,而模块进入时因为走的也是我们的入口,这样就可以知晓当前active的是哪个模块,从而方便一些统计上的需求。
第二次组件化的优点
这一次组件化遵循了开闭原则,
无论新增还是删除模块,
Engine都无需维护;
此外,
新模块接入时主工程无序添加任何代码;
如果是是通过服务器数据动态调用的,
那么添加一次调用逻辑,
即支持了所有模块的调用
(包括后续新增的模块)
04
两次组件化的异同
这两次组件化,都是为了解决开篇的两个问题:
1、多业务线复杂架构情况下的独立开发测试,互不干扰;
2、统一调用接口,方便业务间的调用。
第一次组件化我们虽然解决了这两个问题,但是新增业务时,需要增加相应的调用代码,相应的,Engine也要进行相应的修改,这样一来,每次新增业务,我们都需要花人力去对接。
为了彻底解放人力,实现新增业务的热插拔,我们做了第二次组件化,相对第一次,这次达到了增删组件时不需要再维护Engine,也不需要有人对接,并且调用数据可以由服务器下发,以达到动态调用的目的。
到现在,所有垂直业务都已由第一次组件化成功迁移到注册制,也就是第二次组件化的形式。
05
持续集成
处理完耦合及调用的问题,我们开始关注持续集成。为了方便管理,我们使用Cocoapods、Gitlab进行二进制库管理,使用Jenkins进行各模块库的编译及更新。
发布者进行触发编译后,Jenkins会进行主工程podfile的修改及库的编译及上传,这样模块对应的库就发布并集成到主工程了。
06
总结
组件化需要服务器、各业务的支持。
入口的统一也方便做一些统计,比如模块的启动,记录栈顶模块还可以在崩溃时知道是哪个模块是active状态,从而为崩溃统计提供更多的数据支持。
加入自动化的工具后,方便了组件的持续集成,也让组件化成为一套完整流程。
如果觉得不错,就请分享给身边的朋友们吧!