路由组件核心原理探究 - 手写一个ARouter

本篇博文基于自己实现的路由组件,主要功能包括Activity路由跳转,支持自定义服务。代码实现比较简单,重在探讨路由组件的核心原理,如需品尝功能更全、代码更屌的框架,可以直接前往ARouter,下载源码,即可。

手写一个ARouter;

一、核心原理

还是那句话,没什么是一张图解决不了的问题,如果有,那就是两张。O(∩_∩)O

路由组件核心原理探究 - 手写一个ARouter

路由组件的核心原理,如上图所示:

共分为两大部分:编译时 + 运行时。

a)编译时

1、路由信息收集

路由信息收集的前提是有数据源,而这个数据源就是我们自定义的编译时注解Route。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    //配置路由,格式:/xxx/yyy,必须以/开头
    String path();
}

当然,只是定义一个编译时注解是没用的,需要与之配合的编译时注解处理器来完成相应的处理工作。

使用:

@Route(path = "/main/mainactivity")
public class MainActivity extends AppCompatActivity {
......
}

我们这个所说的路由信息就是”/main/mainactivity“及相关信息,相关信息是指该路由对应的目标类,比如本例的MainActivity;还有目标类的类型,比如本例的是Activity,这样实现跳转时我们知道下一步的动作是startActivity了;携带的参数等等。

主要原理如下:

Java编译时注解框架会在编译时扫描所有的Java源文件,并将带有指定注解的所有元素Element作为参数,传递给我们的注解处理器的process方法进行相应的处理。

注解处理方法的第一步就是收集路由信息。路由这个词意味着分组,不然如何路由呢?因此,在收集路由信息的同时,我们按照一定的规则进行分组 - 比如/main/mainactivity,则属于main分组,同一分组下的所有路由信息就对应了一张路由表。也就是说有多少个分组,后面我们就会生成多少个路由表文件。示例:

/**
 * AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class app_Group_main implements IRouteGroup {
  public void loadRouteInfo(Map<String, RouteMeta> routes) {
    routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
    routes.put("/main/shopactivity",RouteMeta.build("/main/shopactivity","main",ShopActivity.class,TypeEnum.ACTIVITY));
  }
}

2、路由信息分组、生成路由表文件、路由入口文件

路由信息分组在第一部分已经说了,这里不再说了。

路由信息完成分组之后,下一步的动作就是生成路由表文件,路由表文件包含了同一分组下的所有路由信息,这是一个java文件,文件名可以按照一定的规则来进行命名,比如

app_Group_main,即 module名 + ”_“ + ”Group “+ ”_“ + 分组名。

一个大型的App,路由表文件的数量是非常多的。如果一开始就把所有的路由信息都加载到内存中,这对于本就内存不宽裕的手机设备来说并不是好的做法。比较优秀的做法是只加载用到的路由信息对应的路由表。这就需要借助路由入口文件实现懒加载了。路由入口文件示例如下:

/**
 * AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class JRouter_Entry_app implements IEntry {
  public void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries) {
    entries.put("test",app_Group_test.class);
    entries.put("main",app_Group_main.class);
  }
}

我们会在生成路由表文件的同时,生成路由入口文件,如上述代码所示。原因已经说过了,不再详述了。

b)运行时

3、运行时扫描dex文件,加载路由入口信息至内存

在初始化路由组件的时候,我们只会把路由入口文件中的所有信息加载到内存中。

4、运行时,根据路由信息,加载路由表至内存中

在进行具体的路由跳转时,我们才会去加载相应的路由表。比如MainActivity,我们首先会查找路由入口信息,根据分组main找到了对应的路由表文件是app_Group_main.class,则利用反射将app_Group_main的所有路由信息加载到内存中。

具体涉及到运行时扫描dex文件,在后面的源码分析中会详述。

小结:

a)路由生成:收集路由信息  ->  对路由信息分组  ->  每个路由分组生成一个路由表文件  ->  生成路由入口文件;

b)路由跳转:

  • [依据分组名]查找路由入口:在路由入口文件查找对应的路由表文件是哪个;
  • 加载路由表文件;
  • 根据路由path信息,在路由表文件中查找具体的路由信息;
  • 拿到足够的信息,进行路由跳转。   

二、手写一个ARouter,探究路由实现核心原理

本部分同样按照博客开始的图示,分四个部分实现。

2.1、路由信息收集 & 分组

a)编译时注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    //配置路由,格式:/xxx/yyy,必须以/开头
    String path();
}

注意:Retention必须是CLASS。Target是TYPE。

b)路由信息提取,分组

    private Map<String, Set<RouteMeta>> group;//路由分组

我们定义了一个Map用于保存路由分组信息。每一个RouteMeta就是一条路由信息。同一分组的所有路由信息会保存在同一个Set<RouteMeta>中。在生成路由表文件的时候,一个Set<RouteMeta>就是一个路由表文件。

提取 & 分组:

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
.....
Set<? extends Element> routeElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
        if (routeElements == null || routeElements.isEmpty()) {
            log.printMessage(Diagnostic.Kind.NOTE, "process roundEnvironment is empty");
            return false;
        }
extractAndGroupRouteInfo(routeElements);    //提取&分组路由信息
.....
}

  //提取&分组路由信息
    private void extractAndGroupRouteInfo(Set<? extends Element> routeElements) {
        log.printMessage(Diagnostic.Kind.NOTE, "start to classify route info");
        for (Element ele : routeElements) { //遍历&路由分组
            TypeMirror annoEle = ele.asType();
            if (ele.getKind() == ElementKind.CLASS) {// Route注解只能使用在Class类上,如interface等类型不可使用
                Route route = ele.getAnnotation(Route.class);
                String path = route.path();//获取路由
                String groupName = extractGroupFromPath(path);//提取分组名
                RouteMeta routeMeta = new RouteMeta(path, groupName, ele, getElementKind(annoEle));//封装路由信息
                Set<RouteMeta> groupRoutes = group.get(groupName);
                if (groupRoutes == null) {
                    groupRoutes = new HashSet<>();
                    groupRoutes.add(routeMeta);
                    group.put(groupName, groupRoutes); // 路由分组
                } else {
                    groupRoutes.add(routeMeta);
                }
            }
        }
        log.printMessage(Diagnostic.Kind.NOTE, "classify route info  -- finish -- group size is " + group.size());
        log.printMessage(Diagnostic.Kind.NOTE, "start to generate route group file");
    }

前面我们说过:Java编译时注解框架会在编译时扫描所有的Java源文件,并将带有指定注解的所有元素Element作为参数,传递给我们的注解处理器的process方法进行相应的处理。

因此,RoundEnvironment 包含了所有带有Route注解的所有元素。所以,通过遍历roundEnvironment.getElementsAnnotatedWith(Route.class)所返回的集合,我们就可以轻松的完成路由表信息的提取工作。提取的信息包括:路由path,分组groupName,目标类信息,目标类的类型。

  • 路由path:通过Element的getAnnotation(Route.class);我们就可以拿到注解信息,从而获取用户填写的路由信息path - route.path();//获取路由
  • 分组groupName:拿到path之后,我们就可以根据一定的规则提取分组名。
   // 从路由中提取分组名
    private String extractGroupFromPath(String path) {
        if (TextUtil.isEmpty(path) || !path.startsWith("/")) {
            return null;
        }
        // 截取group
        String groupName = path.substring(1, path.indexOf("/", 1));
        return TextUtil.isEmpty(groupName) ? "" : groupName;
    }
  • 目标类信息:由于Element对应的就是Route注解的元素,因此包含了目标类的信息。
  • 目标类的类型:
 // 获取被注解的元素的类型
    private TypeEnum getElementKind(TypeMirror typeMirror) {
        if (types.isSubtype(typeMirror, typeMirror_Activity)) {
            return TypeEnum.ACTIVITY;
        } else if (types.isSubtype(typeMirror, typeMirror_Fragment)) {
            return TypeEnum.FRAGMENT;
        } else if (types.isSubtype(typeMirror, typeMirror_Fragment_V4)) {
            return TypeEnum.FRAGMENTV4;
        }
        if (types.isSubtype(typeMirror, typeMirror_Service)) {
            return TypeEnum.ISERVICE;
        }
        return TypeEnum.DEFAULT;
    }

保存目标类的类型的原因是可以指导我们后续的动作,比如如果是一个Activity类型,那么对应的动作就是startActivity了。

2.2、生成路由表文件

    //生成路由表文件
    private void generateRouteGroupFile() {
        //1、生成参数类型:Map<String, RouteMeta>
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ClassName.get(RouteMeta.class)
        );

        //2、生成参数:routes
        ParameterSpec parameterSpec = ParameterSpec.builder(parameterizedTypeName,
                "routes")
                .build();

        //3、遍历路由分组(Map<String, Set<RouteMeta>> group),生成路由表文件
        // String是分组名,每一个Set<RouteMeta>都是一个路由表文件
        //------------------------------------------------------------
        //public class app_Group_main implements IRouteGroup {
        //  public void loadRouteInfo(Map<String, RouteMeta> routes) {
        //    routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
        //  }
        //}
        for (Map.Entry<String, Set<RouteMeta>> routeEntry : group.entrySet()) {
            String groupName = routeEntry.getKey();
            MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(Const.LOAD_ROUTE_INFO)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (RouteMeta routeMeta : routeEntry.getValue()) {//遍历&添加统一分组下的路由信息
                // routes.put("/main/mainactivity", RouteMeta.build(path, group, destination, rawType, type))
                methodSpec.addStatement("routes.put($S,$T.build($S,$S,$T.class,$T." + routeMeta.getType() + "))",
                        routeMeta.getPath(),
                        ClassName.get(RouteMeta.class),
                        routeMeta.getPath(),
                        routeMeta.getGroup(),
                        ClassName.get(routeMeta.getRawType().asType()),
                        ClassName.get(TypeEnum.class));
            }
            //路由文件名
            String groupFileName = moduleName + "_Group_" + groupName;
            TypeSpec typeSpec = TypeSpec.classBuilder(groupFileName)
                    .addSuperinterface(ClassName.get(Const.TEMPLATE, Const.IROUTEGROUP))
                    .addModifiers(Modifier.PUBLIC)
                    .addJavadoc(Const.WARN)
                    .addMethod(methodSpec.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(Const.ROUTE_FILE_DIRECTORY_NAME, typeSpec).build();
            try {
                javaFile.writeTo(filer);
            } catch (Exception e) {
                log.printMessage(Diagnostic.Kind.ERROR, "generate route group file exception");
            }

            if (entries == null) {
                entries = new HashMap<>();
            }
            entries.put(groupName, groupFileName);//保存生成的路由文件信息,作为之后的查找入口
        }
    }

上述代码是使用JavaPoet完成Java源文件的生成的。因此,不熟悉的需要先了解JavaPoet。上述代码的注释很清楚,不再详述了。

生成的路由表如下所示:

/**
 * AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class app_Group_main implements IRouteGroup {
  public void loadRouteInfo(Map<String, RouteMeta> routes) {
    routes.put("/main/mainactivity",RouteMeta.build("/main/mainactivity","main",MainActivity.class,TypeEnum.ACTIVITY));
  }
}

2.3、生成入口文件

在生成路由表文件的同时,我们会保存对应的分组与路由表文件的关系至Map中。

 entries.put(groupName, groupFileName);//保存生成的路由文件信息,作为之后的查找入口

依据这个Map就可以生成路由的入口文件了。

    //生成路由入口文件
    private void generateRouteEntryFile() {
        //4、生成路由入口文件:void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries);
        //4.1、生成参数类型
        ParameterizedTypeName parameterizedTypeName1 = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ParameterizedTypeName.get(
                        ClassName.get(Class.class),
                        WildcardTypeName.subtypeOf(ClassName.get(Const.TEMPLATE, Const.IROUTEGROUP))
                )
        );
        //4.2、生成参数
        ParameterSpec parameterSpec1 = ParameterSpec.builder(parameterizedTypeName1, "entries").build();

        //4.3、生成方法
        MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(Const.LOADROUTEENTRY)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(parameterSpec1);

        for (Map.Entry<String, String> entry : entries.entrySet()) {
            //entries.put("main", JRouterTest_Group_main.class);
            methodSpec.addStatement("entries.put($S,$T.class)",
                    entry.getKey(),
                    ClassName.get(Const.ROUTE_FILE_DIRECTORY_NAME, entry.getValue()));
        }
        // 4.5、生成入口文件

        TypeSpec typeSpec = TypeSpec.classBuilder(Const.JROUTER + Const.SEPELATOR + Const.ENTRY + Const.SEPELATOR + moduleName)
                .addMethod(methodSpec.build())
                .addModifiers(Modifier.PUBLIC)
                .addJavadoc(Const.WARN)
                .addSuperinterface(ClassName.get(Const.TEMPLATE, Const.IENTRY))
                .build();

        JavaFile javaFile = JavaFile.builder(Const.ROUTE_FILE_DIRECTORY_NAME, typeSpec)
                .build();

        try {
            javaFile.writeTo(filer);
        } catch (Exception e) {
            log.printMessage(Diagnostic.Kind.ERROR, "generate route entry file exception");
            e.printStackTrace();
        }
    }

入口文件如下所示:

/**
 * AUTO GENERATED, MODIFY IS USELESS @WRITE BY JSON */
public class JRouter_Entry_app implements IEntry {
  public void loadRouteEntry(Map<String, Class<? extends IRouteGroup>> entries) {
    entries.put("test",app_Group_test.class);
    entries.put("main",app_Group_main.class);
  }
}

2.4、运行时扫描dex,加载路由入口信息至内存中

    //加载路由表入口文件至内存
    private static void loadIntoEntries(Application context) {
        try {
//获取所有dex中指定包名下的所有路由文件信息
            Set<String> routeFiles = ClassUtils.getFileNameByPackageName(context, Const.ROUTE_FILE_DIRECTORY_NAME);
            if (routeFiles != null && routeFiles.size() > 0) {
                for (String fileName : routeFiles) {
                    if (fileName.startsWith(Const.ROUTE_FILE_DIRECTORY_NAME + "." + Const.JROUTER + Const.SEPELATOR + Const.ENTRY + Const.SEPELATOR)) {
//只加载路由入口文件至内存中
                        ((IEntry) Class.forName(fileName).newInstance()).loadRouteEntry(wareHouse.entries);
                    }
                }
            }
        } catch (Exception e) {
            Log.e("TAG", "loadIntoEntries fail");
        }
    }

核心代码:运行时扫描dex文件

 ClassUtils.getFileNameByPackageName(context, Const.ROUTE_FILE_DIRECTORY_NAME);

可以拿到指定包名下的所有文件的全路径className。比如我们生成的路由表及路由入口文件都在com.jrouter.routes下,因此可以在运行时扫描dex文件时,得到所有的路由入口文件的全路径,然后利用反射将路由入口的信息全部加载到内存中。

详细实现如下:

   /**
     * 通过指定包名,扫描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
        final Set<String> classNames = new HashSet<>();

        List<String> paths = getSourcePaths(context);//所有的dex文件的路径
        final CountDownLatch parserCtl = new CountDownLatch(paths.size());

        for (final String path : paths) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexfile = null;

                    try {//加载所有的dex文件
                        if (path.endsWith(EXTRACTED_SUFFIX)) {
                            //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                            dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                        } else {
                            dexfile = new DexFile(path);
                        }

                        Enumeration<String> dexEntries = dexfile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (className.startsWith(packageName)) {
                            //获取指定包名下的所有文件的className
                                classNames.add(className);
                            }
                        }
                    } catch (Throwable ignore) {
                        Log.e("ARouter", "Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null != dexfile) {
                            try {
                                dexfile.close();
                            } catch (Throwable ignore) {
                            }
                        }

                        parserCtl.countDown();
                    }
                }
            });
        }

        parserCtl.await();

        return classNames;
    }

CountDownLatch  +  DefaultPoolExecutor开启多线程,扫描所有dex指定包名下的所有文件,拿到路径信息。

2.5、加载路由表信息 & 路由跳转

a)加载路由表信息至内存

    private PostCard.Builder preparePostCard() {
//wareHouse.entries - 保存入口信息

//wareHouse.routes - 保存路由表信息

        Class<? extends IRouteGroup> entryClass = wareHouse.entries.get(routeParamBuilder.getGroup());
        if (entryClass != null) {
//加载特定路由的路由表信息至内存中
            try {
                ((IRouteGroup) Class.forName(entryClass.getName()).newInstance()).loadRouteInfo(wareHouse.routes);
                wareHouse.entries.remove(routeParamBuilder.getGroup());
            } catch (Exception e) {
                e.printStackTrace();
                return routeParamBuilder;
            }
            preparePostCard();
        } else {
// 构建具体的路由跳转信息
            RouteMeta routeMeta = wareHouse.routes.get(routeParamBuilder.getPath());
            if (routeMeta != null) {
                routeParamBuilder.setDestination(routeMeta.getDestination());
                routeParamBuilder.setType(routeMeta.getType());
                switch (routeMeta.getType()) {
// 如果是Service,则只需返回Service类即可。只有Activity后面需要跳转。
                    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();
                            }
                        }
                        routeParamBuilder.setService(service);
                        break;
                }
            }
            return routeParamBuilder;
        }
        return routeParamBuilder;
    }

b)路由跳转

//Service,则只需返回Service类即可。只有Activity后面需要跳转。    
public Object navigation(Activity activity) {
        if (activity != null) {
            switch (type) {
                case ACTIVITY:
                    Intent intent = new Intent(activity, destination);
                    activity.startActivity(intent);
                    break;
                case ISERVICE:
                    return service;
            }
        }
        return null;
    }