routable-ios 源碼解析

routable-ios 是什么?可以用來做什么?與之類似的框架還有哪些?

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

先看一下 routable-ios 中類的關(guān)系:

routable-ios 類組織結(jié)構(gòu).png

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 文件中做聲明,這個類只在 RoutableUPRouter 中的實現(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 看的,可以下載下來看。 傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容