ARouter原理剖析及手动实现

简介

    最近可能入了魔怔,也多是闲的蛋疼,本身私下学习了ARouter的原理以及一些APT的知识,为了加深对技术的理解,同时也本着热爱开源的精神为你们提供分享,因此就带着你们强行撸码,分析下ARouter路由原理和Android中APT的使用吧
    本篇文章我会带着你们一步步手动实现路由框架来理解相似ARouter的路由框架原理,撸码的demo我会附在文末。本路由框架就叫EaseRouter。(注:demo里搭建了组件化开发,组件化和路由自己并无什么联系,可是两个单向依赖的组件之间须要互相启动对方的Activity,由于没有相互引用,startActivity()是实现不了的,必须须要一个协定的通讯的方式,此时相似ARouter和ActivityRouter的框架就派上用场了)。java

涉及知识点

  • Router框架原理
  • apt、javapoet知识
  • Router框架实现
第一节:组件化原理

    本文的重点是对路由框架的实现进行介绍,因此对于组件化的基本知识在文中不会过多阐述,若有同窗对组件化有不理解,能够参考网上众多的博客等介绍,而后再阅读demo中的组件化配置进行熟悉,这里附上github demo地址:ARouter原理剖析及手动实现,点我访问源码,欢迎star

git

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

    在组件化中,咱们一般都会在base_module上层再依赖一个router_module,而这个router_module就是负责各个模块之间服务暴露和页面跳转的。
    用过ARouter路由框架的同窗应该都知道,在每一个须要对其余module提供调用的Activity中,都会声明相似下面@Route注解,咱们称之为路由地址github

@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文件一一对应保存,如直接保存在map中。为了让你们理解,我这里来使用近乎伪代码给你们简单演示一下。api

//项目编译后经过apt生成以下方法
public HashMap<String, ClassBean> routeInfo() {
    HashMap<String, ClassBean> route = new HashMap<String, ClassBean>();
    route.put("/main/main", MainActivity.class);
    route.put("/module1/module1main", Module1MainActivity.class);
    route.put("/login/login", LoginActivity.class);
}
复制代码

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

//不一样模块之间启动Activity
public void login(String name, String password) {
    HashMap<String, ClassBean> route = routeInfo();
    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应该是下面这个写法app

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

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

第二节:Route注解的做用

    简单讲,要经过apt生成咱们的路由表,首先第一步须要定义注解ide

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    /**
     * 路由的路径
     * @return
     */
    String path();

    /**
     * 将路由节点进行分组,能够实现动态加载
     * @return
     */
    String group() default "";

}
复制代码

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

@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 {}

复制代码

在项目编译的时候,咱们将会经过apt生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件对应于app module,里面记录着本module下全部的分组信息,EaseRouter_Group_main、EaseRouter_Group_show文件分别记载着当前分组的全部路由地址和ActivityClass映射信息。
本demo在编译的时候会生成类以下所示,先不要管这些类是怎么生成的,仔细看类的内容工具

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里。这两个接口是干吗的咱们先搁置,继续往下看
若是咱们在login_module中想启动app_module中的MainActivity类,首先,咱们已知MainActivity类的路由地址是"/main/main",第一个"/main"表明分组名,那么咱们岂不是能够像下面这样调用去获得MainActivity类文件,而后startActivity。这里的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();
    }

}
复制代码

能够看到,只要有了这些实现了IRouteRoot和IRouteGroup的类文件,咱们便能轻易的启动其余module的Activity了。这些类文件,咱们能够约定好以后,在代码的编写过程当中本身动手实现,也能够经过apt生成。做为一个框架,固然是自动解析Route注解而后生成这些类文件更好了。那么就看下节,如何去生成这些文件。

第三节:apt和javapoet详解

    经过上节咱们知道在Activity类上加上@Route注解以后,即可经过apt来生成对应的路由表,那么这节咱们就来说述一下如何经过apt来生成路由表。这节我会拿着demo里面的代码来跟你们详细介绍,咱们先来了解一下apt吧!
    APT是Annotation Processing Tool的简称,即注解处理工具。它是在编译期对代码中指定的注解进行解析,而后作一些其余处理(如经过javapoet生成新的Java文件)。咱们经常使用的ButterKnife,其原理就是经过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,而后生成XXX_ViewBinding类,实现了view的绑定。

    第一步:定义注解处理器,用来在编译期扫描加入@Route注解的类,而后作处理。
这也是apt最核心的一步,新建RouterProcessor 继承自 AbstractProcessor,而后实现process方法。在项目编译期会执行RouterProcessor的process()方法,咱们即可以在这个方法里处理Route注解了。此时咱们须要为RouterProcessor指明它须要处理什么注解,这里引入一个google开源的自动注册工具AutoService,以下依赖(也能够手动进行注册,不过略微麻烦):

implementation 'com.google.auto.service:auto-service:1.0-rc2'
复制代码

这个工具能够经过添加注解来为RouterProcessor指定它须要的配置(固然也能够本身手动去配置,不过会有点麻烦),以下所示

@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {

  //...
}
复制代码

完整的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下存放路由信息的类文件名。在这以前,咱们须要在module的gradle下配置以下

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
复制代码

Constant.ARGUMENTS_NAME即是每一个module的名字。

@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文件。这里在process()里生成文件用javapoet,这是squareup公司开源的一个库,经过调用它的api,能够很方便的生成java文件,在含有注解处理器(demo中apt相关的代码实现都在easy-compiler module中)的module中引入依赖以下:

implementation 'com.squareup:javapoet:1.7.0'
复制代码

好了,咱们终于能够生成文件了,在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);
}

复制代码

上节中提到过生成的root文件和group文件分别实现了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等文件的具体实现,代码太多,我粘出一个实现供你们参考,其实生成java文件的思路都是同样的,咱们只须要熟悉javapoet的api如何使用便可。你们能够后续在demo里详细分析,这里我只是讲解核心的实现。

/**
 * 生成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 {
        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();
    }
}

复制代码

能够看到,ParameterizedTypeName是建立参数类型的api,ParameterSpec是建立参数的实现,MethodSpec是函数的生成实现等等。最后,当参数、方法、类信息都准备好了以后,调用JavaFileapi生成类文件。JavaFile的builder ()方法传入了PACKAGE_OF_GENERATE_FILE变量,这个就是指定生成的类文件的目录,方便咱们在app进程启动的时候去遍历拿到这些类文件。

第四节 实现路由框架的初始化

    经过前几节的讲解,咱们知道了看似很复杂的路由框架,其实原理很简单,咱们能够理解为一个map(实际上是两个map,一个保存group列表,一个保存group下的路由地址和activityClass关系)保存了路由地址和ActivityClass的映射关系,而后经过map.get("router address") 拿到AncivityClass,经过startActivity()调用就行了。但一个框架的设计要考虑的事情远远没有这么简单。下面咱们就来分析一下:

    要实现这么一个路由框架,首先咱们须要在用户使用路由跳转以前把这些路由映射关系拿到手,拿到这些路由关系最好的时机就是应用程序初始化的时候,前面的讲解中我贴过几行代码,是经过apt生成的路由映射关系文件,为了方便你们理解,我把这些文件从新粘贴到下面代码中(这几个类都是单独的文件,在项目编译后会在各个模块的/build/generated/source/apt文件夹下面生成,为了演示方便我只贴出来了app模块下生成的类,其余模块如module一、module2下面的类跟app下面的没有什么区别),在程序启动的时候扫描这些生成的类文件,而后获取到映射关系信息,保存起来。

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接口的类都是保存了group分组映射信息,实现了IRouteGroup接口的类都保存了单个分组下的路由映射信息。只要咱们获得实现IRouteRoot接口的全部类文件,便能经过循环调用它的loadInfo()方法获得全部实现IRouteGroup接口的类,而全部实现IRouteGroup接口的类里面保存了项目的全部路由信息。IRouteGroup的loadInfo()方法,经过传入一个map,便会将这个分组里的映射信息存入map里。能够看到map里的value是“RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")”,RouteMeta.build()会返回RouteMeta,RouteMeta里面便保存着ActivityClass的全部信息。那么咱们这个框架,就有了第一个功能需求,即是在app进程启动的时候进行框架的初始化(或者在你开始用路由跳转以前进行初始化均可以),在初始化中拿到映射关系信息,保存在map里,以便程序运行中能够快速找到路由映射信息实现跳转。下面看具体的初始化代码。
注:这里咱们只讲解大致的思路,不会细致到讲解每个方法每一行代码的具体做用,跟着个人思路你会明白框架设计的具体细节,每一步要实现的功能是什么,可是精确到方法和每一行代码的具体含义你还须要仔细研读demo。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        EasyRouter.init(this);
    }
}

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);
       }
   }

   //...
}

复制代码

能够看到,init()方法中调用了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接口的类文件集合,经过上面的讲解咱们知道,拿到这些类文件即可以获得全部的routerAddress---activityClass映射关系。
这个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 paths = getSourcePaths(context)这句代码会得到全部的apk文件(instant run会产生不少split apk),这个方法的具体实现你们kandemo便可,再也不阐述。这里用到了CountDownLatch类,会分path一个文件一个文件的检索,等到全部的类文件都找到后便会返回这个Set集合。因此咱们能够知道,初始化时找到这些类文件会有必定的耗时,因此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;

    //服务
    private IService service;

    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;}

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

    /**
     * Intent.FLAG_ACTIVITY**
     * @param flag
     * @return
     */
    public Postcard withFlags(int flag) {
        this.flags = flag;
        return this;
    }

    public int getFlags() {
        return flags;
    }

    /**
     * 跳转动画
     *
     * @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 withShort(@Nullable String key, short value) {
        mBundle.putShort(key, value);
        return this;
    }


    public Postcard withInt(@Nullable String key, int value) {
        mBundle.putInt(key, value);
        return this;
    }


    public Postcard withLong(@Nullable String key, long value) {
        mBundle.putLong(key, value);
        return this;
    }


    public Postcard withDouble(@Nullable String key, double value) {
        mBundle.putDouble(key, value);
        return this;
    }


    public Postcard withByte(@Nullable String key, byte value) {
        mBundle.putByte(key, value);
        return this;
    }


    public Postcard withChar(@Nullable String key, char value) {
        mBundle.putChar(key, value);
        return this;
    }


    public Postcard withFloat(@Nullable String key, float value) {
        mBundle.putFloat(key, value);
        return this;
    }


    public Postcard withParcelable(@Nullable String key, @Nullable Parcelable value) {
        mBundle.putParcelable(key, value);
        return this;
    }


    public Postcard withStringArray(@Nullable String key, @Nullable String[] value) {
        mBundle.putStringArray(key, value);
        return this;
    }


    public Postcard withBooleanArray(@Nullable String key, boolean[] value) {
        mBundle.putBooleanArray(key, value);
        return this;
    }


    public Postcard withShortArray(@Nullable String key, short[] value) {
        mBundle.putShortArray(key, value);
        return this;
    }


    public Postcard withIntArray(@Nullable String key, int[] value) {
        mBundle.putIntArray(key, value);
        return this;
    }


    public Postcard withLongArray(@Nullable String key, long[] value) {
        mBundle.putLongArray(key, value);
        return this;
    }


    public Postcard withDoubleArray(@Nullable String key, double[] value) {
        mBundle.putDoubleArray(key, value);
        return this;
    }


    public Postcard withByteArray(@Nullable String key, byte[] value) {
        mBundle.putByteArray(key, value);
        return this;
    }


    public Postcard withCharArray(@Nullable String key, char[] value) {
        mBundle.putCharArray(key, value);
        return this;
    }


    public Postcard withFloatArray(@Nullable String key, float[] value) {
        mBundle.putFloatArray(key, value);
        return this;
    }


    public Postcard withParcelableArray(@Nullable String key, @Nullable Parcelable[] value) {
        mBundle.putParcelableArray(key, value);
        return this;
    }

    public Postcard withParcelableArrayList(@Nullable String key, @Nullable ArrayList<? extends
            Parcelable> value) {
        mBundle.putParcelableArrayList(key, value);
        return this;
    }

    public Postcard withIntegerArrayList(@Nullable String key, @Nullable ArrayList<Integer> value) {
        mBundle.putIntegerArrayList(key, value);
        return this;
    }

    public Postcard withStringArrayList(@Nullable String key, @Nullable ArrayList<String> value) {
        mBundle.putStringArrayList(key, value);
        return this;
    }

    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;
        case ISERVICE:
            return postcard.getService();
        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就是专门用来存放路由映射关系的类,这在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和组件化思路、编写框架的思路等。看到这里,若是感受干货不少,欢迎点个star或分享给更多人。

demo地址

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

联系方式

email:xiasem@163.com

相关文章
相关标签/搜索