iOS VIPER架構實踐(三):面向接口的路由設計

路由是實現模塊間解耦的一個有效工具。若是要進行組件化開發,路由是必不可少的一部分。目前iOS上絕大部分的路由工具都是基於URL匹配的,優缺點都很明顯。這篇文章裏將會給出一個更加原生和安全的設計,這個設計的特色是:html

  • 路由時用protocol尋找模塊
  • 能夠對模塊進行固定的依賴注入和運行時依賴注入
  • 支持不一樣模塊間進行接口適配和轉發,所以無需和某個固定的protocol關聯
  • 充分解耦的同時,增長類型安全
  • 支持移除已執行的路由
  • 封裝UIKit界面跳轉方法,能夠一鍵跳轉和移除
  • 支持storyboard,支持其餘任意模塊
  • 能夠檢測界面跳轉時的大部分錯誤

若是你想要一個可以充分解耦、類型安全、有依賴注入功能的路由器,那這個就是目前所能找到的最佳方案。git

這個路由工具是爲了實踐VIPER模式而設計的,目的是爲VIPER提供依賴注入功能,不過它也能夠用於MVC、MVP、MVVM,沒有任何限制。github

工具和Demo地址:ZIKRouterweb

目錄

  • Router的做用
    • 路由缺失時的狀況
    • 尋找模塊
    • 聲明依賴和接口
    • Builder和依賴注入
  • 現有的Router
    • URL Router
      • 優勢
        • 極高的動態性
        • 統一多端路由規則
        • 適配URL scheme
      • 缺點
        • 不適合通用模塊
        • 安全性差
        • 維護困難
    • Protocol Router
      • 優勢
        • 安全性好,維護簡單
        • 適用於全部模塊
        • 優雅地聲明依賴
      • 缺點
        • 動態性有限
        • 須要額外適配URL Scheme
      • Protocol是否會致使耦合?
        • 業務設計的互相關聯
        • Required InterfaceProvided Interface
    • Target-Action
      • 優勢
      • 缺點
    • UIStoryboardSegue
    • 總結
  • ZIKRouter的特性
    • 離散式管理
    • 自由定義路由參數
    • 移除已執行的路由
    • 經過protocol獲取對應模塊
      • Protocol做爲匹配標識
      • 多對一匹配
    • 依賴注入和依賴聲明
      • 固定依賴和運行時依賴
      • 直接在頭文件中聲明
    • 使用泛型指明特定router
    • 類型安全
      • 傳入正確的protocol
      • 泛型的協變和逆變
    • 用Adapter兼容接口
      • Provided模塊添加Required Interface
      • 用中介者轉發接口
    • 封裝UIKit跳轉和移除方法
      • 封裝iOS的路由方法
      • 識別adaptative類型的路由
      • 支持自定義路由
      • 關於extension裏的跳轉方法
    • 支持storyboard
    • AOP
    • 路由錯誤檢查
    • 支持任意模塊
    • 性能
  • 項目地址和Demo

Router的做用

首先,咱們須要梳理清楚,爲何咱們須要Router,Router能帶來什麼好處,解決什麼問題?咱們須要一個什麼樣的Router?swift

路由缺失時的狀況

沒有路由時,界面跳轉的代碼就很容易產生模塊間耦合。設計模式

iOS中執行界面跳轉時,用的是UIViewController上提供的跳轉方法:安全

[sourceViewController.navigationController pushViewController:destinationViewController animated:YES];
複製代碼
[sourceViewController presentViewController:destinationViewController animated:YES completion:nil];
複製代碼

若是是直接導入destinationViewController的頭文件進行引用,就會致使和destinationViewController模塊產生耦合。相似的,一個模塊引用另外一個模塊時也會產生這樣的耦合。所以咱們須要一個方式來獲取destinationViewController,但又不能對其產生直接引用。bash

這時候就須要路由提供的"尋找模塊"的功能。以某種動態的方式獲取目的模塊。markdown

那麼路由是怎麼解決模塊耦合的呢?在上一篇VIPER講解裏,路由有這幾個主要職責:app

  • 尋找指定模塊,執行具體的路由操做
  • 聲明模塊的依賴
  • 聲明模塊的對外接口
  • 對模塊內各部分進行依賴注入

經過這幾個功能,就能實現模塊間的徹底解耦。

尋找模塊

路由最重要的功能就是給出一種尋找某個指定模塊的方案。這個方案是鬆耦合的,獲取到的模塊在另外一端能夠隨時被另外一個相同功能的模塊替換,從而實現兩個模塊之間的解耦。

尋找模塊的實現方式其實只有有限的幾種:

  • 用一個字符串identifier來標識某個對應的界面(URL Router、UIStoryboardSegue)
  • 利用Objective-C的runtime特性,直接調用目的模塊的方法(CTMediator)
  • 用一個protocol來和某個界面進行匹配(蘑菇街的第二種路由和阿里的BeeHive),這樣就能夠更安全的對目的模塊進行傳參

這幾種方案的優劣將在以後逐一細說。

聲明依賴和接口

一個模塊A有時候須要使用其餘模塊的功能,例如最通用的log功能,不一樣的app有不一樣的log模塊,若是模塊A對通用性要求很高,log方法就不能在模塊A裏寫死,而是應該經過外部調用。這時這個模塊A就依賴於一個log模塊了。App在使用模塊A的時候,須要知道它的依賴,從而在使用模塊A以前,對其注入依賴。

當經過cocoapods這樣的包管理工具來配置不一樣模塊間的依賴時,通常模塊之間是強耦合的,模塊是一一對應的,當須要替換一個模塊時會很麻煩,容易牽一髮而動全身。若是是一個單一功能模塊,的確須要依賴其餘特定的各類庫時,那這樣作沒有問題。可是若是是一個業務模塊中引用了另外一個業務模塊,就應該儘可能避免互相耦合。由於不一樣的業務模塊通常是由不一樣的人負責,應該避免出現一個業務模塊的簡單修改(例如調整了方法或者屬性的名字)致使引用了它的業務模塊也必須修改的狀況。

這時候,業務模塊就須要在代碼裏聲明本身須要依賴的模塊,讓app在使用時提供這些模塊,從而充分解耦。

示例代碼:

@protocol ZIKLoginServiceInput <NSObject>
- (void)loginWithAccount:(NSString *)account
                password:(NSString *)password
                 success:(void(^_Nullable)(void))successHandler
                   error:(void(^_Nullable)(void))errorHandler;
@end
複製代碼
@interface ZIKNoteListViewController ()
//筆記界面須要登陸後才能查看,所以在頭文件中聲明,讓外部在使用的時候設置此屬性
@property (nonatomic, strong) id<ZIKLoginServiceInput> loginService;
@end
複製代碼

這個聲明依賴的工做實際上是模塊的Builder的職責。一個界面模塊大部分狀況下都不止有一個UIViewController,也有其餘一些Manager或者Service,而這些角色都是有各自的依賴的,都統一由模塊的Builder聲明,再在Builder內部設置依賴。不過在上一篇文章的VIPER講解裏,咱們把Builder的職責也放到了Router裏,讓每一個模塊單獨提供一個本身的Router。所以在這裏,Router是一個離散的設計,而不是一個單例Router掌管全部的路由。這樣的好處就是每一個模塊能夠充分定製和控制本身的路由過程。

能夠聲明依賴,也就能夠同時聲明模塊的對外接口。這二者很類似,因此再也不重複說明。

Builder和依賴注入

執行路由的同時用Builder進行模塊構建,構建的時候就對模塊內各個角色進行依賴注入。當你調用某個模塊的時候,須要的不是某個簡單的具體類,而是一個構建完畢的模塊中的某個具體類。在使用這個模塊前,模塊須要作一些初始化的操做,好比VIPER裏設置各個角色之間的依賴關係,就是一個初始化操做。所以使用路由去獲取某個模塊中的類,一定須要經過模塊的Builder進行。不少路由工具都缺失了這部分功能。

你能夠把依賴注入簡單地當作對目的模塊傳參。在進行界面跳轉和使用某個模塊時,常常須要設置目的模塊的一些參數,例如設置delegate回調。這時候就必須調用一些目的模塊的方法,或者傳遞一些對象。因爲每一個模塊須要的參數都不同,目前大部分Router都是使用字典包裹參數進行傳遞。但其實還有更好、更安全的方案,下面將會進行詳解。

你也能夠把Router、Builder和Dependency Injector分開,不過若是Router是一個離散型的設計,那麼都交給各自的Router去作也很合理,同時可以減小代碼量,也可以提供細粒度的AOP。

現有的Router

梳理完了路由的職責,如今來比較一下現有的各類Router方案。關於各個方案的具體實現細節我就再也不展開看,能夠參考這篇詳解的文章:iOS 組件化 —— 路由設計思路分析

URL Router

目前絕大多數的Router都是用一串URL來表示須要打開的某個界面,代碼上看來大概是這樣:

//註冊某個URL,和路由處理進行匹配保存
[URLRouter registerURL:@"settings" handler:^(NSDictionary *userInfo) {
	UIViewController *sourceViewController = userInfo[@"sourceViewController"];
	//獲取其餘參數
	id param = userInfo[@"param"];
	//獲取須要的界面
	UIViewController *settingViewController = [[SettingViewController alloc] init];
	[sourceViewController.navigationController pushViewController: settingViewController animated:YES];
}];
複製代碼
//調用路由
[URLRouter openURL:@"myapp://noteList/settings?debug=true" userInfo:params completion:^(NSDictionary *info) {

}];
複製代碼

傳遞一串URL就能打開noteList界面的settings界面,用字典包裹須要傳遞的參數,有時候還會把UIKit的push、present等方法進行簡單封裝,提供給調用者。

這種方式的優勢和缺點都很突出。

優勢

極高的動態性

這是動態性最高的方案,甚至能夠在運行時隨時修改路由規則,指向不一樣的界面。也能夠很輕鬆地支持多級頁面的跳轉。

若是你的app是電商類app,須要常常作活動,app內的跳轉規則常常變更,那麼就很適合使用URL的方案。

統一多端路由規則

URL的方案是最容易跨平臺實現的,iOS、Andorid、web、PC都按照URL來進行路由時,也就能夠統一管理多端的路由規則,下降多端各自維護和修改的成本,讓不懂技術的運營人員也能夠簡單快速地修改路由。

和上一條同樣,這也是一個和業務強相關的優勢。若是你有統一多端的業務需求,使用URL也很合適。

適配URL scheme

iOS中的URL scheme能夠跨進程通訊,從app外打開app內的某個指定頁面。當app內的頁面都能使用URL打開時,也就直接兼容了URL scheme,無需再作額外的工做。

缺點

不適合通用模塊

URL Router的設計只適合UI模塊,不適合其餘功能性模塊的組件。功能性模塊的調用並不須要如此強的動態特性,除非是有模塊熱更新的需求,不然一個模塊的調用在一個版本里應該老是穩定不變的,即使要進行模塊間解耦,也不該該用這種方式。

安全性差

字符串匹配的方式沒法進行編譯時檢查,當頁面配置出錯時,只能在運行時才能發現。若是某個開發人員不當心在字符串里加了一個空格,編譯時也沒法發現。你能夠用宏定義來減小這種出錯的概率。

維護困難

沒有高效地聲明接口的方式,只能從文檔裏查找,編寫時必須仔細對照字符串及其參數類型。

傳參經過字典來進行,參數類型沒法保證,並且也沒法準確地知道所調用的接口須要哪些參數。當目的模塊進行了接口升級,修改了參數類型和數量,那全部用到的地方都要一一修改,而且沒有編譯器的幫助,你沒法知道是否遺漏了某些地方。這將會給維護和重構帶來極大的成本。

針對這個問題,蘑菇街的選擇是用另外一個Router,用protocol來獲取目的模塊,再進行調用,增長安全性。

Protocol Router

這個方案也很容易理解。把以前的字符串匹配改爲了protocol匹配,就能獲取到一個實現了某個protocol的對象。

開源方案裏只看到了BeeHive實現了這樣的方式:

id<ZIKLoginServiceInput> loginService = [[BeeHive shareInstance] createService:@protocol(ZIKLoginServiceInput)];
複製代碼

優勢

安全性好,維護簡單

再對這個對象調用protocol中的方法,就十分安全了。在重構和修改時,有了編譯器的類型檢查,效率更高。

適用於全部模塊

Protocol更加符合OC和Swift原生的設計思想,任何模塊均可以使用,而不侷限於UI模塊。

優雅地聲明依賴

模塊A須要用到登陸模塊,可是它要怎麼才能聲明這種依賴關係呢?若是使用Protocol Router,那就只須要在頭文件裏定義一個屬性:

@property (nonatomic, string) id<ZIKLoginServiceInput> *loginService;
複製代碼

若是這個依賴是必需依賴,而不是一個可選依賴,那就添加到初始化參數裏:

@interface ModuleA ()
- (instancetype)initWithLoginService:(id<ZIKLoginServiceInput>)loginService;
@end
複製代碼

問題是,若是這樣的依賴不少,那麼初始化方法就會變得很長。所以更好的作法是由Builder進行固定的依賴注入,再提供給外部。目前BeeHive並無提供依賴注入的功能。

缺點

動態性有限

你能夠維護一份protocol和模塊的對照表,使用動態的protocol來嘗試動態地更改路由規則,也能夠在Protocol Router之上封裝一層URL Router專門用於動態性的需求。

須要額外適配URL Scheme

使用了Protocol Router就須要再額外處理URL Scheme了。不過這樣也是正常的,解析URL Scheme原本就應該放到另外一個單獨的模塊裏。

Protocol是否會致使耦合?

不少談到這種方案的文章都會指出,和URL Router相比,Protocol Router會致使調用者引用目的模塊的protocol,所以會產生"耦合"。我認爲這是對"解耦"的錯誤理解。

要想避免耦合,首先要弄清楚,咱們須要什麼程度的解耦。個人定義是:模塊A調用了模塊B,模塊B的接口或者實如今作出簡單的修改時,或者模塊B被替換爲相同功能的模塊C時,模塊A不須要進行任何修改。這時候就能夠認爲模塊A和模塊B是解耦的。

業務設計的互相關聯

有些時候,表達出兩個模塊之間的關聯是有意義的。

當一個界面A須要展現一個登陸界面時,它可能須要向登陸界面傳遞一個"提示語"參數,用於在登陸界面顯示一串提示。這時候,界面A在調用登陸界面時,是要求登陸界面可以顯示這個自定義提示語的,在業務設計中就存在兩個模塊間的強關聯性。這時候,URL Router和Protocol Router沒有任何區別,包括下面將要提到的Target-Action路由方式,都存在耦合,可是Protocol Router經過簡單地改善,是能夠把這部分耦合去除的。

URL Router:

[URLRouter openURL:@"login" userInfo:@{@"message":@"請登陸查看筆記詳情"}];
複製代碼

Protocol Router:

@protocol LoginViewInput <NSObject>
@property (nonatomic, copy) NSString *message;
@end

//獲取登陸界面進行設置
UIViewController<LoginViewInput> *loginViewController = [ProtocolRouter destinationForProtocol:@protocol(LoginViewInput)];
loginViewController.message = @"請登陸查看筆記詳情";
複製代碼

因爲字典傳參的緣由,URL Router只不過是把這種接口上的關聯隱藏到了字典key裏,它在參數字典裏使用@"message"時,就是在隱式地使用LoginViewInput的接口。

這種業務設計上致使的模塊之間互相關聯是不可避免的,也是不須要去隱藏的。隱藏了反而會引來麻煩。若是登陸界面的屬性名字變了,從NSString *message改爲了NSString *notifyString,那麼URL Router在register的時候也必須修改傳參時的代碼。若是register是由登陸界面本身執行和處理的,而不是由App Context來處理的,那麼此時參數key是固定爲@"notifyString"的,那就會要求全部調用者的傳參key也修改成notifyString,這種修改若是缺乏編譯器的幫助會很危險,目前是用宏來減小這種修改致使的工做量。而Protocol Router在修改時就能充分利用編譯器進行檢查,可以保證100%安全。

所以,URL Router並不能作到解耦,只是隱藏了接口關聯而已。一旦遇到了須要修改或者重構的狀況,麻煩就出現了,在替換宏的時候,你還必須仔細檢查有沒有哪裏有直接使用字符串的key。只是簡單地修更名字仍是可控的,若是是須要增長參數呢?這時候就根本沒法檢查哪裏遺漏了參數傳遞了。這就是字典傳參的壞處。

關於這部分的討論,也能夠參考Peak大佬的文章:iOS組件化方案

Protocol Router在這種狀況下也須要做出修改,可是它能幫助你安全高效地進行重構。並且只要稍加改進,也能夠徹底無需修改。解決方法就是把Protocol分離爲Required InterfaceProvided Interface

Required InterfaceProvided Interface

模塊的接口實際上是有Required InterfaceProvided Interface的區別的。Required Interface就是調用者須要用到的接口,Provided Interface就是實際的被調用者提供的接口。

在UML的組件圖中,就很明確地表現出了這二者的概念。下圖中的半圓就是Required Interface,框外的圓圈就是Provided Interface

組件圖

那麼如何實施Required InterfaceProvided Interface?上一篇文章裏已經討論過,應該由App Context在一個adapter裏進行接口適配,從而使得調用者能夠繼續在內部使用Required Interface,adapter負責把Required Interface和修改後的Provided Interface進行適配。

示例代碼:

@protocol ModuleARequiredLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *message;
@end

//Module A中的調用代碼
UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination];
loginViewController.message = @"請登陸查看筆記詳情";
複製代碼
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
複製代碼
//App Context 中的 Adapter,用Objective-C的category或者Swift的extension進行接口適配
@interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapte)
- (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	return self.notifyString;
}
@end
複製代碼

用category、extension、NSProxy等技術兼容新舊接口,工做所有由模塊的使用和裝配者App Context完成。若是LoginViewController已經有了本身的message屬性,這時候就說明新的登陸模塊是不可兼容的,必須有某一方作出修改。固然,接口適配能作的事情是有限的,例如一個接口從同步變成了異步,那麼這時候兩個模塊也是不能兼容的。

所以,若是模塊須要進行解耦,那麼它的接口在設計的時候就應該十分仔細,儘可能不要在參數中引入太多其餘的模塊依賴。

只有存在Required InterfaceProvided Interface概念的設計,才能作到完全的解耦。目前的路由方案都缺失了這一部分。

Target-Action

CTMediator的方案,把對模塊的調用封裝到Target-Action中,利用了Objective-C的runtime特性,省略了Target-Action的註冊和綁定工做,直接經過CTMediator中介者調用目的模塊的方法。

@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去以後,能夠由外界選擇是push仍是present
        return viewController;
    } else {
        // 這裏處理異常場景,具體如何處理取決於產品
        return [[UIViewController alloc] init];
    }
}
@end
複製代碼

-performTarget:action:params:shouldCacheTarget:方法經過NSClassFromString,獲取目的模塊提供的Target類,再調用Target提供的Action,實現了方法調用:

@implementation CTMediator
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這裏是處理無響應請求的地方之一,這個demo作得比較簡單,若是沒有能夠響應的target,就直接return了。實際開發過程當中是能夠事先給一個固定的target專門用於在這個時候頂上,而後處理這種請求的
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 有可能target是Swift對象
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這裏是處理無響應請求的地方,若是無響應,則嘗試調用對應target的notFound方法統一處理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程當中,能夠用前面提到的固定的target頂上的。
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
    }
}
@end
複製代碼

優勢

  • 實現簡潔,整個實現的代碼量不多
  • 省略了路由註冊的步驟,能夠減小一部份內存消耗和時間消耗,可是也略微下降了調用時的性能
  • 使用場景不侷限於界面模塊,全部模塊均可以經過中介者調用

缺點

  • 在調用action時使用字典傳參,沒法保證類型安全,維護困難
  • 直接使用runtime互相調用,難以明確地區分Required InterfaceProvided Interface,所以其實沒法實現徹底解耦。和URL Router同樣,在目的模塊變化時,調用模塊也必須作出修改
  • 過於依賴runtime特性,和Swift的類型安全設計是不兼容的,也沒法跨平臺多端實現

UIStoryboardSegue

蘋果的storyboard其實也有一套路由API,只不過它的侷限性很大。在這裏簡單介紹一下:

@implementation SourceViewController

- (void)showLoginViewController {
	//調用在storyboard中定義好的segue identifier
	[self performSegueWithIdentifier:@"presentLoginViewController" sender:nil];
}

//perform segue時的回調
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender {
    return YES;
}

//配置目的界面
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    //用[segue destinationViewController]獲取目的界面,再對目的界面進行傳參
}
@end
複製代碼

UIStoryboardSegue是和storyboard一塊兒使用的,storyboard中定義好了一些界面跳轉的參數,好比轉場方式(push、present等),在執行路由前,執行路由的UIViewController會收到回調,讓調用者配置目的界面的參數。

在storyboard中鏈接segue,實際上是跳轉到一個已經配置好界面的view controller,也就是和View相關的參數,都已經作好了依賴注入。可是自定義的依賴,卻仍是須要在代碼中注入,因此又給了咱們一個-prepareForSegue:sender:回調。

我不建議使用segue,由於它會致使強耦合。可是咱們能夠借鑑UIStoryboardSegue的sourceViewController、destinationViewController、封裝跳轉邏輯到segue子類、對頁面執行依賴注入等設計。

總結

總結了幾個路由工具以後,個人結論是:路由的選擇仍是以業務需求爲先。當對動態性要求極高、或者須要多平臺統一路由,則選擇URL Router,其餘狀況下,我更傾向於使用Protocol Router。和Peak大大的結論一致。

Protocol Router目前並無一個成熟的開源方案。所以我造了個輪子,增長了上面提到的一些需求。

ZIKRouter的特性

離散式管理

每一個模塊都對應一個或者多個router子類,在子類中管理各自的路由過程,包括對象的生成、模塊的初始化、路由狀態管理、AOP等。路由時,須要使用對應的router子類,而不是一個單例router掌管全部的路由。若是想要避免引用子類帶來的耦合,能夠用protocol動態獲取router子類,或者用父類+泛型在調用者中代替子類。

採用離散式的設計的緣由是想讓各個模塊對路由擁有充分的控制權。

一個router子類的簡單實現以下:

@interface ZIKLoginViewRouter : ZIKViewRouter
@end

@implementation ZIKLoginViewRouter
//app啓動時,註冊對應的模塊和Router
//不使用+load和+initialize方法,由於在Swift中已經不適用
+ (void)registerRoutableDestination {
    [self registerView:[ZIKLoginViewController class]];
    [self registerViewProtocol:ZIKRoutableProtocol(ZIKLoginViewProtocol)];
}
//執行路由時,返回對應的viewController或者UIView
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ZIKLoginViewController *destination = [sb instantiateViewControllerWithIdentifier:@"ZIKLoginViewController"];
    return destination;
}
//檢查來自storyboard的界面是否須要讓外界進行配置
+ (BOOL)destinationPrepared:(UIViewController<ZIKLoginViewProtocol> *)destination {
    if (destination.loginService != nil) {
        return YES;
    }
    return NO;
}
//初始化工做
- (void)prepareDestination:(UIViewController<ZIKLoginViewProtocol> *)destination configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
    if (destination.loginService == nil) {
        //ZIKLoginService也能夠用ZIKServiceRouter動態獲取
        destination.loginService = [ZIKLoginService new];
    }
}
//路由時的AOP回調
+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
@end
複製代碼

你甚至能夠在不一樣狀況下返回不一樣的destination,而調用者對此徹底不知情。例如一個alertViewRouter爲了兼容UIAlertView和UIAlertController,能夠在router內部,iOS8以上使用UIAlertController,iOS8如下則使用UIAlertView。

一切路由的控制都在router類內部,不須要模塊作出任何額外的修改。

自由定義路由參數

路由的配置信息都存儲在configuration裏,在調用者執行路由的時候傳入。基本的跳轉方法以下:

//跳轉到Login界面
 [ZIKLoginViewRouter
     performFromSource:self //界面跳轉時的源界面
     configuring:^(ZIKViewRouteConfiguration *config) {
         //跳轉類型,支持push、presentModally、presentAsPopover、performSegue、show、showDetail、addChild、addSubview、custom、getDestination
         config.routeType = ZIKViewRouteTypePush;
         config.animated = NO;
         config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
             //跳轉前配置界面
         };
         config.routeCompletion = ^(id<NoteEditorProtocol> destination) {
	         //跳轉成功並結束處理
	      };
	      config.performerErrorHandler = ^(ZIKRouteAction routeAction, NSError * error) {
	         //跳轉失敗處理,有失敗的詳細信息
	      };
 }];
複製代碼

Configuration只能在初始化block裏配置,出了block之後就沒法修改。你也能夠用一個configuration子類添加更多自定義信息。

若是不須要複雜的配置,也能夠只用最簡單的跳轉:

[ZIKLoginViewRouter performFromSource:self routeType:ZIKViewRouteTypePush];
複製代碼

移除已執行的路由

你能夠先初始化一個router,再交給其餘角色執行路由:

//初始化router
self.loginRouter = [[ZIKLoginViewRouter alloc] initWithConfiguring:^(ZIKViewRouteConfiguration * _Nonnull config) {
                               config.source = self;
                               config.routeType = ZIKViewRouteTypePush;
                           }];
                           
//執行路由
if ([self.loginRouter canPerform] == NO) {
    NSLog(@"此時沒法執行路由:%@",self.loginRouter);
    return;
}
[self.loginRouter performRouteWithSuccessHandler:^{
    NSLog(@"performer: push success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: push failed: %@",error);
}];
複製代碼

當你須要消除已經展現的界面,或者銷燬一個模塊時,能夠調用移除路由方法一鍵移除:

if ([self.loginRouter canRemove] == NO) {
    NSLog(@"此時沒法移除路由:%@", self.loginRouter);
    return;
}
[self.loginRouter removeRouteWithSuccessHandler:^{
    NSLog(@"performer: pop success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: pop failed,error:%@",error);
}];
複製代碼

從而無需再區分調用pop、dismiss、removeFromParentViewController、removeFromSuperview等方法。

經過protocol獲取對應模塊

Protocol做爲匹配標識

咱們不想讓外部引用ZIKLoginViewRouter頭文件致使耦合,調用者只須要獲取一個符合了ZIKLoginViewProtocol的view controller,所以只須要根據ZIKLoginViewProtocol獲取到對應的router子類,而後在子類上調用父類ZIKViewRouter提供的路由方法便可,這樣就能夠作到隱藏子類。

使用ZIKViewRouterToViewZIKViewRouterToModule宏,便可經過protocol獲取到對應的router子類,而且子類返回的destination一定符合ZIKLoginViewProtocol

[ZIKViewRouterToView(ZIKLoginViewProtocol)
    performFromSource:self
    configuring:^(ZIKViewRouteConfiguration *config) {
         config.routeType = ZIKViewRouteTypePush;
         config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
             //跳轉前配置界面
         };
         config.routeCompletion = ^(id<ZIKLoginViewProtocol> destination) {
	         //跳轉成功並結束處理
	      };
 }];
複製代碼

這時候ZIKLoginViewProtocol就至關於LoginView模塊的惟一identifier,不能再用到其餘view controller上。你能夠用多個protocol註冊同一個router,用於區分requiredProtocolprovidedProtocol

多對一匹配

有時候,一些第三方的模塊或者系統模塊並無提供本身的router,你能夠爲其封裝一個router,此時能夠有多個不一樣的router管理同一個UIViewController或者UIView類。這些router可能提供了不一樣的功能,好比一樣是alertRouter,routerA多是用於封裝UIAlertController,routerB多是用於兼容UIAlertView和UIAlertController,這時候要如何區分並獲取兩個不一樣的router?

像這種提供了獨特功能的router,須要你使用configuration的子類,而後讓子類conform對應功能的protocol。因而就能夠根據configuration的protocol來獲取對應的router:

[ZIKViewRouterToModule(ZIKCompatibleAlertConfigProtocol)
    performFromSource:self
    configuring:^(ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> * _Nonnull config) {
 	config.routeType = ZIKViewRouteTypeCustom;
 	config.title = @"Compatible Alert";
 	config.message = @"Test custom route for alert with UIAlertView and UIAlertController";
 	[config addCancelButtonTitle:@"Cancel" handler:^{
	 	NSLog(@"Tap cancel alert");
 	}];
 	[config addOtherButtonTitle:@"Hello" handler:^{
	 	NSLog(@"Tap hello button");
 	}];
 	config.routeCompletion = ^(id _Nonnull destination) {
	 	NSLog(@"show custom alert complete");
 	};
}];
複製代碼

若是模塊本身提供了router,而且這個router用於依賴注入,不能被其餘router替代,能夠聲明本router爲本模塊的惟一指定router,當有多個router嘗試管理此模塊時,啓動時就會產生斷言錯誤。

依賴注入和依賴聲明

固定依賴和運行時依賴

模塊的依賴分爲固定依賴和運行時參數依賴。

固定依賴就相似於VIPER各角色之間的依賴關係,是一個模塊中固定不變的依賴。這種依賴只須要在router內部的-prepareDestination:configuration:固定配置便可。

運行時依賴就是外部傳入的參數,由configuration負責傳遞,而後一樣是在-prepareDestination:configuration:中,獲取configuration並配置destination。你能夠用一個configuration子類和router配對,這樣就能添加更多自定義信息。

若是依賴參數很簡單,也可讓router直接對destination進行配置,聲明router的destination遵照ZIKLoginViewProtocol,讓調用者在prepareDestination裏設置destination。可是若是依賴涉及到了model對象的傳遞,而且因爲須要隔離View和Model,destination不能接觸到這些model對象,這時候仍是須要讓configuration傳遞依賴,在router內部再把model傳給負責管理model的角色。

所以,configuration和destination的protocol就負責依賴聲明和暴露接口。調用者只須要傳入protocol裏要求的參數或者調用一些初始化方法便可,至於router內部怎麼使用和配置這些依賴,調用者就不用關心了。

直接在頭文件中聲明

聲明一個protocol是一個router的config protocol或者view protocol時,只須要讓這個protocol繼承自ZIKViewConfigRoutable或者ZIKViewRoutable便可。這樣,全部的依賴聲明均可以在頭文件裏明確表示,沒必要再從文檔中查找。

使用泛型指明特定router

一個模塊能夠直接在內部用ZIKViewRouterToModuleZIKViewRouterToView動態獲取router,也能夠在頭文件裏添加一個router屬性,讓builder注入。

那麼一個模塊怎麼向builder聲明本身須要某個特定功能的router呢?答案是父類+泛型。

ZIKRouter支持用泛型指定參數類型。在OC中能夠這樣使用:

//注意這個示例代碼只是用於演示泛型的意思,實際運行時必需要用一個ZIKViewRouter子類才能夠
[ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *>
  performFromSource:self
  configuring:^(ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *config) {
    config.routeType = ZIKViewRouteTypePerformSegue;
    config.configureSegue(^(ZIKViewRouteSegueConfiguration *segueConfig) {
    	segueConfig.identifier = @"showLoginViewController";
    );
}];
複製代碼

ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *>就是一個指定了泛型的類,尖括號中指定了router的destination和configuration類型。這一串說明至關因而在聲明:這是一個destination爲UIViewController類型,用ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *做爲執行路由時的configuration的router類。你能夠對configuration再添加protocol,代表這個configuration必須遵照指定的protocol。

這時你就能夠用父類+泛型來聲明一個router類,這個router類的configuration符合特定的config protocol。並且在寫的時候Xcode會給你自動補全。這是一種很好的隱藏子類的方式,並且符合原生的語法。

可是因爲OC中的類都是Class類型,所以只能這樣聲明一個實例屬性:

@property (nonatomic, strong) ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *> *loginViewRouter;
複製代碼

Builder只能注入一個router實例,而不是一個router class。所以在OC裏通常不這麼使用。

可是在Swift這種類型安全語言中這種模式就能更好地發揮做用了,你能夠直接注入一個符合某個泛型的router:

//在Builder中注入alertRouter
swiftSampleViewController.alertRouter = Router.to(RoutableViewModule<ZIKCompatibleAlertConfigProtocol>())
複製代碼
class SwiftSampleViewController: UIViewController {    
    //在Builder裏注入alertRouterClass後,就能夠直接用這個routerClass執行路由
    var alertRouter: ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>!
    
    @IBAction func testInjectedRouter(_ sender: Any) {
        self.alertRouter.perform(
            from: self,
            configuring: { (config, prepareDestination, prepareModule) in
            prepareModule({ moduleConfig in
                //moduleConfig在類型推斷時就是ZIKCompatibleAlertConfigProtocol,無需在判斷後再強制轉換
                moduleConfig.title = "Compatible Alert"
                moduleConfig.message = "Test custom route for alert with UIAlertView and UIAlertController"
                moduleConfig.addCancelButtonTitle("Cancel", handler: {
                print("Tap cancel alert")
                })
                moduleConfig.addOtherButtonTitle("Hello", handler: {
                    print("Tap Hello alert")
                })
            })
        }
    }
}
複製代碼

聲明瞭ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>的屬性後,外部就能夠直接注入一個對應的router。能夠用這種設計模式來轉移、集中獲取router的職責。

Router能夠在定義的時候限制本身的泛型:

Objective-C:

@interface ZIKCompatibleAlertViewRouter : ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> *>

@end
複製代碼

Swift:

class ZIKCompatibleAlertViewRouter: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol> {

}
複製代碼

這樣在傳遞的時候,就可讓編譯器檢查router是否正確。

調用安全和類型安全

上面的演示已經展現了類型安全的處理,由protocol和泛型共同完成了這個類型安全的設計。不過有一些問題還須要特別注意。

編譯檢查

使用ZIKViewRouterToModuleZIKViewRouterToView時,會對傳入的protocol進行編譯檢查。保證傳入的protocol是可路由的protocol,不能隨意濫用。具體用到的方式有些複雜,並且在Objective-C和Swift上使用了兩種方式來實現編譯檢查,具體實現能夠看源代碼。

泛型的協變和逆變

Swift的自定義泛型不支持協變,因此使用起來有點奇怪。

let alertRouterClass: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type
 
 //編譯錯誤
 //ZIKCompatibleAlertViewRouter.Type is ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type
 alertRouterClass = ZIKCompatibleAlertViewRouter.self
複製代碼

Swift的自定義泛型不支持子類型轉爲父類型,所以把ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type賦值給ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type類型時就會出現編譯錯誤。奇怪的是反過來逆變反而沒有編譯錯誤。而Swift原生的集合類型是支持協變的。從2015年開始就有人提議Swift對自定義泛型加入協變,到如今也沒支持。在Objective-C裏自定義泛型是能夠正常協變的。

所以在swift裏,使用了另外一個類來包裹真正的router,而這個類是能夠隨意指定泛型的。

用Adapter兼容接口

能夠用不一樣的protocol獲取到相同的router。也就是requiredProtocolprovidedProtocol只要有聲明,均可以獲取到同一個router。

首先檢查requiredProtocolprovidedProtocol,肯定兩個接口提供的功能是一致的。不然沒法兼容。

Provided模塊添加Required Interface

requiredProtocol是外部的要求目的模塊額外兼容的,由App Context在ZIKViewAdapter的子類裏進行接口兼容。

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end

//Module A中的調用代碼
UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination];
loginViewController.message = @"請登陸查看筆記詳情";
複製代碼
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
複製代碼
//ZIKEditorAdapter.h,ZIKViewAdapter子類
@interface ZIKEditorAdapter : ZIKViewRouteAdapter
@end
複製代碼
//ZIKEditorAdapter.m
//用Objective-C的category、Swift的extension進行接口適配
@interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapte)
- (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	return self.notifyString;
}
@end

@implementation ZIKEditorAdapter

+ (void)registerRoutableDestination {
	//註冊NoteListRequiredNoteEditorProtocol和ZIKEditorViewRouter匹配
	[ZIKEditorViewRouter registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)];
}

@end
複製代碼

用中介者轉發接口

若是遇到protocol裏的一些delegate須要兼容:

@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
複製代碼
@protocol LoginViewDelegate <NSObject>
- (void)didLogin;
@end

@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<LoginViewDelegate> delegate;
@end
複製代碼

這種狀況在OC裏能夠hook-setDelegate:方法,用NSProxy來進行消息轉發,把LoginViewDelegate的消息轉發爲對應的ModuleARequiredLoginViewDelegate中的消息。

不過Swift裏就不適合這麼幹了,相同方法有不一樣參數類型時,能夠用一個新的router代替真正的router,在新的router裏插入一箇中介者,負責轉發接口:

@implementation ZIKEditorMediatorViewRouter
+ (void)registerRoutableDestination {
	//註冊NoteListRequiredNoteEditorProtocol,和新的ZIKEditorMediatorViewRouter配對,而不是目的模塊中的ZIKEditorViewRouter
	//新的ZIKEditorMediatorViewRouter負責調用ZIKEditorViewRouter,插入一箇中介者
	[self registerView:/* mediator的類*/];	
	[self registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)];
}
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
   //用ZIKEditorViewRouter獲取真正的destination
   id<ProvidedLoginViewInput> realDestination = [ZIKEditorViewRouter makeDestination];
    //獲取一個負責轉發ProvidedLoginViewInput和ModuleARequiredLoginViewInput的mediator
    id<ModuleARequiredLoginViewInput> mediator = MediatorForDestination(realDestination);
    return mediator;
}
@end
複製代碼

通常來講,並不須要當即把全部的protocol都分離爲requiredProtocolprovidedProtocol。調用模塊和目的模塊能夠暫時共用protocol,或者只是簡單地改個名字,在第一次須要替換模塊的時候再對protocol進行分離。

封裝UIKit跳轉和移除方法

封裝iOS的路由方法

ZIKViewRouter把UIKit中路由相關的方法:

  • -pushViewController:animated:
  • -presentViewController:animated:completion:
  • UIPopoverController-presentPopoverFromRect:inView:permittedArrowDirections:animated:
  • UIPopoverPresentationController的配置
  • -performSegueWithIdentifier:sender:
  • -showViewController:sender:
  • -showDetailViewController:sender:
  • -addChildViewController:
  • -addSubview:

全都統一封裝,能夠用枚舉一鍵切換:

[ZIKViewRouterToView(ZIKLoginViewProtocol)
    performFromSource:self routeType::ZIKViewRouteTypePush];
複製代碼

對應的枚舉值:

  • ZIKViewRouteTypePush
  • ZIKViewRouteTypePresentModally
  • ZIKViewRouteTypePresentAsPopover
  • ZIKViewRouteTypePerformSegue
  • ZIKViewRouteTypeShow
  • ZIKViewRouteTypeShowDetail
  • ZIKViewRouteTypeAddAsChildViewController
  • ZIKViewRouteTypeAddAsSubview
  • ZIKViewRouteTypeCustom
  • ZIKViewRouteTypeGetDestination

移除路由時,也沒必要再判斷不一樣狀況分別調用-popViewControllerAnimated:-dismissViewControllerAnimated:completion:-dismissPopoverAnimated:-removeFromParentViewController-removeFromSuperview等方法。

ZIKViewRouter會在內部自動調用對應的方法。

識別adaptative類型的路由

-performSegueWithIdentifier:sender:-showViewController:sender:-showDetailViewController:sender:這些adaptative的路由方法,系統會根據不一樣的狀況適配UINavigationControllerUISplitViewController,選擇調用pushpresent或者其餘方式。直接調用時沒法明確知道最終調用的是哪一個方法,也就沒法移除界面。

ZIKViewRouter能夠識別這些路由方法在調用後真正執行的路由操做,因此你如今也能夠在使用這些方法後移除界面。

支持自定義路由

ZIKViewRouter也支持在子類中提供自定義的路由和移除路由方法。只要寫好對應的協議便可。

關於extension裏的跳轉方法

App extension裏還有一些特有的跳轉方法,好比Watch擴展裏WKInterfaceController-pushControllerWithName:context:-popControllerShare擴展裏SLComposeServiceViewController-pushConfigurationViewController:-popConfigurationViewController

看了一下extension的種類有十幾個,懶得一個個去適配了。並且extension裏的界面不會特別複雜,不是特別須要路由工具。若是你須要適配extension,能夠本身增長,也能夠用ZIKViewRouteTypeCustom來適配。

支持storyboard

ZIKViewRouter支持storyboard,這也是和其餘Router相比更強的地方。畢竟storyboard有時候也是很好用的,當使用了storyboard的項目中途使用router的時候,總不能爲了適配router,把全部使用storyboard的界面都重構吧?

適配storyboard的原理是hook了全部UIViewController的-prepareForSegue:sender:方法,檢查destinationViewController是否遵照ZIKRoutableView協議,若是遵照,就說明是一個由router管理的界面,獲取註冊的對應router類,生成router實例,對其進行依賴注入。若是destination須要傳入動態參數,就會調用sourceViewController的-prepareDestinationFromExternal:configuration:方法,讓sourceViewController傳參。若是有多個router類註冊了同一個view controller,則取隨機的一個router。

你不須要對現有的模塊作任何修改,就能夠直接兼容。並且原來view controller中的-prepareForSegue:sender:也能照常使用。

AOP

ZIKViewRouter會在一個界面執行路由和移除路由的時候,對全部註冊了此界面的router回調4個方法:

+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
複製代碼

你能夠在這些方法中檢查界面是否配置正確。也能夠用於AOP記錄。

例如,你能夠爲UIViewController這個全部view controller的父類註冊一個router,這樣就能夠監控全部的UIViewController子類的路由事件。

路由錯誤檢查

ZIKRouter會在啓動時進行全部router的註冊,這樣就能檢測出router是否有衝突、protocol是否和router正確匹配,保證全部router都能正確工做。當檢測到錯誤時,斷言將會失敗。

ZIKViewRouter在執行界面路由時,會檢測並報告路由時的錯誤。例如:

  • 使用了錯誤的protocol執行路由
  • 執行路由時configuration配置錯誤
  • 不支持的路由方式(router能夠限制界面只能使用push、present等有限的跳轉方式)
  • 在其餘界面的跳轉過程當中,執行了另外一個界面的跳轉(unbalanced transition錯誤,會致使-viewWillAppear:-viewDidAppear:-viewWillDisAppear:-viewDidDisappear:等事件的順序發生錯亂)
  • Source view controller此時的狀態沒法執行當前路由
  • 路由時container view controller配置錯誤
  • segue在代理方法中被取消,致使路由未執行
  • 重複執行路由

基本上包含了界面跳轉時會發生的大部分錯誤事件。

支持任意模塊

ZIKRouter包含ZIKViewRouterZIKServiceRouterZIKViewRouter專門用於界面跳轉,ZIKServiceRouter則能夠添加任意類進行實例獲取。

你能夠用ZIKServiceRouter管理須要的類,而且ZIKServiceRouter增添了和ZIKViewRouter相同的動態性和泛型支持。

性能

爲了錯誤檢查、支持storyboard和註冊,ZIKViewRouterZIKServiceRouter會在app啓動時遍歷全部類,進行hook和註冊的工做。註冊時只是把view class、protocol和router class的地址加入字典,不會對內存有影響。

在release模式下,iPhone6s機型上,測試了5000個UIViewController以及5000個對應的router,遍歷全部類而且hook的耗時大約爲15ms,註冊router的耗時大約爲50ms。基本上不會遇到性能問題。

若是你不須要支持storyboard,能夠去掉view class和router class配對的註冊,去掉之後就沒法自動爲storyboard裏的view controller建立router。至於protocol和router的註冊,目前彷佛是沒法避免的。

項目地址和Demo

簡單來講,ZIKRouter就是一個用於模塊間路由,基於接口進行模塊發現和依賴注入的Router。它以原生的語法執行路由,在OC和Swift中都能使用。

項目地址在:ZIKRouter。裏面包含了一個demo,用於演示iOS中大部分的界面路由場景,建議在橫屏iPad上運行。

最後記得點個star~

Demo截圖,控制檯的輸出就是界面路由時的AOP回調:

demo

參考

相關文章
相關標籤/搜索