前言
這篇文章主要是對(duì)MGJRouter和CTMediator組件框架調(diào)研之后寫(xiě)的介紹與理解。
主要也是市面比較主流是URL-Scheme和Target-Action兩種方式,下面我會(huì)對(duì)這兩種分別說(shuō)明并且附上demo。
組件化的優(yōu)點(diǎn)
- 業(yè)務(wù)劃分更加清晰,新人接手更加容易,可以按組件分配開(kāi)發(fā)任務(wù)。
- 項(xiàng)目可維護(hù)性更強(qiáng),提高開(kāi)發(fā)效率
- 單獨(dú)測(cè)試某個(gè)組件
- 加快編譯速度,不需要編譯整個(gè)項(xiàng)目代碼
URL-Scheme庫(kù)(蘑菇街MGJRouter)
URL-Scheme庫(kù)我以蘑菇街為例,其他的庫(kù)也大同小異,有興趣可以去看。
- JLRoutes
- routable-ios
- HHRouter
-
MGJRouter
蘑菇街通過(guò)MGJRouter中間層,通過(guò)MGJRouter進(jìn)行組件間的消息轉(zhuǎn)發(fā),更像一種路由的形式。實(shí)現(xiàn)方式大致是,在提供服務(wù)的組件中提前注冊(cè)block,然后調(diào)用方組件通過(guò)URL調(diào)用block。
蘑菇街實(shí)現(xiàn)方式
注冊(cè)代碼
@interface MGJRouter ()
/**
* 保存了所有已注冊(cè)的 URL
* 結(jié)構(gòu)類似 @{@"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的文字---------");
}];
每個(gè)組件都需要提前注冊(cè),通過(guò)URL保存需要執(zhí)行的Block代碼到Router里面,URL-Router接受各個(gè)組件的注冊(cè),用字典保存了每個(gè)組件注冊(cè)過(guò)來(lái)的URL和對(duì)應(yīng)的服務(wù),只要其他組件調(diào)用了openURL方法,就會(huì)去這個(gè)字典里面根據(jù)URL找到對(duì)應(yīng)的block執(zhí)行(也就是執(zhí)行其他組件提供的服務(wù))
也可以通過(guò)objectForURL執(zhí)行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":@"我是通過(guò)MGJRouter過(guò)來(lái)的"}];
[self.navigationController pushViewController:businessVC animated:YES];
可以看出這種方式,里面有很多硬編碼,某個(gè)URL不小心寫(xiě)錯(cuò)就會(huì)匹配不到,另外參數(shù)都是字典,數(shù)據(jù)類型不明確,修改參數(shù)比較危險(xiǎn),需要組件去對(duì)應(yīng)修改,并不會(huì)編譯報(bào)錯(cuò)。對(duì)于這個(gè)問(wèn)題,蘑菇街在此基礎(chǔ)上推出Protocol-Class方案。
Protocol-Class方案
Protocol-Class方案和上面路由差別不是很大,只是把URL->Block變?yōu)镻rotocol-Class的形式。
每個(gè)組件都有一個(gè)protocol,組件實(shí)現(xiàn)了協(xié)議里面的方法,供其他組件調(diào)用,相當(dāng)于通過(guò)Protocol,組件對(duì)外提供一個(gè)可被調(diào)用的方法列表。
有一個(gè)ComponentProtocol用來(lái)引入組件協(xié)議,方便各組組件互相調(diào)用。
和MGJRouter一樣,每個(gè)組件都要注冊(cè),程序開(kāi)始運(yùn)行時(shí)將自身的Class注冊(cè)到Manager中,用Protocol反射出字符串當(dāng)做key。
代碼實(shí)例:
+ (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)];
}
注冊(cè):
+ (void)load {
[[ModuleManager sharedInstance]registerClass:[self class] forProtocol:@protocol(ProtocolA)];
}
組件A協(xié)議的方法
@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:@"我是通過(guò)Protocol過(guò)來(lái)的"];
[self.navigationController pushViewController:compontAVC animated:YES];
這種方式彌補(bǔ)了上面URL-Router硬編碼和參數(shù)不明確的問(wèn)題,但是組件方法的調(diào)用是分散在各地的,沒(méi)有統(tǒng)一的入口,也就沒(méi)法做組件方法不存在時(shí)的統(tǒng)一處理,使用的這個(gè)方法的每個(gè)組件都需要去處理。蘑菇街是兩種方式混用,使用者還要區(qū)分不同的參數(shù)要使用的不同的方法,這種其實(shí)也不太友好。
內(nèi)存問(wèn)題
蘑菇街這兩種方式都有注冊(cè)到字典,常駐內(nèi)存。
- block實(shí)現(xiàn)方式可能導(dǎo)致的內(nèi)存問(wèn)題,需要避免循環(huán)引用的問(wèn)題。
經(jīng)過(guò)暴力測(cè)試,證明并不會(huì)導(dǎo)致內(nèi)存問(wèn)題。被保存在字典中是一個(gè)block對(duì)象,而block對(duì)象本身并不會(huì)占用多少內(nèi)存。在調(diào)用block后會(huì)對(duì)block體中的方法進(jìn)行執(zhí)行,執(zhí)行完成后block體中的對(duì)象釋放。
block自身的實(shí)現(xiàn)只是一個(gè)結(jié)構(gòu)體,也就相當(dāng)于字典中存放的是很多結(jié)構(gòu)體,所以內(nèi)存的占用并不是很大。 - 對(duì)于協(xié)議這種實(shí)現(xiàn)方式,和block內(nèi)存常駐方式差不多。只是將存儲(chǔ)的block對(duì)象換成Class對(duì)象,如果不是已經(jīng)實(shí)例化的對(duì)象,內(nèi)存占用還是比較小的。
Target-Action
這個(gè)是casatwy大神提出來(lái)的,利用runtime特性,不需要注冊(cè),在外層通過(guò)CTMediator調(diào)用performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget找到對(duì)應(yīng)的組件,執(zhí)行方法和傳遞參數(shù)。CTMediator內(nèi)部主要是通過(guò)[target performSelector:action withObject:params]去執(zhí)行對(duì)應(yīng)組件的方法,接下來(lái)我們分開(kāi)來(lái)講target、action、params。
Target
[target performSelector:action withObject:params]中這個(gè)target是方法執(zhí)行的類,每一個(gè)組件都有一個(gè)執(zhí)行的Target,里面放著這個(gè)組件對(duì)外可調(diào)用方法。CTMediator這個(gè)Target命名也有規(guī)定,Target_前綴開(kāi)始,避免命名沖突。
#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]中這個(gè)action是被執(zhí)行的方法,定義在target里面,action有Action_作為前綴,每個(gè)組件命名最好根據(jù)組件來(lái),這樣好區(qū)分。
#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
這樣組件定義好了方法就可以通過(guò)CTMediator去調(diào)用了,如果組件隨著業(yè)務(wù)變多,方法更多,那這個(gè)類里面方法會(huì)越來(lái)越多,類越來(lái)越大,用category可以解決這個(gè)問(wèn)題,各個(gè)組件方法都分散到各自組件的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過(guò)來(lái)的"} 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
調(diào)用組件方法
UIViewController *compoentAVC = [[CTMediator sharedInstance] compoentAVC];
[self.navigationController pushViewController:compoentAVC animated:YES];
CTMediator不用注冊(cè),可以直接調(diào)用。如果某個(gè)組件的方法參數(shù)變了,修改組件及組件對(duì)應(yīng)CTMediator的categroy方法,如果里面帶參數(shù)的話,需要修改其他組件,不修改會(huì)編譯報(bào)錯(cuò)預(yù)警,MGJRouter需要到組件去修改,可以編譯通過(guò)。CTMediator命名需要按照規(guī)則,命名大寫(xiě)字母開(kāi)頭加_,可能對(duì)一些開(kāi)發(fā)者有點(diǎn)不適應(yīng)。
總結(jié)
蘑菇街方式都需要維護(hù)一張表,參數(shù)類型和修改參數(shù)對(duì)開(kāi)發(fā)者也不是很友好,硬編碼比較多;CTMediator不用注冊(cè),但是創(chuàng)建的類比較多,不是很直觀,另外組件間的交互需要額外再設(shè)計(jì)。
摘自casa的建議:
組件化方案在App業(yè)務(wù)穩(wěn)定,且規(guī)模(業(yè)務(wù)規(guī)模和開(kāi)發(fā)團(tuán)隊(duì)規(guī)模)增長(zhǎng)初期去實(shí)施非常重要,它助于將復(fù)雜App分而治之,也有助于多人大型團(tuán)隊(duì)的協(xié)同開(kāi)發(fā)。但組件化方案不適合在業(yè)務(wù)不穩(wěn)定的情況下過(guò)早實(shí)施,至少要等產(chǎn)品已經(jīng)經(jīng)過(guò)MVP階段時(shí)才適合實(shí)施組件化。因?yàn)闃I(yè)務(wù)不穩(wěn)定意味著鏈路不穩(wěn)定,在不穩(wěn)定的鏈路上實(shí)施組件化會(huì)導(dǎo)致將來(lái)主業(yè)務(wù)產(chǎn)生變化時(shí),全局性模塊調(diào)度和重構(gòu)會(huì)變得相對(duì)復(fù)雜。
tips
前期開(kāi)發(fā)中項(xiàng)目可能項(xiàng)目比較小,可能組件化可能更復(fù)雜,但是后期產(chǎn)品線多,業(yè)務(wù)更多,那組件化就需要提上議程,開(kāi)發(fā)時(shí)就需要做好這個(gè)業(yè)務(wù)模塊抽離出來(lái)打成私有pod準(zhǔn)備。需要注意以下幾點(diǎn):
- 基礎(chǔ)功能打成私有pod ,然后盡量用里面的方法
- 圖片資源盡量放在一起,公有圖片資源抽出
- token最好傳值進(jìn)去,因?yàn)橛锌赡苓@個(gè)業(yè)務(wù)模塊被好幾個(gè)app使用,每個(gè)app獲取token的方式都不一樣
- 業(yè)務(wù)模塊的統(tǒng)計(jì)最好也抽離出來(lái)
參考
組件化架構(gòu)漫談
組件化方案調(diào)研
組件化方案