您当前所在的位置:官网首页 > 新闻资讯 > 行业新闻 >

有赞移动 iOS 组件化(模块化)架构设计实践

根据具体业务和需求的不同,大部分公司会采用以上一种或者某几种的组合。

统跳路由是页面解耦的最常见方式,大量应用于前端页面。通过把一个 URL 与一个页面绑定,需要时通过 URL 可以方便的打开相应页面。

//通过路由URL跳转到商品列表页面
//kRouteGoodsList = @"//goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList]; 
if {
 [self.navigationController pushViewController:vc animated:YES];
}复制代码

当然有些场景会比这个复杂,比如有些页面需要更多参数。基本类型的参数,URL 协议天然支持:

//kRouteGoodsDetails = @“//goods/goods_detail?goods_id=%d”
NSString *urlStr = [NSString stringWithFormat:@"kRouteGoodsDetails", 123];
UIViewController *vc = [Router handleURL:urlStr];
if {
 [self.navigationController pushViewController:vc animated:YES];
}复制代码

复杂类型的参数,可以提供一个额外的字典参数 complexParams, 将复杂参数放到字典中即可:

+ handleURL:urlStr
 complexParams:complexParams
 completion:completion;复制代码

上面方法里的 completion 参数,是一个回调 block, 处理打开某个页面需要有回调功能的场景。比如打开会员选择页面,搜索会员,搜到之后点击确定,回传会员数据:

//kRouteMemberSearch = @“//member/member_search”
UIViewController *vc = [Router handleURL:urlStr complexParams:nil completion:^ {
 //code to handle the result
if {
 [self.navigationController pushViewController:vc animated:YES];
}复制代码

考虑到实现的灵活性,提供路由服务的页面,会将 URL 与一个 block 相绑定。block 中放入所需的初始化代码。可以在合适的地方将初始化 block 与路由 URL 绑定,比如在 +load 方法里:

+ load {
 [Router bindURL:kRouteGoodsList
 toHandler:^id _Nullable {
 return [[GoodsListViewController alloc] init];
}复制代码

更多路由 URL 相关例子,可以参考 Bifrost 项目中的 Demo.

URL 本身是一种跨多端的通用协议。使用路由URL统跳方案的优势是动态性及多端统一 ; 缺点是能处理的交互场景偏简单。所以一般更适用于简单 UI 页面跳转。一些复杂操作和数据传输,虽然也可以通过此方式实现,但都不是很效率。目前天猫和蘑菇街都有使用路由 URL 作为自己的页面统跳方案,达到解耦的目的。

当无法 import 某个类的头文件但仍需调用其方法时,最常想到的就是基于反射来实现了。例:

Class manager = NSClassFromString;
NSArray *list = [manager performSelector:@selector];
//code to handle the list
...复制代码

但这种方式存在大量的 hardcode 字符串。无法触发代码自动补全,容易出现拼写错误,而且这类错误只能在运行时触发相关方法后才能发现。无论是开发效率还是开发质量都有较大的影响。

如何进行优化呢?这其实是各端远程调用都需要解决的问题。移动端最常见的远程调用就是向后端接口发网络请求。针对这类问题,我们很容易想到创建一个网络层,将这类“危险代码”封装到里面。上层业务调用时网络层接口时,不需要 hardcode 字符串,也不需要理解内部麻烦的逻辑。

类似的,我可以将凯发在线娱乐模块间通讯也封装到一个“网络层”中。这样危险代码只存在某几个文件里,可以特别地进行 code review 和联调测试。后期还可以通过单元测试来保障质量。模块化方案中,我们可以称这类“转发层”为 Mediator 。同时因为 performSelector 方法附带参数数量有限,也没有返回值,所以更适合使用 NSInvocation 来实现。

//Mediator提供基于NSInvocation的远程接口调用方法的统一封装
- performTarget:targetName
 action:actionName
 params:params;
//Goods模块所有对外提供的方法封装在一个Category中
@interface Mediator
- goods_getGoodsList;
- goods_getGoodsCount;
@impletation Mediator
- goods_getGoodsList {
 return [self performTarget:@“GoodsModule” action:@"getGoodsList" params:nil];
- goods_getGoodsCount {
 return [self performTarget:@“GoodsModule” action:@"getGoodsCount" params:nil];
@end复制代码

然后各个业务模块依赖Mediator, 就可以直接调用这些方法了。

//业务方依赖Mediator模块,可以直接调用相关方法
NSArray *list = [[Mediator sharedInstance] goods_getGoodsList];
...复制代码

这种方案的优势是调用简单方便,代码自动补全和编译时检查都仍然有效。劣势是 category 存在重名覆盖的风险,需要通过开发规范以及一些检查机制来规避。同时 Mediator 只是收敛了 hardcode, 并未消除 hardcode, 仍然对开发效率有一定影响。

业界的 CTMediator 开源库,以及美团都是采用类似方案。

有没有办法绝对的避免 hardcode 呢?如果接触过后端的服务化改造,会发现和移动端的业务模块化很相似。Dubbo 就是服务化的经典框架之一。它是通过服务注册的方式来实现远程接口调用的。即每个模块提供自己对外服务的协议声明,然后将此声明注册到中间层。调用方能从中间层看到存在哪些服务接口,然后直接调用即可。例:

//Goods模块提供的所有对外服务都放在GoodsModuleService中
@protocol GoodsModuleService
- getGoodsList;
- getGoodsCount;
//Goods模块提供实现GoodsModuleService的对象, 
//并在+load方法中注册
@interface GoodsModule : NSObject GoodsModuleService 
@implementation GoodsModule
+ load {
 //注册服务
 [ServiceManager registerService:@protocol 
 withModule:self.class]
//提供具体实现
- getGoodsList {...}
- getGoodsCount {...}
//将GoodsModuleService放在某个公共模块中,对所有业务模块可见
//业务模块可以直接调用相关接口
id GoodsModuleService module = [ServiceManager objByService:@protocol];
NSArray *list = [module getGoodsList];
...复制代码

这种方式的优势也包括调用简单方便。代码自动补全和编译时检查都有效。实现起来也简单,协议的所有实现仍然在模块内部,所以不需要写反射代码了。同时对外暴露的只有协议,符合团队协作的“面向协议编程”的思想。劣势是如果服务提供方和使用方依赖的是公共模块中的同一份协议, 当协议内容改变时,会存在所有服务依赖模块编译失败的风险。同时需要一个注册过程,将 Protocol 协议与具体实现绑定起来。

业界里,蘑菇街的 ServiceManager 和阿里的 BeeHive 都是采用的这个方案。

基于通知的模块间通讯方案,实现思路非常简单, 直接基于系统的 NSNotificationCenter 即可。优势是实现简单,非常适合处理一对多的通讯场景。劣势是仅适用于简单通讯场景。复杂数据传输,同步调用等方式都不太方便。模块化通讯方案中,更多的是把通知方案作为以上几种方案的补充。

除了模块间通讯的实现,业务模块化架构还需要考虑每个模块内部的设计,比如其生命周期控制,复杂对象传输,重复资源的处理等。可能因为每个公司都有自己的实际场景,业界方案里对这些问题描述的并不是很多。但实际上他们非常重要,有赞在模块化过程中做了很多相关思考和尝试,会在后面环节进行介绍。

有赞移动自 16 年起开始实践业务模块化架构方式,大致经历了 2016 年的尝试+摸索,2017 年的思考+优化以及 2018 年的成熟+沉淀几个阶段。期间有过对已有 App 的模块化改造,也试过直接应用于新起项目。模块化方案经历过几次改版,踩过一些坑,也收获了很多宝贵的经验。

上一篇:Facebook脑机新成果:迅速读取大脑构思单词,进一步解码AR 下一篇:如何开发一款高性能的 gradle transform