/ LJROUTER

LJRouter源码分析

LianjiaTech/LJRouter是链家开源的一款路由组件,使用方法详见链接。这篇文章是对该路由的实现思路的学习。

声明注册信息

每一个支持LJRouter的ViewController必须实现Init方法,即:

LJRouterInit(@"注释", 页面Key, (类型1)参数名1, (类型2)参数名2, ...) // 没有参数可以不写

该方法是一个宏,封装了一系列宏和代码。其中有一块代码构建了静态LJRouterRegister结构体,把注册信息,即上面代码中的Key和参数以及没有体现出来的类名,写入结构体,并指定其存储在名为__LJRouter的数据块中。

__used static struct LJRouterRegister name_combined_by_params                                                   
__attribute__ ((used, section ("__DATA,__LJRouter"))) = {...}                              

加载注册信息

程序运行后,在需要进行路由跳转时,调用loadAllPageProperty懒加载注册信息。通过上一步定义的标识__LJRouter,获取所有描述了注册信息的结构体。

uint8_t *data = getsectiondata(header, "__DATA", "__LJRouter",&size);
uint32_t count = (uint32_t)(size / sizeof(struct LJRouterRegister));
for (uint32_t i = 0 ; i < count ; i ++) {
    //...见下文
}

构造一个更为友好的LJInvocationCenterItemRule的类对象,用保存获取Router对象的所有描述信息。如下可见,有selector、返回类型和参数数组等。

@interface LJInvocationCenterItemRule : NSObject
@property (nonatomic, readonly) SEL selector;
@property (nonatomic, readonly) NSString *returnTypeName;
@property (nonatomic, readonly) NSString *returnTypeEncoding;
@property (nonatomic, readonly) NSArray<LJInvocationCenterItemParam*>* paramas;
//...此处省略一个方法
@end

将所有构造的Rule对象,相应的Key和Target一并加入LJInvocationCenter。Key是LJRouterInit中定义的Router标识。Target对应的ViewController类。LJRouter是一个单例,所以加载后这些健值对就常驻在LJInvocationCenter内存中了。

{
     //对...我就是上文循环中的方法...
     //此处先省略一万字...
     LJInvocationCenterItemRule *rule =
                [[LJInvocationCenterItemRule alloc] initWithSelector:NSSelectorFromString(items[i].selName)
                                                      returnTypeName:items[i].returnTypeName
                                                  returnTypeEncoding:[NSString stringWithUTF8String:items[i].returnTypeEncoding]
                                                              params:params];

      [center addInvocationWithKey:items[i].key.lowercaseString
                            target:NSClassFromString(className)
                              rule:rule];
}
            

Rule只代表一种构造方法,一个ViewController可以有多种初始化方式。所以在前面addInvocationWithKey:target:rule方法中,会将Key、Target和Rule再进一步封装到名为LJInvocationCenterItem的对象中,同样Key的不同rule会被添加到rules数组中,LJRouter会rule参数匹配合适方法初始化。

@interface LJInvocationCenterItem : NSObject

@property (nonatomic, readonly) NSString *key;
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) NSArray<LJInvocationCenterItemRule*> *rules;

@end

安全检查

在Debug环境下,通过宏__LJRouterUseCheck定义了安全检查。即在上一步加载完成后,会调用checkAllMethodTypeName方法进行校验。如果发现声明LJRouterInit和使用LJRouterUsePage时的方法名和参数不能匹配,即刻给予抛出异常。使用者能认识到出错的位置。这也是为什么明明可以用字符串类型的URL来进行Route,源码里还要建议用强制声明的函数来跳转。虽然稍显繁琐,但是安全,只是一种权衡。对于外部跳转,无论App或者WebView,都无可避免的使用URL字符串。这个时候采用下面的routerKey方法,是一个合理的方案。因为字符串可能认为输错,所以可以自定义实现canNotRouterBlock方法处理没有匹配的情况。

- (BOOL)routerKey:(NSString*)key
             data:(NSDictionary*)data
		   sender:(UIResponder*)sender
        pageBlock:(void(^)(__kindof UIViewController* viewController))pageBlock
    callbackBlock:(void(^)(NSString* key,NSString *value,NSString* data,BOOL complete))callbackBlock
canNotRouterBlock:(void(^)(void))canNotRouterBlock;

路由跳转

LJRouterUsePage其实是用宏声明了一个跳转函数。在程序内部,我们通过跳转函数来进行ViewController之间的切换,除了上文提到的安全,调用时所需参数也一目了然,是拥有强可读性的代码。

//宏声明
LJRouterUsePage(webview, (NSString*)url);
//实际生成函数
open_webview_controller_with_url(id sender, NSString *url) {
    //...此处又省略一万字
}
//调用
open_webview_controller_with_url(self, @"http://example.com");

跳转函数里面到底做了什么操作呢?以下是宏与objc参杂的一块简化后的代码。

第一步:调用LJRouterGetClassForKey函数,通过Key(上例为webview)从LJInvocationCenter中,获取target,也就是对应的ViewController的Class。

第二步:使用LJ_ROUTER_FUNCTION_PARAM_SELNAME结合参数,拼接出这个类的构造的方法,并构造对应的Selector。从这里可以看出来无论是LJRouterInit还是LJRouterUsePage它们一定是使用相同的拼接逻辑,这样才能保证最终能够匹配成功,而不是出现空指针异常。拼接逻辑详见宏LJ_ROUTER_FUNCTION_PARAM_DEFINELJ_ROUTER_FUNCTION_PARAM_SELNAME

第三步:通过runtime的class_getMethodImplementation方法获取到ViewController真实的函数指针。可以注意到这个是一个类方法,说明LJRouterInit同时定义了两个初始化方法,类方法和实例方法,应用于不同的场景。

第四步:调用函数指针,获取ViewController的实例。三四两步都用到了名为LJ_ROUTER_FOREACH_ARGS的宏用于展开参数。有兴趣的小伙伴可以进一步研究,宏无能的就先溜了。最后调用openViewController进行跳转。

//第一步
Class theClass = LJRouterGetClassForKey(@#KEY);

//第二步
NSString *selName =LJ_ROUTER_FUNCTION_PARAM_SELNAME(GET_FUN_PRE,GET_FUN_PRE2,LJ_ROUTER_CREATE_SEL_STR,##__VA_ARGS__);    
SEL sel = NSSelectorFromString(selName);

//第三步
GET_FUN_RET_TYPE(*imp)(id,SEL LJ_ROUTER_FOREACH_ARGS(LJ_ROUTER_PARAMS_DOT_TYPEOF_VALUE,1,##__VA_ARGS__  )) =  
(void*)class_getMethodImplementation(object_getClass(theClass), sel);  

//第四步
UIViewController *vc = 
imp(theClass,sel LJ_ROUTER_FOREACH_ARGS(LJ_ROUTER_PARAMS_DOT_AND_VALUE,1,##__VA_ARGS__)); 
[[LJRouter sharedInstance] openViewController:vc withSender:controller];

总结

本文只是简单介绍了一下LJRouter从声明到调用的流程。LJRouter相比市面早期的Router更加高效和安全,非常推荐适用。代码中有很多复杂的宏定义,使得在声明的时候变得简洁。但限于本人理解不深,恕不能深入展开。

以下是我总结的优点:

  1. app内用宏定义的函数跳转,目标及传参明确。
  2. 注册过程全自动化。
  3. 检查工作能够及早发现定义错误的跳转函数。
  4. 默认支持字符串类型的类型的路由跳转。
  5. 自定义错误处理。
  6. 不仅仅跳转,还可以自定义Action:通过Router调用一个方法。
  7. 可以导入元数据进行二次开发(详见源码README)。

如有错误或疑问,还望不吝致评。

补充 March 9, 2018 – 15:11

跟作者沟通后,同步了一下他的想法,主要有两点:

第一点体现在元数据的拓展性及代码自动生成。

将所有的页面启动需要的元数据描述出来。这样每个action/page都是可以通过一些适配器去调用的。 比如我现在想为webview提供一票接口,就可以直接生成js。或者拿着这些资源注入到jscore。想为React Native也没提供接口也是一样的。

第二点体现了去中心化思想,自动化的注册与维护。

我之前的项目里面有很多中心的配置文件,这些文件大家经常忘记添加新配置或者更新代码的时候忘记更新配置,最后就导致大家对这个文件不敢改不敢删,维护成本变高。这个去中心的思路下,每一个功能注册的代码都和业务代码在一起,可以让大家维护起来更方便一些。基于这个思路,我能想到比较便捷的方式就是放到section了。对应到section的方案,也只有宏能搞定了。其实正常并不推荐这个方式,实在是被逼的。Swift都不让相关操作了。

其实由于LJInvocationCenter的存在,实现原理是依旧是中心化的处理。只不过在用户声明上感知不出来。所以作者又补充了一下另一个思路:

在宏注册的时候,其实可以直接生成一个c函数。在使用的时候直接extern c函数 这样整个调用就完全没有中心了。

注:如果使用了不当的extern函数,编译器也会直接报错ld: symbol(s) not found for architecture x86_64。关于extern的副作用syntax - Effects of the extern keyword on C functions - Stack Overflow有很好的讨论,在LJRouter场景下,应该不存上述问题。

针对“不敢改不敢删”的问题,通过安全检查能够很好的发现。所以开发人员在重构过程中不至于畏手畏脚,代码质量也会提升。最近在两个项目之间拷贝代码时候就出现了不同的ViewController注册了同样的key。或者有需要的ViewControll没有被引入,这些都及时的在编译和运行检查初级就被发现了。