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

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

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

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

iOS 混編 模塊化/組件化 經驗指北

蘑菇街 App 的組件化之路

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


image.png

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

image.png

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


image.png

這個問題可以歸納為如何在APP內組件間進行路由設計。我們將業務進行模塊化的架構往往是為了:

  1. 代碼拆分,將關聯性強的基礎服務代碼或者業務代碼抽調在一起,單獨封版,獨立開發
  2. 防止主工程越來越大,變得臃腫

所以相對應的,模塊化就需要以下功能:

  1. 提供多個庫之間的服務調用
  2. 保持庫與庫之間的獨立、非強依賴

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

1、利用url-scheme注冊

2、Protocol-class注冊

3、利用runtime實現的target-action方法

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

  1. JLRoutes

  2. routable-ios

  3. HHRouter

  4. MGJRouter

Target-Action庫:

? 1、CTMediator

接下來對這三種方法的實現進行簡單的介紹:

URL—Scheme

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

可以打開微信應用。自然在APP內部通過這種方法也能實現組件之間的路由設計。

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

URL-Router中保存了各個組件對應的URL-scheme,只要其他組件調用了 open URL的方法,URL-Router就會去根據URL查找對應的服務并執行。

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:@"調用組件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:@"調用組件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、當組件多起來的時候,需要提供一個關于URL和服務的對應表,并且需要開發人員對這樣一份表進行維護。

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

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


URL組件化調用方式.png

protocol-class 「協議」 <-> 「類」綁定的方式

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

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

在各個模塊中實現其協議

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:@"調用組件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路由方式的補充,通過這種方法可以實現組件間非常規數據的傳遞方式,以及對模塊中方法的調用。

protocol-class.png

RunTime(target-action)

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


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進行調用 ====== ==%@", 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進行調用%ld====== ==%@",(long)type );
}
-(void)btn_click{
    [Mediator AVC_viewcontroller:@"dsds"];
}

以上使用runtime的方式對組件間進行路由的一個小例子。由于受限于performSelector方法,最多只能傳遞兩個參數。因此可以通過對組件增加一層wrapper,把對外提供的業務包裝一次。
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調用組件B的步驟變成如下:

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

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

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

在去model的組件化方案中,影響效率的點有兩個:調用方如何知道接收方需要哪些key的參數?調用方如何知道有哪些target可以被調用?其實后面的那個問題不管是不是去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

在調用的過程中使用如下:

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

推薦閱讀更多精彩內容