ARouter原理剖析及手动实现

ARouter原理剖析及手动实现

 

前言

路由跳转在项目中用了一段时间了,最近对Android中的ARouter路由原理也是研究了一番,因而就给你们分享一下本身的心得体会,并教你们如何实现一款简易的路由框架。

本篇文章分为两个部分,第一部分着重剖析ARouter路由的原理,第二部分会带着你们仿照ARouter撸一个本身的路由框架,咱们本身撸的路由框架可能没有Arouter众多的功能如过滤器、provider等,可是却实现了ARouter最核心的功能:路由跳转,同时你也能学会如何去设计一个框架等等。

这里先附上我本身实现的路由框架demo地址:ARouter原理剖析及手动实现,demo点我访问,欢迎starjavascript

第一部分:ARouter原理剖析

说到路由便不得不提一下Android中的组件化开发思想,组件化是最近比较流行的架构设计方案,它能对代码进行高度的解耦、模块分离等,能极大地提升开发效率(若有同窗对组件化有不理解,能够参考网上众多的博客等介绍,而后再阅读demo源码中的组件化配置进行熟悉)。路由和组件化自己没有什么联系,由于路由的责任是负责页面跳转,可是组件化中两个单向依赖的module之间须要互相启动对方的Activity,由于没有相互引用,startActivity()是实现不了的,必须须要一个协定的通讯方式,此时相似ARouter和ActivityRouter等的路由框架就派上用场了。php

  • 第一节:ARouter路由跳转的原理

<img src="http://pcayc3ynm.bkt.clouddn.com/module_1.png" /> java

如上图,在组件化中,为了业务逻辑的完全解耦,同时也为了每一个module均可以方便的单独运行和调试,上层的各个module不会进行相互依赖(只有在正式联调的时候才会让app壳module去依赖上层的其余组件module),而是共同依赖于base module,base module中会依赖一些公共的第三方库和其余配置。那么在上层的各个module中,如何进行通讯呢?

咱们知道,传统的Activity之间通讯,经过startActivity(intent),而在组件化的项目中,上层的module没有依赖关系(即使两个module有依赖关系,也只能是单向的依赖),那么假如login module中的一个Activity须要启动pay_module中的一个Activity便不能经过startActivity来进行跳转。那么你们想一下还有什么其余办法呢? 可能有同窗会想到隐式跳转,这固然也是一种解决方法,可是一个项目中不可能全部的跳转都是隐式的,这样Manifest文件会有不少过滤配置,并且很是不利于后期维护。固然你用反射拿到Activity的class文件也能够实现跳转,可是第一:大量的使用反射跳转对性能会有影响,第二:你须要拿到Activity的类文件,在组件开发的时候,想拿到其余module的类文件是很麻烦的,由于组件开发的时候组件module之间是没有相互引用的,你只能经过找到类的路径去反射拿到这个class,那么有没有一种更好的解决办法呢?办法固然是有的。下面看图:

<img src="http://pcayc3ynm.bkt.clouddn.com/module_2.png" />

在组件化中,咱们一般都会在base_module上层再依赖一个router_module,而这个router_module就是负责各个模块之间页面跳转的。

用过ARouter路由框架的同窗应该都知道,在每一个须要对其余module提供调用的Activity中,都会声明相似下面@Route注解,咱们称之为路由地址git

@Route(path = "/main/main") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } @Route(path = "/module1/module1main") public class Module1MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_module1_main); } } 

那么这个注解有什么用呢,路由框架会在项目的编译期经过注解处理器扫描全部添加@Route注解的Activity类,而后将Route注解中的path地址和Activity.class文件映射关系保存到它本身生成的java文件中。为了让你们理解,我这里来使用近乎伪代码给你们简单演示一下。github

public class MyRouters{ //项目编译后经过apt生成以下方法 public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) { route.put("/main/main", MainActivity.class); route.put("/module1/module1main", Module1MainActivity.class); route.put("/login/login", LoginActivity.class); } } 

这样咱们想在app模块的MainActivity跳转到login模块的LoginActivity,那么便只需调用以下:api

//不一样模块之间启动Activity public void login(String name, String password) { HashMap<String, ClassBean> route = MyRouters.getRouteInfo(new HashMap<String, ClassBean>); LoginActivity.class classBean = route.get("/login/login"); Intent intent = new Intent(this, classBean); intent.putExtra("name", name); intent.putExtra("password", password); startActivity(intent); } 

这样是否是很简单就实现了路由的跳转,既没有隐式意图的繁琐,也没有反射对性能的损耗。用过ARouter的同窗应该知道,用ARouter启动Activity应该是下面这个写法bash

// 2. Jump with parameters ARouter.getInstance().build("/test/login") .withString("password", 666666) .withString("name", "小三") .navigation(); 

那么ARouter背后是怎么样实现跳转的呢?实际上它的核心思想跟上面讲解是同样的,咱们在代码里加入的@Route注解,会在编译时期经过apt生成一些存储path和activity.class映射关系的类文件,而后app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),而后在进行路由跳转的时候,经过build()方法传入要到达页面的路由地址,ARouter会经过它本身存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),而后new Intent(context, activity.Class),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样即可以实现两个相互没有依赖的module顺利的启动对方的Activity了。架构

  • 第二节:ARouter映射关系如何生成

经过上节咱们知道在Activity类上加上@Route注解以后,即可经过apt生成对应的路由表。那么如今咱们来搞清楚,既然路由和Activity的映射关系咱们能够很容易地获得(由于代码都是咱们写的,固然很容易获得),那么为何咱们要繁琐的经过apt来生成类文件而不是本身直接写一个契约类来保存映射关系呢。若是站在一个框架开发者的角度去理解,就不难明白了,由于框架是给上层业务开发者调用的,若是业务开发者在开发页面的过程当中还要时不时的更新或更改契约类文件,难免过于麻烦?若是有自动根据路由地址生成映射表文件的技术该多好啊!app

技术固然是有的,那就是被众多框架使用的apt及javapoet技术,那么什么是apt,什么是javapoet呢?咱们先来看下图:

<img src="http://pcayc3ynm.bkt.clouddn.com/apt_javapoet.png" />

APT是Annotation Processing Tool的简称,即注解处理工具。由图可知,apt是在编译期对代码中指定的注解进行解析,而后作一些其余处理(如经过javapoet生成新的Java文件)。咱们经常使用的ButterKnife,其原理就是经过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,而后生成XXX_ViewBinding类,实现了view的绑定。javapoet是鼎鼎大名的squareup出品的一个开源库,是用来生成java文件的一个library,它提供了简便的api供你去生成一个java文件。能够以下引入javapoet框架

implementation 'com.squareup:javapoet:1.7.0' 

下面我经过demo中的例子带你了解如何经过apt和javapoet技术生成路由映射关系的类文件:

首先第一步,定义注解:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Route { /** * 路由的路径 * @return */ String path(); /** * 将路由节点进行分组,能够实现动态加载 * @return */ String group() default ""; } 

这里看到Route注解里有path和group,这即是仿照ARouter对路由进行分组。由于当项目变得愈来愈庞大的时候,为了便于管理和减少首次加载路由表过于耗时的问题,咱们对全部的路由进行分组。在ARouter中会要求路由地址至少须要两级,如"/xx/xx",一个模块下能够有多个分组,这里咱们就将路由地址定为必须大于等于两级,其中第一级是group。

第二步,在Activity上使用注解

@Route(path = "/main/main") public class MainActivity extends AppCompatActivity { } @Route(path = "/main/main2") public class Main2Activity extends AppCompatActivity { } @Route(path = "/show/info") public class ShowActivity extends AppCompatActivity { } 

第三步,编写注解处理器,在编译器找到加入注解的类文件,进行处理,这里我只展现关键代码,具体的细节还须要你去demo中仔细研读:

@AutoService(Processor.class)
/** 处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数 */ @SupportedOptions(Constant.ARGUMENTS_NAME) /** * 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数 */ @SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE) public class RouterProcessor extends AbstractProcessor { /** * key:组名 value:类名 */ private Map<String, String> rootMap = new TreeMap<>(); /** * 分组 key:组名 value:对应组的路由信息 */ private Map<String, List<RouteMeta>> groupMap = new HashMap<>(); /** * * @param set 使用了支持处理注解的节点集合 * @param roundEnvironment 表示当前或是以前的运行环境,能够经过该对象查找找到的注解。 * @return true 表示后续处理器不会再处理(已经处理) */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (!Utils.isEmpty(set)) { //被Route注解的节点集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; } //... } 

如代码中所示,要想在编译期对注解作处理,就须要RouterProcessor继承自AbstractProcessor并经过@AutoService注解进行注册,而后实现process()方法。尚未完,你还须要经过@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定要处理哪一个注解,Constant.ANNOTATION_TYPE_ROUTE即是咱们的Route注解的路径。看process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)方法,set集合就是编译期扫描代码获得的加入了Route注解的文件集合,而后咱们就能够在process方法生成java文件了。

这里的@AutoService是为了注册注解处理器,须要咱们引入一个google开源的自动注册工具AutoService,以下依赖(固然也能够手动进行注册,不过略微麻烦,这里不太推荐):

implementation 'com.google.auto.service:auto-service:1.0-rc2' 

第四步:经过javapoet生成java类:
在第三步中process()方法里有一句代码:processorRoute(rootElements),这个就是生成java文件的方法了,下面我贴出代码:

private void processorRoute(Set<? extends Element> rootElements) { //... //生成Group记录分组表 generatedGroup(iRouteGroup); //生成Root类 做用:记录<分组,对应的Group类> generatedRoot(iRouteRoot, iRouteGroup); } 

processorRoute()方法内容不少,这里我只贴出生成java文件相关,其余代码我会在第二部分手动实现路由框架中详细介绍。如上,generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成java文件的核心了。这里我只贴出generatedRoot()方法,由于生成类文件的原理都是同样的,至于生成什么功能的类,只要你会一个,触类旁通,这便没有什么难度。

/** * 生成Root类 做用:记录<分组,对应的Group类> * @param iRouteRoot * @param iRouteGroup */ private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) { //建立参数类型 Map<String,Class<? extends IRouteGroup>> routes> //Wildcard 通配符 ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get( ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup)) )); //生成参数 Map<String,Class<? extends IRouteGroup>> routes> routes ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build(); //生成函数 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes) MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(parameter); //生成函数体 for (Map.Entry<String, String> entry : rootMap.entrySet()) { methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue())); } //生成$Root$类 String className = Constant.NAME_OF_ROOT + moduleName; TypeSpec typeSpec = TypeSpec.classBuilder(className) .addSuperinterface(ClassName.get(iRouteRoot)) .addModifiers(Modifier.PUBLIC) .addMethod(methodBuilder.build()) .build(); try { //生成java文件,PACKAGE_OF_GENERATE_FILE就是生成文件须要的路径 JavaFile.builder(Constant.PACKAGE_OF_GENERATE_FILE, typeSpec).build().writeTo(filerUtils); log.i("Generated RouteRoot:" + Constant.PACKAGE_OF_GENERATE_FILE + "." + className); } catch (IOException e) { e.printStackTrace(); } } 

如上,我把每一块代码的做用注释了出来,相信你们很容易就能理解每个代码段的做用。可见,其实生成文件只是调用一些api而已,只要咱们熟知api的调用,生成java文件便没有什么难度。

第二部分:动手实现一个路由框架

经过第一部分的讲述,我相信你们对于ARouter的原理已经有了总体轮廓的理解,这一部分,我便会经过代码带你去实现一个本身的路由框架。要实现这个路由框架,咱们先来实现生成路由映射文件这一块,由于这一块是路由框架可以运行起来的核心。

  • 第一节:生成路由映射文件

经过第一部分的讲述咱们知道在Activity类上加上@Route注解以后,即可经过apt来生成对应的路由表,那么如今咱们就来生成这些路由映射文件。首先,咱们要理解一个问题,就是咱们的路由映射文件是在编译期间生成的,那么在程序的运行期间咱们要统一调用这些路由信息,便须要一个统一的调用方式。咱们先来定义这个调用方式:

public interface IRouteGroup { void loadInto(Map<String, RouteMeta> atlas); } public interface IRouteRoot { void loadInto(Map<String, Class<? extends IRouteGroup>> routes); } 

咱们定义两个接口来对生成的java文件进行约束,IRouteGroup是生成的分组关系契约,IRouteRoot是单个分组路由信息契约,只要咱们生成的java文件继承自这个接口并实现loadInto()方法,在运行期间咱们就能够统一的调用生成的java文件,获取路由映射信息。

如今咱们来把RouterProcessor生成路由映射文件相关的代码补全:

@AutoService(Processor.class) /** 处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数 */ @SupportedOptions(Constant.ARGUMENTS_NAME) /** * 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数 */ @SupportedSourceVersion(SourceVersion.RELEASE_7) /** * 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数 */ @SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE) public class RouterProcessor extends AbstractProcessor { /** * key:组名 value:类名 */ private Map<String, String> rootMap = new TreeMap<>(); /** * 分组 key:组名 value:对应组的路由信息 */ private Map<String, List<RouteMeta>> groupMap = new HashMap<>(); /** * 节点工具类 (类、函数、属性都是节点) */ private Elements elementUtils; /** * type(类信息)工具类 */ private Types typeUtils; /** * 文件生成器 类/资源 */ private Filer filerUtils; private String moduleName; private Log log; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //得到apt的日志输出 log = Log.newLog(processingEnvironment.getMessager()); elementUtils = processingEnvironment.getElementUtils(); typeUtils = processingEnvironment.getTypeUtils(); filerUtils = processingEnvironment.getFiler(); //参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件 Map<String, String> options = processingEnvironment.getOptions(); if (!Utils.isEmpty(options)) { moduleName = options.get(Constant.ARGUMENTS_NAME); } if (Utils.isEmpty(moduleName)) { throw new RuntimeException("Not set processor moudleName option !"); } log.i("init RouterProcessor " + moduleName + " success !"); } /** * * @param set 使用了支持处理注解的节点集合 * @param roundEnvironment 表示当前或是以前的运行环境,能够经过该对象查找找到的注解。 * @return true 表示后续处理器不会再处理(已经处理) */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (!Utils.isEmpty(set)) { //被Route注解的节点集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; } //... } 

咱们经过@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每一个module的名字,用来生成对应module下存放路由信息的类文件名。这里变量Constant.ARGUMENTS_NAME的值就是moduleName,在这以前,咱们须要在每一个组件module的gradle下配置以下

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()] } } 

@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了须要处理的注解的路径地址,在此就是Route.class的路径地址。

RouterProcessor中咱们实现了init方法,拿到log apt日志输出工具用以输出apt日志信息,并经过如下代码获得上面提到的每一个module配置的moduleName

//参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件 Map<String, String> options = processingEnvironment.getOptions(); if (!Utils.isEmpty(options)) { moduleName = options.get(Constant.ARGUMENTS_NAME); } if (Utils.isEmpty(moduleName)) { throw new RuntimeException("Not set processor moudleName option !"); } 

而后在process()方法里开始生成文件名以EaseRouter_Route_moduleName和EaseRouter_Group_moduleName命名的文件。(这里的moduleName指具体的module名,demo中apt相关的代码实现都在easy-compiler module中),生成EaseRouter_Route_moduleName相关文件存储的就是分组关系,生成EaseRouter_Group_moduleName相关文件里存储的就是分组下的路由映射关系。

好了,咱们终于能够生成文件了,在process()方法里有以下代码,

if (!Utils.isEmpty(set)) { //被Route注解的节点集合 Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class); if (!Utils.isEmpty(rootElements)) { processorRoute(rootElements); } return true; } return false; 

set就是扫描获得的支持处理注解的节点集合,而后获得rootElements,即被@Route注解的节点集合,此时就能够调用
processorRoute(rootElements)方法去生成文件了。processorRoute(rootElements)方法实现以下:

private void processorRoute(Set<? extends Element> rootElements) { //得到Activity这个类的节点信息 TypeElement activity = elementUtils.getTypeElement(Constant.ACTIVITY); TypeElement service = elementUtils.getTypeElement(Constant.ISERVICE); for (Element element : rootElements) { RouteMeta routeMeta; //类信息 TypeMirror typeMirror = element.asType(); log.i("Route class:" + typeMirror.toString()); Route route = element.getAnnotation(Route.class); if (typeUtils.isSubtype(typeMirror, activity.asType())) { routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element); } else if (typeUtils.isSubtype(typeMirror, service.asType())) { routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element); } else { throw new RuntimeException("Just support Activity or IService Route: " + element); } categories(routeMeta); } TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP); TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT); //生成Group记录分组表 generatedGroup(iRouteGroup); //生成Root类 做用:记录<分组,对应的Group类> generatedRoot(iRouteRoot, iRouteGroup); } 

上面提到的生成的EaseRouter_Route_moduleName文件和EaseRouter_Group_moduleName文件分别实现了IRouteRoot和IRouteGroup接口,就是经过下面这两行代码拿到IRootGroup和IRootRoot的字节码信息,而后传入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,这两个方法内部会经过javapoet api生成java文件,并实现这两个接口。

TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);

generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成上面提到的EaseRouter_Root_app和EaseRouter_Group_main等文件的具体实现,生成的方法我在第一部分已经贴出来过了,这里再也不阐述。

好了,如今咱们编译下项目就会在每一个组件module的build/generated/source/apt目录下生成相关映射文件。这里我把app module编译后生成的文件贴出来,app module编译后会生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件对应于app module的分组,里面记录着本module下全部的分组信息,EaseRouter_Group_main、EaseRouter_Group_show文件分别记载着当前分组下的全部路由地址和ActivityClass映射信息。以下所示:

public class EaseRouter_Root_app implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("main", EaseRouter_Group_main.class); routes.put("show", EaseRouter_Group_show.class); } } public class EaseRouter_Group_main implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main","main")); atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main2","main")); } } public class EaseRouter_Group_show implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")); } } 

你们会看到生成的类分别实现了IRouteRoot和IRouteGroup接口,而且实现了loadInto()方法,而loadInto方法经过传入一个特定类型的map就能把分组信息放入map里,只要分组信息存入到特定的map里后,咱们就能够随意的从map里取路由地址对应的Activity.class作跳转使用。那么若是咱们在login_module中想启动app_module中的MainActivity类,首先,咱们已知MainActivity类的路由地址是"/main/main",第一个"/main"表明分组名,那么咱们岂不是能够像下面这样调用去获得MainActivity类文件,而后startActivity()跳转到MainActivity。(这里的RouteMeta只是存有Activity class文件的封装类,先不用理会)。

public void test() { EaseRouter_Root_app rootApp = new EaseRouter_Root_app(); HashMap<String, Class<? extends IRouteGroup>> rootMap = new HashMap<>(); rootApp.loadInto(rootMap); //获得/main分组 Class<? extends IRouteGroup> aClass = rootMap.get("main"); try { HashMap<String, RouteMeta> groupMap = new HashMap<>(); aClass.newInstance().loadInto(groupMap); //获得MainActivity RouteMeta main = groupMap.get("/main/main"); Class<?> mainActivityClass = main.getDestination(); Intent intent = new Intent(this, mainActivityClass); startActivity(intent); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } 

能够看到,只要有了这些附带路由映射信息的类文件,并将其保存的映射关系存入map里,咱们便能轻易的启动其余module的Activity了。

  • 第二节 路由框架的初始化

上节咱们已经经过apt生成了映射文件,而且知道了如何经过映射文件去调用Activity,然而咱们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务作跳转使用。那么在什么时机去拿到这些映射文件中的信息呢?首先咱们须要在上层业务作路由跳转以前把这些路由映射关系拿到手,但咱们不能事先预知上层业务会在何时作跳转,那么拿到这些路由关系最好的时机就是应用程序初始化的时候。

知道了在什么时机去拿到映射关系,接下来就要考虑如何拿了。咱们在上面已经介绍过实现IRouteRoot接口的全部类文件里保存着各个module的分组文件(分组文件就是实现了IRouteGroup接口的类文件),那么只要拿到全部实现IRouteGroup接口的类的集合,便能获得左右的路由信息了。下面看初始化的代码:

public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); EasyRouter.init(this); } } //咱们手动实现的路由框架,咱们就叫它EasyRouter public class EasyRouter { private static final String TAG = "EasyRouter"; private static final String ROUTE_ROOT_PAKCAGE = "com.xsm.easyrouter.routes"; private static final String SDK_NAME = "EaseRouter"; private static final String SEPARATOR = "_"; private static final String SUFFIX_ROOT = "Root"; private static EasyRouter sInstance; private static Application mContext; private Handler mHandler; private EasyRouter() { mHandler = new Handler(Looper.getMainLooper()); } public static EasyRouter getsInstance() { synchronized (EasyRouter.class) { if (sInstance == null) { sInstance = new EasyRouter(); } } return sInstance; } public static void init(Application application) { mContext = application; try { loadInfo(); } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "初始化失败!", e); } } //... } 

能够看到,进程启动的时候咱们调用EasyRouter.init()方法,init()方法中调用了loadInfo()方法,而这个loadInfo()即是咱们初始化的核心。我把loadInfo的代码贴出来:

private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //得到全部 apt生成的路由类的全类名 (路由表) Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); for (String className : routerMap) { if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { //root中注册的是分组信息 将分组信息加入仓库中 ((IRouteRoot) Class.forName(className).getConstructor().newInstance()).loadInto(Warehouse.groupsIndex); } } for (Map.Entry<String, Class<? extends IRouteGroup>> stringClassEntry : Warehouse.groupsIndex.entrySet()) { Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + "]"); } } 

咱们首先经过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)获得apt生成的全部实现IRouteRoot接口的类文件集合,经过上面的讲解咱们知道,拿到这些类文件即可以获得全部的路由地址和Activity映射关系。

这个ClassUtils.getFileNameByPackageName()方法就是具体的实现了,下面咱们看具体的代码:

/** * 获得路由表的类名 * @param context * @param packageName * @return * @throws PackageManager.NameNotFoundException * @throws InterruptedException */ public static Set<String> getFileNameByPackageName(Application context, final String packageName) throws PackageManager.NameNotFoundException, InterruptedException { final Set<String> classNames = new HashSet<>(); List<String> paths = getSourcePaths(context); //使用同步计数器判断均处理完成 final CountDownLatch countDownLatch = new CountDownLatch(paths.size()); ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size()); for (final String path : paths) { threadPoolExecutor.execute(new Runnable() { @Override public void run() { DexFile dexFile = null; try { //加载 apk中的dex 并遍历 得到全部包名为 {packageName} 的类 dexFile = new DexFile(path); Enumeration<String> dexEntries = dexFile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) { classNames.add(className); } } } catch (IOException e) { e.printStackTrace(); } finally { if (null != dexFile) { try { dexFile.close(); } catch (IOException e) { e.printStackTrace(); } } //释放一个 countDownLatch.countDown(); } } }); } //等待执行完成 countDownLatch.await(); return classNames; } 

这个方法会经过开启子线程,去扫描apk中全部的dex,遍历找到全部包名为packageName的类名,而后将类名再保存到classNames集合里。

List<String> paths = getSourcePaths(context)这句代码会得到全部的apk文件(instant run会产生不少split apk),这个方法的具体实现你们看demo便可,再也不阐述。这里用到了CountDownLatch类,会分path一个文件一个文件的检索,等到全部的类文件都找到后便会返回这个Set<String>集合。因此咱们能够知道,初始化时找到这些类文件会有必定的耗时,若是你已经看过ARouter的源码便会知道ARouter这里会有一些优化,只会遍历找一次类文件,找到以后就会保存起来,下次app进程启动会检索是否有保存这些文件,若是有就会直接调用保存后的数据去初始化。

  • 第三节 路由跳转实现

通过上节的介绍,咱们已经可以在进程初始化的时候拿到全部的路由信息,那么实现跳转便好作了。直接看代码:

@Route(path = "/main/main") public class MainActivity extends AppCompatActivity { public void startModule1MainActivity(View view) { EasyRouter.getsInstance().build("/module1/module1main").navigation(); } } 

在build的时候,传入要跳转的路由地址,build()方法会返回一个Postcard对象,咱们称之为跳卡。而后调用Postcard的navigation()方法完成跳转。用过ARouter的对这个跳卡都应该很熟悉吧!Postcard里面保存着跳转的信息。下面我把Postcard类的代码实现粘下来:

public class Postcard extends RouteMeta { private Bundle mBundle; private int flags = -1; //新版风格 private Bundle optionsCompat; //老版 private int enterAnim; private int exitAnim; public Postcard(String path, String group) { this(path, group, null); } public Postcard(String path, String group, Bundle bundle) { setPath(path); setGroup(group); this.mBundle = (null == bundle ? new Bundle() : bundle); } public Bundle getExtras() {return mBundle;} public int getEnterAnim() {return enterAnim;} public int getExitAnim() {return exitAnim;} /** * 跳转动画 * @param enterAnim * @param exitAnim * @return */ public Postcard withTransition(int enterAnim, int exitAnim) { this.enterAnim = enterAnim; this.exitAnim = exitAnim; return this; } /** * 转场动画 * @param compat * @return */ public Postcard withOptionsCompat(ActivityOptionsCompat compat) { if (null != compat) { this.optionsCompat = compat.toBundle(); } return this; } public Postcard withString(@Nullable String key, @Nullable String value) { mBundle.putString(key, value); return this; } public Postcard withBoolean(@Nullable String key, boolean value) { mBundle.putBoolean(key, value); return this; } public Postcard withInt(@Nullable String key, int value) { mBundle.putInt(key, value); return this; } //还有许多给intent中bundle设置值得方法我就不一一列出来了,能够看demo里全部的细节 public Bundle getOptionsBundle() { return optionsCompat; } public Object navigation() { return EasyRouter.getsInstance().navigation(null, this, -1, null); } public Object navigation(Context context) { return EasyRouter.getsInstance().navigation(context, this, -1, null); } public Object navigation(Context context, NavigationCallback callback) { return EasyRouter.getsInstance().navigation(context, this, -1, callback); } public Object navigation(Context context, int requestCode) { return EasyRouter.getsInstance().navigation(context, this, requestCode, null); } public Object navigation(Context context, int requestCode, NavigationCallback callback) { return EasyRouter.getsInstance().navigation(context, this, requestCode, callback); } } 

若是你是一个Android开发,Postcard类里面的东西就不用我再给你介绍了吧!(哈哈)我相信你一看就明白了。咱们只介绍一个方法navigation(),他有好几个重载方法,方法里面会调用EasyRouter类的navigation()方法。EaseRouter的navigation()方法,就是跳转的核心了。下面请看:

protected Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { try { prepareCard(postcard); }catch (NoRouteFoundException e) { e.printStackTrace(); //没找到 if (null != callback) { callback.onLost(postcard); } return null; } if (null != callback) { callback.onFound(postcard); } switch (postcard.getType()) { case ACTIVITY: final Context currentContext = null == context ? mContext : context; final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } mHandler.post(new Runnable() { @Override public void run() { //可能须要返回码 if (requestCode > 0) { ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle()); } else { ActivityCompat.startActivity(currentContext, intent, postcard .getOptionsBundle()); } if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { //老版本 ((Activity) currentContext).overridePendingTransition(postcard .getEnterAnim() , postcard.getExitAnim()); } //跳转完成 if (null != callback) { callback.onArrival(postcard); } } }); break; default: break; } return null; } 

这个方法里先去调用了prepareCard(postcard)方法,prepareCard(postcard)代码我贴出来,

private void prepareCard(Postcard card) { RouteMeta routeMeta = Warehouse.routes.get(card.getPath()); if (null == routeMeta) { Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup()); if (null == groupMeta) { throw new NoRouteFoundException("没找到对应路由:分组=" + card.getGroup() + " 路径=" + card.getPath()); } IRouteGroup iGroupInstance; try { iGroupInstance = groupMeta.getConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("路由分组映射表记录失败.", e); } iGroupInstance.loadInto(Warehouse.routes); //已经准备过了就能够移除了 (不会一直存在内存中) Warehouse.groupsIndex.remove(card.getGroup()); //再次进入 else prepareCard(card); } else { //类 要跳转的activity 或IService实现类 card.setDestination(routeMeta.getDestination()); card.setType(routeMeta.getType()); switch (routeMeta.getType()) { case ISERVICE: Class<?> destination = routeMeta.getDestination(); IService service = Warehouse.services.get(destination); if (null == service) { try { service = (IService) destination.getConstructor().newInstance(); Warehouse.services.put(destination, service); } catch (Exception e) { e.printStackTrace(); } } card.setService(service); break; default: break; } } } 

注意,Warehouse就是专门用来存放路由映射关系的类,里面保存着存路由信息的map,这在ARouter里面也是同样的。这段代码Warehouse.routes.get(card.getPath())经过path拿到对应的RouteMeta,这个RouteMeta里面保存了activityClass等信息。继续往下看,若是判断拿到的RouteMeta是空,说明这个路由地址尚未加载到map里面(初始化时为了节省性能,只会加载全部的分组信息,而每一个分组下的路由映射关系,会使用懒加载,在首次用到的时候去加载),只有在第一次用到当前路由地址的时候,会去Warehouse.routes里面拿routeMeta,若是拿到的是空,会根据当前路由地址的group拿到对应的分组,经过反射建立实例,而后调用实例的loadInfo方法,把它里面保存的映射信息添加到Warehouse.routes里面,而且再次调用prepareCard(card),这时再经过Warehouse.routes.get(card.getPath())就能够顺利拿到RouteMeta了。进入else{}里面,调用了card.setDestination(routeMeta.getDestination()),这个setDestination就是将RouteMeta里面保存的activityClass放入Postcard里面,下面switch代码块能够先不用看,这是实现ARouter中经过依赖注入实现Provider 服务的逻辑,有心研究的同窗能够去读一下demo。

好了,prepareCard()方法调用完成后,咱们的postcard里面就保存了activityClass,而后switch (postcard.getType()){}会判断postcard的type为ACTIVITY,而后经过ActivityCompat.startActivity启动Activity。到这里,路由跳转的实现已经讲解完毕了。

小结

EaseRouter自己只是参照ARouter手动实现的路由框架,而且剔除掉了不少东西,如过滤器等,若是想要用在项目里,建议仍是用ARouter更好,毕竟这只是个练手项目,功能也不够全面,固然有同窗想对demo扩展后使用那固然更好,遇到什么问题能够及时联系我。个人目的是经过本身手动实现路由框架来加深对知识的理解,如这里面涉及到的知识点apt、javapoet和组件化思路、编写框架的思路等。看到这里,若是感受干货不少,欢迎关注个人github,里面会有更多干货!

demo地址

仿ARouter一步步实现一个路由框架,点我访问源码,欢迎star

相关文章
相关标签/搜索