组件化架构深入剖析<二>-----组件通信原理剖析及实现注解处理器生成路由工具类
继续接着上一次https://www.cnblogs.com/webor2006/p/12275665.html的代码开撸, 不过在继续开撸之前,咱们先用图的方式对其实现组件之间通讯的原理进行一下整体的了解,这样有助于我们能清楚的知道接下来手写路由更加的明确。
组件路由通信原理剖析:
一说到组件化则就会说到路由,那为啥要有路由的存在呢?所以有必要将整个我们接下来要实现的一个路由的原理给梳理一下,对于组件化的APP,形态是这样:
其中各个library都集成到了主app当中了,并且各library之间是解耦的,而不是相互存有依赖的,假如现在有这么个需求:
回到咱们的DEMO来举例:
当然可以通过隐式意图的方式来进行调用,但是这种方式太麻烦了,每个Activity都要定义相应的Intent action之类的,所以有没有一种方式能用达到显示意图调用呢?答案就是利用路由,怎么个路由法呢?还记得咱们上一次中编写了注解么,此时可以组件中的Activity进行标记,如下:
然后此时会由我们的注解处理器来对这些进行处理,那如何处理呢?这里先提前说思路,正是有了思路我们在之后的撸码环节才能有的放矢,此时注解处理器会扫描每个module中带有Router的注解,那这样说是不是每一个module都需要声明annotationProcessor依赖了?答案是的!此时注解处理器会将path对应的真实的Activity形成一个对照关系,如下:
目前这些映射关系是每个Module有一份,最终使用时肯定是需要将其汇总到一个MAP集合当中,具体细节先不用管,总之接下来将其汇总一下,就为:
好,此时又有一个问题出来了,这个map汇总集合存哪呢?这里其实需要再用一个新的module来对这些数据进行承载,如:
这个目前我们还没有创建,先不管具体实现,主要是理清思路,有这个module之后,接下来的整个结构则会变为:
也就是最终map映射的汇总存放在了一个路由的核心模块,它里面则是整个路由的核心实现,那它里面的Map的数据是在何时进行汇总初始化呢?在我们的主APP中进行,这样由于其它所有的module都依赖于这个核心模块,那么此时就可以通过路由手段从一个module跳到另一个module了,调用的代码可以如下:
public void mainJump(View view) { MyRouter.getInstance().build("/app/module2").withString("a", "从Module1").navigation(this); }
上面则会通过路由核心代码中的map所有数据中来找到"/app/module2"所对应的Module2中的MainActivity了,最终来完成整个页面的跳转。注意:在前一句中我将“map所有数据”标红了,很明显这种在map中数据比较多的情况下有待优化的地方,因为实际商用项目要跳转的界面应该非常之多,所以必须要考虑到一个性能的问题,所以这里得想办法优化一下,优化的思路其实很简单,增加一个module分组既可,也就是最终的MAP是这样一个结构:
“<groupName, <routePath, xxxActivity>>”
回到刚才咱们的图例来说,其Map的形态就变为:
这样改造之后,则直接根据当前的module名称就可以定位到只属于它里面的路由表,数据量顺间就降低了,这样拿的效率绝对高。以上就是对于整个路由功能实现原理的一个粗略的分析,有了指导方针,接下来实现起来也会要顺畅很多。
手写Arouter路由核心功能:
javapoet认识:
从上面路由通信原理分析中可以发现,重点是要有一个Map映射数据,它里面是包含所有各模块注册处理器所扫描的映射关系,那么怎么能将所有模块的注解处理器中扫描到的关系都存放到一个总的Map当中呢?这里先将思路梳理一下:
比如Module1和Module2都有@Route注解,然后各自的注解处理器最终会在编译阶段能生成出这样一个工具类,如下:
public class MyRouter$$Root$$module1{ @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("module1", MyRouter$$Group$$module1.class); } }
而其中标红的为具体的路由表,如下:
public class MyRouter$$Group$$module1 { @Override public void loadInto(Map<String, Class<?>> atlas) { atlas.put("/app/module1", MainActivity.class); } }
最终在我们主APP中来初始化整个映射MAP时,是不是只要咱们定义好一个Map,然后通过反射去调用这些注解生成器自动生成的工具类loadInfo()方法,是不是我们要的Map的汇总信息就搞定了,当然要通过反射来统一处理肯定得要有一个抽象的行为,不然每个module生成的类名都是不一样的无法统一,所以咱们需要定义两个接口:
public interface IRouteRoot { void loadInto(Map<String, Class<? extends IRouteGroup>> routes); }
public interface IRouteGroup { void loadInto(Map<String, Class<?>> atlas); }
然后最终生成的类就会是如下:
public class MyRouter$$Root$$module1 implements IRouteRoot{ @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("module1", MyRouter$$Group$$module1.class); } }
public class MyRouter$$Group$$module1 implements IRouteGroup { @Override public void loadInto(Map<String, Class<?>> atlas) { atlas.put("/app/module1", MainActivity.class); } }
以上只是一个大致的思路,但是实际实现时肯定会有变化的,这个小节的标题不是要来认识"javapoet"么?那说了一堆这个实现思路跟它有啥关系呢?其实关系大着呢,因为要靠着注解处理器自动生成上面的代码,就需要借助于javapoet这个开源库,地址为:https://github.com/square/javapoet,如果不使用它的话,那么咱们想生成指定的代码会比较累,需要用字符串拼接的方式来进行,比如像这样:
所以接下来咱们先来了解一下javapoet:
然后官网上给了一个生成示例:
上面的使用其实很简单,稍加说明一下:
另外关于javapoet的一些误法这里罗列一下,有个大概的了解既可:
那咱们啥也不干,先照着官网的示例来生成一个HelloWorld类看一下效果,先引入一下javapoet的依赖:
然后将官方的DEMO代码拷贝到咱们工程中:
@AutoService(Processor.class) /** * 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数 * 声明我们注解支持的JDK的版本 */ @SupportedSourceVersion(SourceVersion.RELEASE_7) /** * 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数 * 声明我们要处理哪一些注解 该方法返回字符串的集合表示该处理器用于处理哪些注解 */ @SupportedAnnotationTypes({Constants.ANN_TYPE_ROUTE}) public class RouteProcessor extends AbstractProcessor { private Log log; private Filer filer; //初始化 @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); log = Log.newLog(processingEnvironment.getMessager()); log.i("RouteProcessor.init()"); filer = processingEnvironment.getFiler();//这里是用来生成文件用的 } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { log.i("RouteProcessor.process()"); MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); try { javaFile.writeTo(filer);//注意:这里输出要改一下,官网是输出到了控制台了,我们是需要生成一个Java文件 } catch (IOException e) { e.printStackTrace(); } return false; } }
构建看一下:
以上就是对于javapoet的一个简单入门,接下来则编写咱们自己的注解处理器的业务逻辑了。
通过注解处理器生成路由工具映射类:
接下来正式编写注解处理器的代码为我们生成那两个辅助类,先将上面的那个官方DEMO的代码删掉,主要是来处理它里面的process方法,它带有两个参数,先来了解一下:
接下来则需要找到Route这个注解,只有是它才会要处理:
其中Utils.java工具类的代码:
public class Utils { public static boolean isEmpty(CharSequence cs) { return cs == null || cs.length() == 0; } public static boolean isEmpty(Collection<?> coll) { return coll == null || coll.isEmpty(); } public static boolean isEmpty(final Map<?, ?> map) { return map == null || map.isEmpty(); } }
接下来则就需要遍历每个注解的信息进行进一步处理:
接着需要做一下容错判断,因为注解有可能可以添加到随便的哪个不是Activity类上,比如:
很显然上面的这种情况是不能跳转的,因为都不是一个Activity,所以需要对其进行检验:
那如何来判断注解所注的类的类型呢?这里又得要引入一个东东,如下:
而isSubType的函数原型如下:
所以此时需要获以Activity的TypeMirror信息才行,那如何获取呢?此时又得借助个工具,如下:
其中在Constants类中增加了一个配置常量:
接下来满足条件的注解则就需要将信息进行保存到JavaBean当中,先新建一个:
package com.android.router_annotation.model; import com.android.router_annotation.Route; import javax.lang.model.element.Element; public class RouteMeta { public enum Type { ACTIVITY, ISERVICE//这个是指的方法,而非Android里面的Service,未来还要实现各module之间方法的调用的,就属于这种类型 } private Type type; /** * 节点 (Activity) */ private Element element; /** * 注解使用的类对象 */ private Class<?> destination; /** * 路由地址 */ private String path; /** * 路由组 */ private String group; public static RouteMeta build(Type type, Class<?> destination, String path, String group) { return new RouteMeta(type, null, destination, path, group); } public RouteMeta() { } /** * Type * * @param route route * @param element element */ public RouteMeta(Type type, Route route, Element element) { this(type, element, null, route.path(), route.group()); } public RouteMeta(Type type, Element element, Class<?> destination, String path, String group) { this.type = type; this.destination = destination; this.element = element; this.path = path; this.group = group; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public Element getElement() { return element; } public RouteMeta setElement(Element element) { this.element = element; return this; } public Class<?> getDestination() { return destination; } public RouteMeta setDestination(Class<?> destination) { this.destination = destination; return this; } public String getPath() { return path; } public RouteMeta setPath(String path) { this.path = path; return this; } public String getGroup() { return group; } public RouteMeta setGroup(String group) { this.group = group; return this; } }
然后生成一下这个实例:
在开始进行解析成groupMap之前还得做个容错,因为有可能用户指定的path不是以“/”分隔的,有可能是以“.”或者其它不符合规定的字串来赋值的,所以有必要对其路径的正确性进行一个校验,如下:
好,校验通过的话,接下来则需要解析到Map当中,具体如下:
好,目前路由分组映射信息的收集代码已经写好了,接下来则就是要生成两个类了,具体这两个类的形态,上面已经说过了,这里再来贴一下:
如上面所示,需要有两个接口,这俩接口的目的在上面也表述过,其实是为了最终反射统一调用用的,这里需要将其放到路由的核心模块中【至于为啥要它在上面阐述过,目的是为了能汇总所有的路由MAP信息】,目前咱们还没有这个模块,所以先新建一下:
然后此时它需要依赖于router_annotation:
而原来直接依赖于router_annotation的module都可以改为依赖于router_core了,如下:
但是此时发现主app在改完依赖之后,发现找不到注解了:
这咋办呢?其实这里是gradle中的api与implementation的区别,这里需要将它改为api既可:
关于这个的区别在当时手写ButterKnife框架时就阐述过,这里不多说了,简单来说就是api支持传递依赖,而implementation不支持传递它只有直接依赖的那个module才能访问到被依赖的资源。继续将其它的moduel依赖也修改全:
组织架构调整好之后,咱们准备开始实现生成代码的逻辑:
由于实现这俩类时都得要实现各自的接口,所以其实得先将这俩接口的类型给生成出来,如下:
其中又多了两个常量:
好,接下来先来生成组映射类,其生成步骤跟javapoet的官网差不多,这里就不多解释了,如下:
也就是准备这块的信息:
接下来则需要遍历所有的MAP中映射路由信息进行一一创建:
此时又多一个常量:
接下来则需要生成方法中的代码块了,也就是这块东东:
只不过put的value不是简单的class了,而是一个做了封装的对象,如下:
具体生成如下:
方法体已经构建好了,接下来则构造类信息,最终生成文件,如下:
最后还有一步,由于在接下来生成组信息时,需要知道其具体组对应的路由表达的名称,如下:
所以这里在生成具体的路由表之后需要做个缓存,如下:
至此,整个路由映射表的文件生成代码就已经写完了,还是比较麻烦的,不过这些都是一些套路,也不用去记,接下来则需要生成分组的类信息了,也就是:
这个放下次再继续。