隨著移動互聯網的不斷發展,用戶的需求越來越多,對App的用戶體驗也變的越來越高。為了更好的應對各種需求,開發人員從軟件工程的角度,將App架構由原來簡單的MVC變成MVVM,VIPER等復雜架構。更換適合業務的架構,是為了后期能更好的維護項目。
?但是用戶依舊不滿意,繼續對開發人員提出了更多更高的要求,不僅需要高質量的用戶體驗,還要求更多的功能體驗,如嗶哩嗶哩客戶端從原有的視頻觀看的基礎之上逐步增加了直播、動態、IM、專欄、會員購、音樂等模塊,可以說web網站所具有的功能都在移動端進行了實現。這樣如果僅僅在 Xcode 目錄這個層次進行分層已經是不夠的了。不管你的目錄是以業務進行劃分還是以 M-V-C 三個部分進行劃分,當業務量非常大(成百上千)的時候,你會發現,想找到某個具體業務的某部分代碼簡直是大海撈針。同時,由于所有文件都在一個 Project 里面,如果開發人員不注意的話,很容易出現頭文件各種互相 include,產生各種混亂的依賴關系。另外我們想要測試某一個部分的功能時,就會產生很多不必要的額外工作。所以,這時我們想到了將整個APP根據業務的不同拆分成很多組件,每個組件可以單獨編譯運行進行測試,并且當我們參與項目的人員越來越多時,代碼量越來越大時,單工程代碼更加難以維護于是,也就有了組件化的概念,實際上組件化也就是模塊化一種的表現方式。
?關于組件化的優缺點,以及確定項目使用組件化如何對代碼進行拆分不在本文的討論之中。如果感興趣可以參考下面幾篇文章。
傳統的頁面之間的跳轉以及通信都是直接通過import的方式進行導入操作,這也是剛接觸iOS開發時最常用的方式。然而,項目越來越龐大,這種方式會導致代碼之間直接的相互依賴、耦合嚴重,管理起來相當混亂,代碼維護成本高。
所以,如果有一個中間模塊(Mediator)負責對各個模塊之間的通信進行協調,模塊通過Mediator發起通信,然后由Mediator負責將信息傳遞到相應模塊,這樣以來就將模塊之間的相互依賴進行了解耦合。
這樣做還有一個問題,雖說模塊之間不存在了依賴,但是每個模塊和中間的通信模塊Mediator都相互產生了依賴,所以最理想的方式就是下面這種:每個模塊只需要做好自己的事情就好,然后中間通信模塊Mediator則在各個組件中進行轉發或者跳轉。實現這一模式需要中間通信模塊Mediator,通過某種方式能夠找到每個組件,并且能調用該組件的方法。
這個問題可以歸納為如何在APP內組件間進行路由設計。我們將業務進行模塊化的架構往往是為了:
- 代碼拆分,將關聯性強的基礎服務代碼或者業務代碼抽調在一起,單獨封版,獨立開發
- 防止主工程越來越大,變得臃腫
所以相對應的,模塊化就需要以下功能:
- 提供多個庫之間的服務調用
- 保持庫與庫之間的獨立、非強依賴
總的來說,模塊化的重點還是如何去除多個模塊之間的耦合,讓每個模塊在不強依賴的情況下可以調用其他模塊的服務。現在在開源的方案中有以下三種方案被廣泛使用。
1、利用url-scheme注冊
2、Protocol-class注冊
3、利用runtime實現的target-action方法
并各自有比較成熟的第三方庫可供使用。如URL—Scheme庫:
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這樣非常規的對象是不行的。所以如果組件接口要考慮遠程調用,這里的參數就不能是這類非常規對象,接口的定義就受限了。出現這種情況的原因就是,遠程調用是本地調用的子集,這里混在一起導致組件只能提供子集功能(遠程調用),所以這個方案是天生有缺陷的)
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路由方式的補充,通過這種方法可以實現組件間非常規數據的傳遞方式,以及對模塊中方法的調用。
RunTime(target-action)
相較于url-scheme的方式進行組件間的路由,Runtime的方式借助了OC運行時的特征,實現了組件間服務的自動發現,無需注冊即可實現組件間的調用。因此,不管是從維護性、可讀性、擴展性來說都是一個比較完美些的解決方案。
#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];