routable-ios 源码解析

作者: chenyu1520 | 来源:发表于2017-11-01 21:56 被阅读81次

    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 看的,可以下载下来看。 传送门

    相关文章

      网友评论

        本文标题:routable-ios 源码解析

        本文链接:https://www.haomeiwen.com/subject/cpkgpxtx.html