routable-ios 是什么?可以用來做什么?與之類似的框架還有哪些?
- routable-ios 是一個路由框架,由兩個文件四個類組成,其中核心的類就一個。
- 可以很方便的實現(xiàn) iOS 中 ViewController 之間的跳轉(zhuǎn)。跳轉(zhuǎn)方式也可以靈活的設(shè)置,后面具體會講到。
- 類似的框架還有 ABRouter & HHRouter。后期的文章也會對 HHRouter 做介紹。
先看一下 routable-ios 中類的關(guān)系:

Routable 繼承自 UPRouter,主要的功能都在 UPRouter 類中,路由主要的功能其實就兩個:
- 注冊希望路由跳轉(zhuǎn)的類、及 URL
- 進行跳轉(zhuǎn)
看一下如何使用routable-ios:
- 將
routable-ios導(dǎo)入項目 - 注冊路由:
[[Routable sharedRouter] map:@"user/:name/:age" toController:[UserController class]];
- 調(diào)用路由進行跳轉(zhuǎn):
[[Routable sharedRouter] open:@"user/chenyu/28"];
- 在 VC 中獲取傳遞的參數(shù)
@implementation UserController
- (id)initWithRouterParams:(NSDictionary *)params {
if ((self = [self initWithNibName:nil bundle:nil])) {
self.title = @"User";
NSLog(@"name: %@",[params objectForKey:@"name"]); //chenyu
NSLog(@"age: %@",[params objectForKey:@"age"]); //28
}
return self;
}
......
@end
首先介紹一下4個類中定義的屬性及方法:
Routable 繼承自 UPRouter
+ (instancetype)sharedRouter; //提供單例方法,用來創(chuàng)建路由類
+ (instancetype)newRouter; //另一種創(chuàng)建路由的方式,一般不推薦,不是單例。
UPRouterOptions 繼承自 NSObject
首先看一下這個類提供的一些屬性,我們就知道這個類是做什么的了。
@property (readwrite, nonatomic, getter=isModal) BOOL modal; //是否是模態(tài)視圖
@property (readwrite, nonatomic) UIModalPresentationStyle presentationStyle; //VC 顯示的樣式
@property (readwrite, nonatomic) UIModalTransitionStyle transitionStyle; //VC 出現(xiàn)時的動畫
@property (readwrite, nonatomic, strong) NSDictionary *defaultParams; //默認的數(shù)據(jù)
@property (readwrite, nonatomic, assign) BOOL shouldOpenAsRootViewController; //是否是根視圖
//.m 文件中的兩個屬性
@property (readwrite, nonatomic, strong) Class openClass; //注冊的類
@property (readwrite, nonatomic, copy) RouterOpenCallback callback; //block 回調(diào)
通過以上內(nèi)容,可以看到UPRouterOptions其實就是一個配置類,里面存儲路由跳轉(zhuǎn)時需要的一些數(shù)據(jù),可以理解成一個輔助的類。這個類中提供了一系列的工廠方法,用來創(chuàng)建不同類型的對象,比如(只列舉部分函數(shù),其他同類型的函數(shù)還有很多,功能大體一致,只是某個配置項不同而已。):
- 全部使用默認配置
//Default construction; like [NSArray array]
+ (instancetype)routerOptions {
return [self routerOptionsWithPresentationStyle:UIModalPresentationNone
transitionStyle:UIModalTransitionStyleCoverVertical
defaultParams:nil
isRoot:NO
isModal:NO];
}
- 傳入所有參數(shù)創(chuàng)建對象
//Explicit construction
+ (instancetype)routerOptionsWithPresentationStyle: (UIModalPresentationStyle)presentationStyle
transitionStyle: (UIModalTransitionStyle)transitionStyle
defaultParams: (NSDictionary *)defaultParams
isRoot: (BOOL)isRoot
isModal: (BOOL)isModal {
UPRouterOptions *options = [[UPRouterOptions alloc] init];
options.presentationStyle = presentationStyle;
options.transitionStyle = transitionStyle;
options.defaultParams = defaultParams;
options.shouldOpenAsRootViewController = isRoot;
options.modal = isModal;
return options;
}
- 自定義部分參數(shù)創(chuàng)建對象
//Custom class constructors, with heavier Objective-C accent
+ (instancetype)routerOptionsAsModal {
return [self routerOptionsWithPresentationStyle:UIModalPresentationNone
transitionStyle:UIModalTransitionStyleCoverVertical
defaultParams:nil
isRoot:NO
isModal:YES];
}
- 剩余的基本就是一些快捷的方法及一些
setters方法,可以查看源碼。
RouterParams 繼承自 NSObject
RouterParams并沒有在.h 文件中做聲明,這個類只在 Routable 和 UPRouter 中的實現(xiàn)中才用到,而這三個類都在一個文件中,所以也沒有必要出現(xiàn)在 .h 文件中。
首先看一下RouterParams的聲明:
@interface RouterParams : NSObject
@property (readwrite, nonatomic, strong) UPRouterOptions *routerOptions;
@property (readwrite, nonatomic, strong) NSDictionary *openParams;
@property (readwrite, nonatomic, strong) NSDictionary *extraParams;
@property (readwrite, nonatomic, strong) NSDictionary *controllerParams;
@end
這個類的出現(xiàn),主要作用是將跳轉(zhuǎn)時匹配好的所有內(nèi)容存起來,緩存到另一個字典中,未來再次跳轉(zhuǎn)的時候,直接可以拿出來用,你也許會問,我們的路由不是在一個字典里嗎,也可以直接拿出來用,為什么還要緩存,后續(xù)到源代碼的地方會細說,為什么要緩存,為什么跳轉(zhuǎn)的時候不是直接去 map 中尋找。
進入核心部分 UPRouter
UPRouter繼承自NSObject,首先看一下類的聲明,刪除了很多注釋
@interface UPRouter : NSObject
/**
The `UINavigationController` instance which mapped `UIViewController`s will be pushed onto.
*/
@property (readwrite, nonatomic, strong) UINavigationController *navigationController;
- (void)pop;
- (void)popViewControllerFromRouterAnimated:(BOOL)animated;
- (void)pop:(BOOL)animated;
@property (readwrite, nonatomic, assign) BOOL ignoresExceptions;
- (void)map:(NSString *)format toCallback:(RouterOpenCallback)callback;
- (void)map:(NSString *)format toCallback:(RouterOpenCallback)callback withOptions:(UPRouterOptions *)options;
- (void)map:(NSString *)format toController:(Class)controllerClass;
//注冊路由,本篇主要分析的方法。上面的方法最終會調(diào)用這個方法,options 傳入的是 nil
- (void)map:(NSString *)format toController:(Class)controllerClass withOptions:(UPRouterOptions *)options;
- (void)openExternal:(NSString *)url;
- (void)open:(NSString *)url;
- (void)open:(NSString *)url animated:(BOOL)animated;
//路由跳轉(zhuǎn),本篇主要分析的方法。上面兩個方法最終都會調(diào)用這個方法。
- (void)open:(NSString *)url animated:(BOOL)animated extraParams:(NSDictionary *)extraParams;
- (NSDictionary*)paramsOfUrl:(NSString*)url;
@end
@interface UPRouter ()
// 存儲注冊的路由
@property (readwrite, nonatomic, strong) NSMutableDictionary *routes;
// 緩存已跳轉(zhuǎn)過的路由
@property (readwrite, nonatomic, strong) NSMutableDictionary *cachedRoutes;
@end
注冊路由
注冊路由比較簡單,就是將傳入的 URL 作為 key,將 Class 作為值存入已初始化的 routes 中。
- (void)map:(NSString *)format toController:(Class)controllerClass {
[self map:format toController:controllerClass withOptions:nil];
}
- (void)map:(NSString *)format toController:(Class)controllerClass withOptions:(UPRouterOptions *)options {
if (!format) {
@throw [NSException exceptionWithName:@"RouteNotProvided"
reason:@"Route #format is not initialized"
userInfo:nil];
return;
}
//如果沒有傳入 options,則會創(chuàng)建一個默認的配置對象
if (!options) {
options = [UPRouterOptions routerOptions];
}
options.openClass = controllerClass;
[self.routes setObject:options forKey:format];
}
路由跳轉(zhuǎn)
路由跳轉(zhuǎn)做的事情比較多,一共有三個比較重要的方法,會詳細看,首先看路由跳轉(zhuǎn)的方法
- (void)open:(NSString *)url
animated:(BOOL)animated
extraParams:(NSDictionary *)extraParams
{
//獲取路由跳轉(zhuǎn)相關(guān)的參數(shù),往下滑動,先看怎么獲取的數(shù)據(jù),看完下面的方法再回來看這個方法
RouterParams *params = [self routerParamsForUrl:url extraParams: extraParams];
UPRouterOptions *options = params.routerOptions;
//好了,拿到數(shù)據(jù)了,開始跳轉(zhuǎn)。先判斷是否有回調(diào),如果有的話,則去執(zhí)行 block
if (options.callback) {
RouterOpenCallback callback = options.callback;
callback([params controllerParams]);
return;
}
//此處刪除了判斷 self.navigationController 是否存在的容錯代碼,無關(guān)緊要。
//獲取將要跳轉(zhuǎn)的 VC,并且將我們傳遞的數(shù)據(jù)以字典的形式,傳遞給這個 VC
//controllerForRouterParams 這個方法比較簡單,打斷點進去看看就 OK 了。
UIViewController *controller = [self controllerForRouterParams:params];
//判斷當前是否有 presented 的 ViewController,有的話要 dismiss,因為接下來要跳轉(zhuǎn)或者 presentViewController
if (self.navigationController.presentedViewController) {
[self.navigationController dismissViewControllerAnimated:animated completion:nil];
}
//是否是以模態(tài)的方式彈出 ViewController
if ([options isModal]) {
if ([controller.class isSubclassOfClass:UINavigationController.class]) {
[self.navigationController presentViewController:controller
animated:animated
completion:nil];
}
else {
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller];
navigationController.modalPresentationStyle = controller.modalPresentationStyle;
navigationController.modalTransitionStyle = controller.modalTransitionStyle;
[self.navigationController presentViewController:navigationController
animated:animated
completion:nil];
}
}
else if (options.shouldOpenAsRootViewController) {
//設(shè)置根視圖
[self.navigationController setViewControllers:@[controller] animated:animated];
}
else {
//直接 push 一個 ViewController
[self.navigationController pushViewController:controller animated:animated];
}
}
獲取路由跳轉(zhuǎn)相關(guān)的參數(shù)方法(刪除了一些容錯處理的代碼):
- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams {
//如果緩存中已經(jīng)有了(證明之前已經(jīng)跳轉(zhuǎn)過這個 VC),并且傳遞的參數(shù)沒有變化。
//這里需要注意了,如果傳遞的參數(shù)你也不確定是不是沒變化,最好給 extraParams 給個值,這樣就不會走緩存了
//否則可能傳遞的數(shù)據(jù)變了,但是走的還是之前的緩存。
//如果 VC 之間不要傳遞數(shù)據(jù),不用考慮這個問題
if ([self.cachedRoutes objectForKey:url] && !extraParams) {
return [self.cachedRoutes objectForKey:url];
}
NSArray *givenParts = url.pathComponents;
NSArray *legacyParts = [url componentsSeparatedByString:@"/"];
//這里判斷傳入的路由路徑是否正確,如果傳入這樣的 "iOS/app//first" 路徑,則會警告。
//也許你的路由路徑是"iOS/app",這樣寫你就少傳了一個實參
if ([legacyParts count] != [givenParts count]) {
NSLog(@"Routable Warning - your URL %@ has empty path components - this will throw an error in an upcoming release", url);
givenParts = legacyParts;
}
//使用枚舉的方式去匹配,這里不能從 self.routes 中通過 [self.routes objectForKey:@"key"] 的方式獲取,
//因為注冊的時候,你后面添加的是參數(shù)(形參),在跳轉(zhuǎn)的時候傳遞的是數(shù)據(jù)(實參)。
//這里也就是為什么需要緩存的原因了,每次跳轉(zhuǎn)都要枚舉這個字典,緩存了以后時間復(fù)雜度直接降到了 O(1)。
__block RouterParams *openParams = nil;
[self.routes enumerateKeysAndObjectsUsingBlock:
^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) {
//routerUrl 是枚舉到的 key,也是當時注冊路由時添加進去的 url,routerOptions 是枚舉到的 value
NSArray *routerParts = [routerUrl pathComponents];
//判斷注冊的路由地址和跳轉(zhuǎn)的帶參數(shù)的地址是否一致,最簡單的辦法就是判斷他們包含的元素個數(shù)是否一致,如果一致,再做更詳細的判斷
if ([routerParts count] == [givenParts count]) {
//如果個數(shù)一致,再判斷是否匹配
NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
if (givenParams) {
//givenParams 存儲的是路由地址中給的數(shù)據(jù),再將 extraParams 一起傳入 RouterParams,創(chuàng)建 RouterParams 的對象。
openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
*stop = YES;//結(jié)束遍歷
}
}
}];
//如果沒有匹配到路由
if (!openParams) {
//用戶設(shè)置了忽略異常,直接返回 nil,否則會走 @throw
if (_ignoresExceptions) {
return nil;
}
@throw [NSException exceptionWithName:@"RouteNotFoundException"
reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
userInfo:nil];
}
//將我們辛辛苦苦封裝好的路由相關(guān)的所有數(shù)據(jù)緩存起來,下次在走這個 url 的時候,直接取緩存中的數(shù)據(jù),這就是為什么要緩存了。
//除非你傳遞的參數(shù)變了,那么一定傳給 extraParams,相關(guān)方法檢測到 extraParams 不為空,會重新組裝數(shù)據(jù)。
[self.cachedRoutes setObject:openParams forKey:url];
return openParams;
}
//判斷注冊的路由和跳轉(zhuǎn)的路由是否一致
- (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents
routerUrlComponents:(NSArray *)routerUrlComponents {
__block NSMutableDictionary *params = [NSMutableDictionary dictionary];
[routerUrlComponents enumerateObjectsUsingBlock:
^(NSString *routerComponent, NSUInteger idx, BOOL *stop) {
NSString *givenComponent = givenUrlComponents[idx];
//判斷是否是形參,所以在注冊路由時,一定要注意,參數(shù)以:開始,否則會當成路徑字符串
if ([routerComponent hasPrefix:@":"]) {
//去除參數(shù)的:,然后將參數(shù)名作為 key,將對應(yīng)的 givenComponent 作為值存入字典中,所以在調(diào)用路由的時候,傳遞參數(shù)(實參)順序要一致,否則參數(shù)就錯亂了
NSString *key = [routerComponent substringFromIndex:1];
[params setObject:givenComponent forKey:key];
}
else if (![routerComponent isEqualToString:givenComponent]) {
//在非傳參數(shù)的情況下,如果路徑不一致,則結(jié)束。結(jié)束后會去路由表中拿下一個路由來判斷。
params = nil;
*stop = YES;
}
}];
return params;
}
將路由跳轉(zhuǎn)最重要的三個方法分析了一下,在重要的代碼前都加上了注釋。接下來總結(jié)一下整體的思路。
注冊的時候,比較簡單,將我們的路徑和 VC 傳遞進去,保存在字典中就可以了。
跳轉(zhuǎn)的時候,做的判斷就比較多。首先判斷緩存中是否有這個路徑,如果有的話,直接跳轉(zhuǎn),在注釋中也詳細說明了為什么要緩存。如果沒有的話,則去枚舉這個路由字典,并組裝數(shù)據(jù),存入緩存中。
任何框架,都會有不完美的地方,沒錯,這里要說說了。如果需要給你跳轉(zhuǎn)的 VC 傳遞數(shù)據(jù),那么需要你的 VC 實現(xiàn)這個方法:initWithRouterParams:params,通過params去獲取你的值。其實在這里也可以通過獲取這個 VC 的所有屬性,在創(chuàng)建這個 VC 的時候,通過 KVC 的方式把值賦給這個 VC 的屬性。
另一種實現(xiàn)辦法是擴展 UIViewController,在這里可以這樣做
@interface UIViewController (Routable)
@property (nonatomic, strong) NSDictionary *params;
@end
@implementation UIViewController (Routable)
static char kAssociatedParamsObjectKey;
- (void)setParams:(NSDictionary *)params{
objc_setAssociatedObject(self, &kAssociatedParamsObjectKey, params, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params
{
return objc_getAssociatedObject(self, &kAssociatedParamsObjectKey);
}
@end
這樣每個 ViewController 中就不用實現(xiàn)固定的方法了,在使用的時候,直接調(diào)用 self. params 就可以拿到這個字典了。
建議
在routable-ios中給出的注冊路由的方式是,一個 VC 一個 VC 的注冊??梢詫⑿枰酚商D(zhuǎn)的 VC 配置到 plist 文件中,寫一個方法,讀取 plist 文件,循環(huán)注冊即可,在application:didFinishLaunchingWithOptions:方法中,調(diào)用注冊路由的方法即可。
我 fork 了一份代碼,并在里面添加了注釋,想通過 Xcode 看的,可以下載下來看。 傳送門