routable-ios 源码解析

routable-ios 是什么?可以用来做什么?与之类似的框架还有哪些?

  • routable-ios 是一个路由框架,由两个文件四个类组成,其中核心的类就一个。
  • 可以很方便的实现 iOS 中 ViewController 之间的跳转。跳转方式也可以灵活的设置,后面具体会讲到。
  • 类似的框架还有 ABRouter & HHRouter。后期的文章也会对 HHRouter 做介绍。

先看一下 routable-ios 中类的关系:

routable-ios 类组织结构.png

Routable 继承自 UPRouter,主要的功能都在 UPRouter 类中,路由主要的功能其实就两个:

  • 注册希望路由跳转的类、及 URL
  • 进行跳转

看一下如何使用routable-ios:

  • routable-ios导入项目
  • 注册路由:
    [[Routable sharedRouter] map:@"user/:name/:age" toController:[UserController class]];
  • 调用路由进行跳转:
    [[Routable sharedRouter] open:@"user/chenyu/28"];
  • 在 VC 中获取传递的参数
@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; //提供单例方法,用来创建路由类
+ (instancetype)newRouter;     //另一种创建路由的方式,一般不推荐,不是单例。

UPRouterOptions 继承自 NSObject
首先看一下这个类提供的一些属性,我们就知道这个类是做什么的了。

@property (readwrite, nonatomic, getter=isModal) BOOL modal;  //是否是模态视图
@property (readwrite, nonatomic) UIModalPresentationStyle presentationStyle;  //VC 显示的样式
@property (readwrite, nonatomic) UIModalTransitionStyle transitionStyle;  //VC 出现时的动画
@property (readwrite, nonatomic, strong) NSDictionary *defaultParams;  //默认的数据
@property (readwrite, nonatomic, assign) BOOL shouldOpenAsRootViewController; //是否是根视图

//.m 文件中的两个属性
@property (readwrite, nonatomic, strong) Class openClass;  //注册的类
@property (readwrite, nonatomic, copy) RouterOpenCallback callback;  //block 回调

通过以上内容,可以看到UPRouterOptions其实就是一个配置类,里面存储路由跳转时需要的一些数据,可以理解成一个辅助的类。这个类中提供了一系列的工厂方法,用来创建不同类型的对象,比如(只列举部分函数,其他同类型的函数还有很多,功能大体一致,只是某个配置项不同而已。):

  • 全部使用默认配置
//Default construction; like [NSArray array]
+ (instancetype)routerOptions {
  return [self routerOptionsWithPresentationStyle:UIModalPresentationNone
                                  transitionStyle:UIModalTransitionStyleCoverVertical
                                    defaultParams:nil
                                           isRoot:NO
                                          isModal:NO];
}
  • 传入所有参数创建对象
//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;
}
  • 自定义部分参数创建对象
//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 中的实现中才用到,而这三个类都在一个文件中,所以也没有必要出现在 .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

这个类的出现,主要作用是将跳转时匹配好的所有内容存起来,缓存到另一个字典中,未来再次跳转的时候,直接可以拿出来用,你也许会问,我们的路由不是在一个字典里吗,也可以直接拿出来用,为什么还要缓存,后续到源代码的地方会细说,为什么要缓存,为什么跳转的时候不是直接去 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;
//注册路由,本篇主要分析的方法。上面的方法最终会调用这个方法,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;
//路由跳转,本篇主要分析的方法。上面两个方法最终都会调用这个方法。
- (void)open:(NSString *)url animated:(BOOL)animated extraParams:(NSDictionary *)extraParams;

- (NSDictionary*)paramsOfUrl:(NSString*)url;

@end
@interface UPRouter ()

// 存储注册的路由
@property (readwrite, nonatomic, strong) NSMutableDictionary *routes;
// 缓存已跳转过的路由
@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,则会创建一个默认的配置对象
  if (!options) {
    options = [UPRouterOptions routerOptions];
  }
  options.openClass = controllerClass;
  [self.routes setObject:options forKey:format];
}

路由跳转
路由跳转做的事情比较多,一共有三个比较重要的方法,会详细看,首先看路由跳转的方法

- (void)open:(NSString *)url
    animated:(BOOL)animated
 extraParams:(NSDictionary *)extraParams
{
  //获取路由跳转相关的参数,往下滑动,先看怎么获取的数据,看完下面的方法再回来看这个方法
  RouterParams *params = [self routerParamsForUrl:url extraParams: extraParams];
  UPRouterOptions *options = params.routerOptions;
  
  //好了,拿到数据了,开始跳转。先判断是否有回调,如果有的话,则去执行 block
  if (options.callback) {
    RouterOpenCallback callback = options.callback;
    callback([params controllerParams]);
    return;
  }
  //此处删除了判断 self.navigationController 是否存在的容错代码,无关紧要。
  
  //获取将要跳转的 VC,并且将我们传递的数据以字典的形式,传递给这个 VC
  //controllerForRouterParams 这个方法比较简单,打断点进去看看就 OK 了。
  UIViewController *controller = [self controllerForRouterParams:params];
  
  //判断当前是否有 presented 的 ViewController,有的话要 dismiss,因为接下来要跳转或者 presentViewController
  if (self.navigationController.presentedViewController) {
    [self.navigationController dismissViewControllerAnimated:animated completion:nil];
  }
  
  //是否是以模态的方式弹出 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) {
    //设置根视图
    [self.navigationController setViewControllers:@[controller] animated:animated];
  }
  else {
    //直接 push 一个 ViewController
    [self.navigationController pushViewController:controller animated:animated];
  }
}

获取路由跳转相关的参数方法(删除了一些容错处理的代码):

- (RouterParams *)routerParamsForUrl:(NSString *)url extraParams: (NSDictionary *)extraParams {
  //如果缓存中已经有了(证明之前已经跳转过这个 VC),并且传递的参数没有变化。
  //这里需要注意了,如果传递的参数你也不确定是不是没变化,最好给 extraParams 给个值,这样就不会走缓存了
  //否则可能传递的数据变了,但是走的还是之前的缓存。
  //如果 VC 之间不要传递数据,不用考虑这个问题
  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"] 的方式获取,
  //因为注册的时候,你后面添加的是参数(形参),在跳转的时候传递的是数据(实参)。
  //这里也就是为什么需要缓存的原因了,每次跳转都要枚举这个字典,缓存了以后时间复杂度直接降到了 O(1)。
  __block RouterParams *openParams = nil;
  [self.routes enumerateKeysAndObjectsUsingBlock:
   ^(NSString *routerUrl, UPRouterOptions *routerOptions, BOOL *stop) {
     //routerUrl 是枚举到的 key,也是当时注册路由时添加进去的 url,routerOptions 是枚举到的 value

     NSArray *routerParts = [routerUrl pathComponents];
     //判断注册的路由地址和跳转的带参数的地址是否一致,最简单的办法就是判断他们包含的元素个数是否一致,如果一致,再做更详细的判断
     if ([routerParts count] == [givenParts count]) {
       //如果个数一致,再判断是否匹配
       NSDictionary *givenParams = [self paramsForUrlComponents:givenParts routerUrlComponents:routerParts];
       if (givenParams) {
         //givenParams 存储的是路由地址中给的数据,再将 extraParams 一起传入 RouterParams,创建 RouterParams 的对象。
         openParams = [[RouterParams alloc] initWithRouterOptions:routerOptions openParams:givenParams extraParams: extraParams];
         *stop = YES;//结束遍历
       }
     }
   }];
  
  //如果没有匹配到路由
  if (!openParams) {
    //用户设置了忽略异常,直接返回 nil,否则会走 @throw
    if (_ignoresExceptions) {
      return nil;
    }
    @throw [NSException exceptionWithName:@"RouteNotFoundException"
                                   reason:[NSString stringWithFormat:ROUTE_NOT_FOUND_FORMAT, url]
                                 userInfo:nil];
  }
  //将我们辛辛苦苦封装好的路由相关的所有数据缓存起来,下次在走这个 url 的时候,直接取缓存中的数据,这就是为什么要缓存了。
  //除非你传递的参数变了,那么一定传给 extraParams,相关方法检测到 extraParams 不为空,会重新组装数据。
  [self.cachedRoutes setObject:openParams forKey:url];
  return openParams;
}
//判断注册的路由和跳转的路由是否一致
- (NSDictionary *)paramsForUrlComponents:(NSArray *)givenUrlComponents
                     routerUrlComponents:(NSArray *)routerUrlComponents {
  
  __block NSMutableDictionary *params = [NSMutableDictionary dictionary];
  [routerUrlComponents enumerateObjectsUsingBlock:
   ^(NSString *routerComponent, NSUInteger idx, BOOL *stop) {
     
     NSString *givenComponent = givenUrlComponents[idx];
     //判断是否是形参,所以在注册路由时,一定要注意,参数以:开始,否则会当成路径字符串
     if ([routerComponent hasPrefix:@":"]) {
       //去除参数的:,然后将参数名作为 key,将对应的 givenComponent 作为值存入字典中,所以在调用路由的时候,传递参数(实参)顺序要一致,否则参数就错乱了
       NSString *key = [routerComponent substringFromIndex:1];
       [params setObject:givenComponent forKey:key];
     }
     else if (![routerComponent isEqualToString:givenComponent]) {
       //在非传参数的情况下,如果路径不一致,则结束。结束后会去路由表中拿下一个路由来判断。
       params = nil;
       *stop = YES;
     }
   }];
  return params;
}

将路由跳转最重要的三个方法分析了一下,在重要的代码前都加上了注释。接下来总结一下整体的思路。
注册的时候,比较简单,将我们的路径和 VC 传递进去,保存在字典中就可以了。
跳转的时候,做的判断就比较多。首先判断缓存中是否有这个路径,如果有的话,直接跳转,在注释中也详细说明了为什么要缓存。如果没有的话,则去枚举这个路由字典,并组装数据,存入缓存中。

任何框架,都会有不完美的地方,没错,这里要说说了。如果需要给你跳转的 VC 传递数据,那么需要你的 VC 实现这个方法:initWithRouterParams:params,通过params去获取你的值。其实在这里也可以通过获取这个 VC 的所有属性,在创建这个 VC 的时候,通过 KVC 的方式把值赋给这个 VC 的属性。

另一种实现办法是扩展 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 中就不用实现固定的方法了,在使用的时候,直接调用 self. params 就可以拿到这个字典了。

建议
routable-ios中给出的注册路由的方式是,一个 VC 一个 VC 的注册。可以将需要路由跳转的 VC 配置到 plist 文件中,写一个方法,读取 plist 文件,循环注册即可,在application:didFinishLaunchingWithOptions:方法中,调用注册路由的方法即可。

我 fork 了一份代码,并在里面添加了注释,想通过 Xcode 看的,可以下载下来看。 传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容