iOS組件化中路由方案的分析

隨著移動互聯(lián)網(wǎng)的不斷發(fā)展,用戶的需求越來越多,對App的用戶體驗也變的越來越高。為了更好的應(yīng)對各種需求,開發(fā)人員從軟件工程的角度,將App架構(gòu)由原來簡單的MVC變成MVVM,VIPER等復(fù)雜架構(gòu)。更換適合業(yè)務(wù)的架構(gòu),是為了后期能更好的維護項目。

?但是用戶依舊不滿意,繼續(xù)對開發(fā)人員提出了更多更高的要求,不僅需要高質(zhì)量的用戶體驗,還要求更多的功能體驗,如嗶哩嗶哩客戶端從原有的視頻觀看的基礎(chǔ)之上逐步增加了直播、動態(tài)、IM、專欄、會員購、音樂等模塊,可以說web網(wǎng)站所具有的功能都在移動端進行了實現(xiàn)。這樣如果僅僅在 Xcode 目錄這個層次進行分層已經(jīng)是不夠的了。不管你的目錄是以業(yè)務(wù)進行劃分還是以 M-V-C 三個部分進行劃分,當(dāng)業(yè)務(wù)量非常大(成百上千)的時候,你會發(fā)現(xiàn),想找到某個具體業(yè)務(wù)的某部分代碼簡直是大海撈針。同時,由于所有文件都在一個 Project 里面,如果開發(fā)人員不注意的話,很容易出現(xiàn)頭文件各種互相 include,產(chǎn)生各種混亂的依賴關(guān)系。另外我們想要測試某一個部分的功能時,就會產(chǎn)生很多不必要的額外工作。所以,這時我們想到了將整個APP根據(jù)業(yè)務(wù)的不同拆分成很多組件,每個組件可以單獨編譯運行進行測試,并且當(dāng)我們參與項目的人員越來越多時,代碼量越來越大時,單工程代碼更加難以維護于是,也就有了組件化的概念,實際上組件化也就是模塊化一種的表現(xiàn)方式。

?關(guān)于組件化的優(yōu)缺點,以及確定項目使用組件化如何對代碼進行拆分不在本文的討論之中。如果感興趣可以參考下面幾篇文章。

iOS 混編 模塊化/組件化 經(jīng)驗指北

蘑菇街 App 的組件化之路

傳統(tǒng)的頁面之間的跳轉(zhuǎn)以及通信都是直接通過import的方式進行導(dǎo)入操作,這也是剛接觸iOS開發(fā)時最常用的方式。然而,項目越來越龐大,這種方式會導(dǎo)致代碼之間直接的相互依賴、耦合嚴重,管理起來相當(dāng)混亂,代碼維護成本高。


image.png

所以,如果有一個中間模塊(Mediator)負責(zé)對各個模塊之間的通信進行協(xié)調(diào),模塊通過Mediator發(fā)起通信,然后由Mediator負責(zé)將信息傳遞到相應(yīng)模塊,這樣以來就將模塊之間的相互依賴進行了解耦合。

image.png

這樣做還有一個問題,雖說模塊之間不存在了依賴,但是每個模塊和中間的通信模塊Mediator都相互產(chǎn)生了依賴,所以最理想的方式就是下面這種:每個模塊只需要做好自己的事情就好,然后中間通信模塊Mediator則在各個組件中進行轉(zhuǎn)發(fā)或者跳轉(zhuǎn)。實現(xiàn)這一模式需要中間通信模塊Mediator,通過某種方式能夠找到每個組件,并且能調(diào)用該組件的方法。


image.png

這個問題可以歸納為如何在APP內(nèi)組件間進行路由設(shè)計。我們將業(yè)務(wù)進行模塊化的架構(gòu)往往是為了:

  1. 代碼拆分,將關(guān)聯(lián)性強的基礎(chǔ)服務(wù)代碼或者業(yè)務(wù)代碼抽調(diào)在一起,單獨封版,獨立開發(fā)
  2. 防止主工程越來越大,變得臃腫

所以相對應(yīng)的,模塊化就需要以下功能:

  1. 提供多個庫之間的服務(wù)調(diào)用
  2. 保持庫與庫之間的獨立、非強依賴

總的來說,模塊化的重點還是如何去除多個模塊之間的耦合,讓每個模塊在不強依賴的情況下可以調(diào)用其他模塊的服務(wù)。現(xiàn)在在開源的方案中有以下三種方案被廣泛使用。

1、利用url-scheme注冊

2、Protocol-class注冊

3、利用runtime實現(xiàn)的target-action方法

并各自有比較成熟的第三方庫可供使用。如URL—Scheme庫:

  1. JLRoutes

  2. routable-ios

  3. HHRouter

  4. MGJRouter

Target-Action庫:

? 1、CTMediator

接下來對這三種方法的實現(xiàn)進行簡單的介紹:

URL—Scheme

在iOS系統(tǒng)中默認是支持URL Scheme的方式,例如可以在瀏覽器中輸入:weixin://

可以打開微信應(yīng)用。自然在APP內(nèi)部通過這種方法也能實現(xiàn)組件之間的路由設(shè)計。

這種方式實現(xiàn)的原理是:在APP啟動的時候,或者向以下實例中的在每個模塊自己的load方法里面注冊自己的短鏈、以及對外提供服務(wù)(通過block)通過URL-scheme標(biāo)記好,然后維護在URL-Router里面。

URL-Router中保存了各個組件對應(yīng)的URL-scheme,只要其他組件調(diào)用了 open URL的方法,URL-Router就會去根據(jù)URL查找對應(yīng)的服務(wù)并執(zhí)行。

A_VC

@interface A_VC : UIViewController
-(void)action_A:(NSString*)para1;
@end


====================
#import "A_VC.h"
#import "URL_Roueter.h"
@implementation A_VC
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        [[self new] action_A:para1];
    }];
}

-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"調(diào)用組件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
}

-(void)btn_click{
    [[URL_Roueter sharedInstance] openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
}
-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}
@end

B_VC

@interface B_VC : UIViewController
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end
=======================
#import "B_VC.h"
#import "URL_Roueter.h"
@implementation B_VC
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://B_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        NSInteger para2 = [para[@"para2"]integerValue];
        NSInteger para3 = [para[@"para3"]integerValue];
        NSInteger para4 = [para[@"para4"]integerValue];
        [[self new] action_B:para1 para2:para2 para3:para3 para4:para4];
    }];
}
-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn setTitle:@"調(diào)用組件A" forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    self.view.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:btn];
}

-(void)btn_click{
    [[URL_Roueter sharedInstance]openURL:@"test://A_Action" withParam:@{@"para1":@"param1"}];
}

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3  {
    NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
}
@end

URL_Router

#import <Foundation/Foundation.h>
typedef void (^componentBlock) (NSDictionary *param);
@interface URL_Roueter : NSObject
+ (instancetype)sharedInstance;
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- (void)openURL:(NSString *)url withParam:(id)param;
@end
=================================
#import "URL_Roueter.h"

@interface URL_Roueter()
@property (nonatomic, strong) NSMutableDictionary *cache;
@end
@implementation URL_Roueter
+ (instancetype)sharedInstance
{
    static URL_Roueter *router;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        router = [[URL_Roueter alloc] init];
    });
    return router;
}

-(NSMutableDictionary *)cache{
    if (!_cache) {
        _cache = [NSMutableDictionary new];
    }
    return _cache;
}
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}
- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}
@end

這種方法會存在一些問題:

1、當(dāng)組件多起來的時候,需要提供一個關(guān)于URL和服務(wù)的對應(yīng)表,并且需要開發(fā)人員對這樣一份表進行維護。

2、這種方式需要在應(yīng)用啟動時每個組件需要到路由管理中心注冊自己的URL及服務(wù),因此內(nèi)存中需要保存這樣一份表,當(dāng)組件多起來之后會出現(xiàn)一些內(nèi)存的問題。

3、混淆了本地調(diào)用和遠程調(diào)用。
(a、遠程調(diào)用和本地調(diào)用的處理邏輯是不同的,正確的做法應(yīng)該是把遠程調(diào)用通過一個中間層轉(zhuǎn)化為本地調(diào)用,如果把兩者兩者混為一談,后期可能會出現(xiàn)無法區(qū)分業(yè)務(wù)的情況。比如對于組件無法響應(yīng)的問題,遠程調(diào)用可能直接顯示一個404頁面,但是本地調(diào)用可能需要做其他處理。如果不加以區(qū)分,那么久無法完成這種業(yè)務(wù)要求。
b、遠程調(diào)用只能傳能被序列化為json的數(shù)據(jù),像 UIImage這樣非常規(guī)的對象是不行的。所以如果組件接口要考慮遠程調(diào)用,這里的參數(shù)就不能是這類非常規(guī)對象,接口的定義就受限了。出現(xiàn)這種情況的原因就是,遠程調(diào)用是本地調(diào)用的子集,這里混在一起導(dǎo)致組件只能提供子集功能(遠程調(diào)用),所以這個方案是天生有缺陷的)


URL組件化調(diào)用方式.png

protocol-class 「協(xié)議」 <-> 「類」綁定的方式

將各個模塊提供的協(xié)議統(tǒng)一放在一個文件中(CommonProtocol.h),在各個模塊中依賴這個文件,實現(xiàn)其協(xié)議。如:

CommonProtocol.h

#import <Foundation/Foundation.h>

@protocol A_VC_Protocol <NSObject>
-(void)action_A:(NSString*)para1;
@end

@protocol B_VC_Protocol <NSObject>
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end

中間件提供模塊的注冊和獲取模塊的功能,如:

ProtocolMediator.h

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- (Class)classForProtocol:(Protocol *)proto;

@end

ProtocolMediator.m

#import "ProtocolMediator.h"

@interface ProtocolMediator()
@property (nonatomic,strong) NSMutableDictionary *protocolCache;

@end
@implementation ProtocolMediator


+ (instancetype)sharedInstance
{
static ProtocolMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    mediator = [[ProtocolMediator alloc] init];
});
return mediator;
}

-(NSMutableDictionary *)protocolCache{
    if (!_protocolCache) {
        _protocolCache = [NSMutableDictionary new];
    }
    return _protocolCache;
}

- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
    [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}


@end

在各個模塊中實現(xiàn)其協(xié)議

A模塊:A_VC.h

#import <UIKit/UIKit.h>
#import "CommonProtocol.h"

@interface A_VC : UIViewController<A_VC_Protocol>
@end

A_VC.m

#import "A_VC.h"
#import "ProtocolMediator.h"


@implementation A_VC

+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];

}
     
     
-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}


-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"調(diào)用組件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    
}
@end

B模塊同A模塊相同,代碼片段不貼出。

該方法是對URL路由方式的補充,通過這種方法可以實現(xiàn)組件間非常規(guī)數(shù)據(jù)的傳遞方式,以及對模塊中方法的調(diào)用。

protocol-class.png

RunTime(target-action)

相較于url-scheme的方式進行組件間的路由,Runtime的方式借助了OC運行時的特征,實現(xiàn)了組件間服務(wù)的自動發(fā)現(xiàn),無需注冊即可實現(xiàn)組件間的調(diào)用。因此,不管是從維護性、可讀性、擴展性來說都是一個比較完美些的解決方案。


image.png
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Mediator : NSObject
+(UIViewController *)AVC_viewcontroller:(NSString *)parent;
+(UIViewController *)BVC_viewcontroller:(NSInteger)type;
@end
============================
#import "Mediator.h"
@implementation Mediator
+ (UIViewController *)AVC_viewcontroller:(NSString *)parent{
    Class cls = NSClassFromString(@"A_VC");
  return  [cls performSelector:NSSelectorFromString(@"a_VC_detailViewController:") withObject:@{@"parent":parent}];
   
}
+(UIViewController *)BVC_viewcontroller:(NSInteger)type{
    Class cls = NSClassFromString(@"B_VC");
    return [cls performSelector:NSSelectorFromString(@"b_VC_detailViewController:") withObject:@{ @"type":@(33)  }];
}
@end

A_VC

#import <UIKit/UIKit.h>
#import "Mediator.h"
@interface A_VC : UIViewController
+(void)a_VC_detailViewController:(NSString *)parent;
@end
==================
+ (void)a_VC_detailViewController:(NSString *)parent{
    NSLog(@"======通過runtime進行調(diào)用 ====== ==%@", parent ); 
}
-(void)btn_click{
    [Mediator BVC_viewcontroller:1];
}

B_VC

#import <UIKit/UIKit.h>
#import "Mediator.h"
@interface B_VC : UIViewController 
+(void)b_VC_detailViewController:(NSInteger)type;
@end
============================================
-(void)b_VC_detailViewController:(NSInteger)type  {
  NSLog(@"======通過runtime進行調(diào)用%ld====== ==%@",(long)type );
}
-(void)btn_click{
    [Mediator AVC_viewcontroller:@"dsds"];
}

以上使用runtime的方式對組件間進行路由的一個小例子。由于受限于performSelector方法,最多只能傳遞兩個參數(shù)。因此可以通過對組件增加一層wrapper,把對外提供的業(yè)務(wù)包裝一次。
Target_B.h

#import <Foundation/Foundation.h>

@interface target_B : NSObject
-(void)B_Action:(NSDictionary*)para;

@end

Target_B.m

#import "target_B.h"
#import "B_VC.h"

@implementation target_B

-(void)B_Action:(NSDictionary*)para{
    NSString *para1 = para[@"para1"];
    NSInteger para2 = [para[@"para2"]integerValue];
    NSInteger para3 = [para[@"para3"]integerValue];
    NSInteger para4 = [para[@"para4"]integerValue];
    B_VC *VC = [B_VC new];
    [VC action_B:para1 para2:para2 para3:para3 para4:para4];
}
@end

組件A調(diào)用組件B的步驟變成如下:

A—》Mediator—>wrapper(B)—>B—>具體object

在這種跨模塊場景中,參數(shù)最好還是以去model化的方式去傳遞,在iOS的開發(fā)中,就是以字典的方式去傳遞。這樣就能夠做到只有調(diào)用方依賴mediator,而響應(yīng)方不需要依賴mediator。然而在去model化的實踐中,由于這種方式自由度太大,我們至少需要保證調(diào)用方生成的參數(shù)能夠被響應(yīng)方理解,然而在組件化場景中,限制去model化方案的自由度的手段,相比于網(wǎng)絡(luò)層和持久層更加容易得多。

因為組件化天然具備了限制手段:參數(shù)不對就無法調(diào)用!無法調(diào)用時直接debug就能很快找到原因。所以接下來要解決的去model化方案的另一個問題就是:如何提高開發(fā)效率。

在去model的組件化方案中,影響效率的點有兩個:調(diào)用方如何知道接收方需要哪些key的參數(shù)?調(diào)用方如何知道有哪些target可以被調(diào)用?其實后面的那個問題不管是不是去model的方案,都會遇到。為什么放在一起說,因為我接下來要說的解決方案可以把這兩個問題一起解決。
CTMediator+A_VC_Action.h

#import "CTMediator.h"

@interface CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;

@end



#import "CTMediator+B_VC_Action.h"

@implementation CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
}
@end

在調(diào)用的過程中使用如下:

[[CTMediator sharedInstance] B_VC_Action:@"para 1"  para2:222 para3:3333 para4:444];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內(nèi)容