前言
这篇文章主要是对MGJRouter和CTMediator组件框架调研之后写的介绍与理解。
主要也是市面比较主流是URL-Scheme和Target-Action两种方式,下面我会对这两种分别说明并且附上demo。
组件化的优点
- 业务划分更加清晰,新人接手更加容易,可以按组件分配开发任务。
- 项目可维护性更强,提高开发效率
- 单独测试某个组件
- 加快编译速度,不需要编译整个项目代码
URL-Scheme库(蘑菇街MGJRouter)
URL-Scheme库我以蘑菇街为例,其他的库也大同小异,有兴趣可以去看。
- JLRoutes
- routable-ios
- HHRouter
-
MGJRouter
蘑菇街通过MGJRouter中间层,通过MGJRouter进行组件间的消息转发,更像一种路由的形式。实现方式大致是,在提供服务的组件中提前注册block,然后调用方组件通过URL调用block。
蘑菇街实现方式
注册代码
@interface MGJRouter ()
/**
* 保存了所有已注册的 URL
* 结构类似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
*/
@property (nonatomic) NSMutableDictionary *routes;
@end
@implementation MGJRouter
+ (instancetype)sharedInstance
{
static MGJRouter *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler
{
[[self sharedInstance] addURLPattern:URLPattern andHandler:handler];
}
__weak typeof(self)weak = self;
[MGJRouter registerURLPattern:@"zhs://businessa/changeText" toHandler:^(NSDictionary *routerParameters) {
NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
NSLog(@"----------------%@",userInfo);
weak.updateText = userInfo[@"changeText"];
weak.updateLabel.text = weak.updateText;
}];
[MGJRouter openURL:@"zhs://businessa/changeText" withUserInfo:@{@"changeText":@"组件B改变了我"} completion:^(id result) {
NSLog(@"---------组件B改变了A的文字---------");
}];
每个组件都需要提前注册,通过URL保存需要执行的Block代码到Router里面,URL-Router接受各个组件的注册,用字典保存了每个组件注册过来的URL和对应的服务,只要其他组件调用了openURL方法,就会去这个字典里面根据URL找到对应的block执行(也就是执行其他组件提供的服务)
也可以通过objectForURL执行block之后得到返回值。
+ (void)load {
[MGJRouter registerURLPattern:@"zhs://businessa/createa" toObjectHandler:^id(NSDictionary *routerParameters) {
NSDictionary *userInfo = routerParameters[MGJRouterParameterUserInfo];
RouterBusinessAVC *businessVC = [[RouterBusinessAVC alloc]initWithId:userInfo[@"uuid"] text:userInfo[@"text"]];
return businessVC;
}];
}
id businessVC = [MGJRouter objectForURL:@"zhs://businessa/createa" withUserInfo:@{@"uuid":@"uw93428",@"text":@"我是通过MGJRouter过来的"}];
[self.navigationController pushViewController:businessVC animated:YES];
可以看出这种方式,里面有很多硬编码,某个URL不小心写错就会匹配不到,另外参数都是字典,数据类型不明确,修改参数比较危险,需要组件去对应修改,并不会编译报错。对于这个问题,蘑菇街在此基础上推出Protocol-Class方案。
Protocol-Class方案
Protocol-Class方案和上面路由差别不是很大,只是把URL->Block变为Protocol-Class的形式。
每个组件都有一个protocol,组件实现了协议里面的方法,供其他组件调用,相当于通过Protocol,组件对外提供一个可被调用的方法列表。
有一个ComponentProtocol用来引入组件协议,方便各组组件互相调用。
和MGJRouter一样,每个组件都要注册,程序开始运行时将自身的Class注册到Manager中,用Protocol反射出字符串当做key。
代码实例:
+ (instancetype)sharedInstance {
static ModuleManager *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mediator = [[ModuleManager alloc] init];
});
return mediator;
}
- (void)registerClass:(Class)cls
forProtocol:(Protocol *)proto {
[self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}
- (Class)classForProtocol:(Protocol *)proto {
return self.protocolCache[NSStringFromProtocol(proto)];
}
注册:
+ (void)load {
[[ModuleManager sharedInstance]registerClass:[self class] forProtocol:@protocol(ProtocolA)];
}
组件A协议的方法
@protocol ProtocolA <NSObject>
- (void)configureAVCWithUuid:(NSString *)uuid
text:(NSString *)text;
@end
- (void)configureAVCWithUuid:(NSString *)uuid
text:(NSString *)text {
self.updateText = text;
self.uuid = uuid;
}
使用:
Class vcClass = [[ModuleManager sharedInstance]classForProtocol:@protocol(ProtocolA)];
UIViewController<ProtocolA> *compontAVC = [[vcClass alloc]init];
[compontAVC configureAVCWithUuid:@"i9342f8" text:@"我是通过Protocol过来的"];
[self.navigationController pushViewController:compontAVC animated:YES];
这种方式弥补了上面URL-Router硬编码和参数不明确的问题,但是组件方法的调用是分散在各地的,没有统一的入口,也就没法做组件方法不存在时的统一处理,使用的这个方法的每个组件都需要去处理。蘑菇街是两种方式混用,使用者还要区分不同的参数要使用的不同的方法,这种其实也不太友好。
内存问题
蘑菇街这两种方式都有注册到字典,常驻内存。
- block实现方式可能导致的内存问题,需要避免循环引用的问题。
经过暴力测试,证明并不会导致内存问题。被保存在字典中是一个block对象,而block对象本身并不会占用多少内存。在调用block后会对block体中的方法进行执行,执行完成后block体中的对象释放。
block自身的实现只是一个结构体,也就相当于字典中存放的是很多结构体,所以内存的占用并不是很大。 - 对于协议这种实现方式,和block内存常驻方式差不多。只是将存储的block对象换成Class对象,如果不是已经实例化的对象,内存占用还是比较小的。
Target-Action
这个是casatwy大神提出来的,利用runtime特性,不需要注册,在外层通过CTMediator调用performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget找到对应的组件,执行方法和传递参数。CTMediator内部主要是通过[target performSelector:action withObject:params]去执行对应组件的方法,接下来我们分开来讲target、action、params。
Target
[target performSelector:action withObject:params]中这个target是方法执行的类,每一个组件都有一个执行的Target,里面放着这个组件对外可调用方法。CTMediator这个Target命名也有规定,Target_前缀开始,避免命名冲突。
#import "Target_CompoentA.h"
#import "CTCompoentAVC.h"
@implementation Target_CompoentA
- (UIViewController *)Action_compoentAVC:(NSDictionary *)params{
CTCompoentAVC *vc = [[CTCompoentAVC alloc]initWithUuid:params[@"uuid"] text:params[@"text"]];
return vc;
}
@end
Action
[target performSelector:action withObject:params]中这个action是被执行的方法,定义在target里面,action有Action_作为前缀,每个组件命名最好根据组件来,这样好区分。
#import "Target_CompoentB.h"
#import "CTCompoentBVC.h"
@implementation Target_CompoentB
- (UIViewController *)Action_compoentBVC:(NSDictionary *)params{
CTCompoentBVC *vc = [[CTCompoentBVC alloc]init];
return vc;
}
- (UIViewController *)Action_compoentBVCwithBlock:(NSDictionary *)params {
CTCompoentBVC *vc = [[CTCompoentBVC alloc]initWithBlockParams:params];
return vc;
}
@end
这样组件定义好了方法就可以通过CTMediator去调用了,如果组件随着业务变多,方法更多,那这个类里面方法会越来越多,类越来越大,用category可以解决这个问题,各个组件方法都分散到各自组件的category里面去。
#import "CTMediator+CompoentAAction.h"
NSString *const kCTMediatorTargetA = @"CompoentA";
NSString *const kCTMediatorTargetARootVC = @"compoentAVC";
@implementation CTMediator (CompoentAAction)
- (UIViewController *)compoentAVC {
UIViewController *vc = [self performTarget:kCTMediatorTargetA action:kCTMediatorTargetARootVC params:@{@"uuid":@"ei3423d",@"text":@"我是从Mediator过来的"} shouldCacheTarget:NO];
if([vc isKindOfClass:[UIViewController class]]){
return vc;
}
return nil;
}
@end
@implementation CTMediator (CompoentBAction)
- (UIViewController *)compoentBVC {
UIViewController *vc = [self performTarget:kCTMediatorTargetB action:kCTMediatorTargetBRootVC params:@{@"uuid":@"9oieru3"} shouldCacheTarget:NO];
if([vc isKindOfClass:[UIViewController class]]){
return vc;
}
return nil;
}
@end
调用组件方法
UIViewController *compoentAVC = [[CTMediator sharedInstance] compoentAVC];
[self.navigationController pushViewController:compoentAVC animated:YES];
CTMediator不用注册,可以直接调用。如果某个组件的方法参数变了,修改组件及组件对应CTMediator的categroy方法,如果里面带参数的话,需要修改其他组件,不修改会编译报错预警,MGJRouter需要到组件去修改,可以编译通过。CTMediator命名需要按照规则,命名大写字母开头加_,可能对一些开发者有点不适应。
总结
蘑菇街方式都需要维护一张表,参数类型和修改参数对开发者也不是很友好,硬编码比较多;CTMediator不用注册,但是创建的类比较多,不是很直观,另外组件间的交互需要额外再设计。
摘自casa的建议:
组件化方案在App业务稳定,且规模(业务规模和开发团队规模)增长初期去实施非常重要,它助于将复杂App分而治之,也有助于多人大型团队的协同开发。但组件化方案不适合在业务不稳定的情况下过早实施,至少要等产品已经经过MVP阶段时才适合实施组件化。因为业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会导致将来主业务产生变化时,全局性模块调度和重构会变得相对复杂。
tips
前期开发中项目可能项目比较小,可能组件化可能更复杂,但是后期产品线多,业务更多,那组件化就需要提上议程,开发时就需要做好这个业务模块抽离出来打成私有pod准备。需要注意以下几点:
- 基础功能打成私有pod ,然后尽量用里面的方法
- 图片资源尽量放在一起,公有图片资源抽出
- token最好传值进去,因为有可能这个业务模块被好几个app使用,每个app获取token的方式都不一样
- 业务模块的统计最好也抽离出来
网友评论