美文网首页安卓开发博客
我能用注解处理器APT做什么 - 手写一个路由框架

我能用注解处理器APT做什么 - 手写一个路由框架

作者: 小风风吖 | 来源:发表于2021-11-24 17:02 被阅读0次

    引言

    一般来说,我们在项目开发中,功能性类似的同一层级,会有许多相同逻辑。很多时候,一个简单有效的方法,就是定义base类,比如我们已经司空见惯以至于写习惯了的BaseActivity、BaseFragment、BasePresenter等,然后在子类对应的流程节点中,对super进行调用。
    而在父类中实现的这些逻辑,被设计之初就是对应的某些特定的切面(Aspect),可以称作AOP思想的一种体现。

    但在某些情况下,很多逻辑并不适合放在Activity基类中,在无法多继承的限制下,就需要通过代理或包装的方式来实现解耦,例如androidx中的LifeCycle相关框架。

    同样的,面向对象的思想中,组合也是优于继承的。
    特别是对于一些可以高度抽象、代码高度一致的情况,为了避免大量的重复性代码,可以通过APT或ASM进行代码的自动生成,或者对编译完的class进行字节码操作插桩。今天主要来聊一聊APT的使用。


    APT 是什么

    APT 全称 Annotation Processing Tool,即注解处理器,APT 工具是用于注解处理的命令行程序,它可以找到源码中对应注解的对象(类、方法、字段)并使用注解处理器对其进行处理。
    进而根据需求在编译期生成一些附加的代码,然后与源码共同进行编译。最后打包到一起。

    注解处理器是基于注解(Annotation)的,需要定义一些注解,对应到想要处理的对象。例如ButterKnife对字段的注入,需要定义字段上的注解;例如ARouter对页面的关联,需要定义类上的注解。
    然后利用 javax.annotation.processing.Processor 对代码中带有对应注解的部分进行扫描,拿到注解下的类信息、字段信息、方法信息,按想要的逻辑进行自动编码,生成供后期调用的逻辑。


    路由框架要解决什么问题

    在单项目单模块的开发场景中,activity之间的相互跳转没有任何障碍,彼此间的代码都是可见的,如果需要统一跳转逻辑,方便review路由线路,也只需要一个工具类,把页面引用、需要的参数进行简单封装而已。

    但在模块化的开发中,模块间没有互相依赖,无法感知其他模块中的具体类型,就无法拿到构建intent需要的Activity.class。

    当然也可以定义公用的字符串常量,使用反射进行class的加载。但反射的性能较差,固定的少量使用还好,但随着项目增大,页面数量上涨,页面间数据交互复杂化,过多的反射使用就可能成为性能瓶颈。
    而且,既然都要对页面路由进行封装了,难道就不搞个传递数据的自动注入?还用getExtras、putExtras一个一个的赋值?不会吧不会吧?

    好,现在我们有了初步的预期,想要一个可以跨模块的,可以根据字符串或数字对应页面并实现跳转的,可以传递各种参数、而且可以在目标页面中自动注入参数的脚手架!

    这篇文章,我们就一起来实现一个自己的路由框架。
    本文很多地方参考了ARouter的实现,并做了许多简化,重在说明APT的使用和路由框架的思想。


    先来画几个图,清晰一下总流程

    自己画的-1.jpg

    如上图,模块化的项目中,主要业务功能集中在几个功能模块里,能够引用的逻辑只有自身包含的,和基础功能集中所包含的。而在APP模块中,可以拿到所有模块的引用
    所以,我们可以在APP中,在启动时,将各个模块中想要暴露的XXX.class收集起来,放到基础功能中某个地方中存起来,然后在功能模块里,就可以使用约定好的方式,通过某个数字或字符串标签,路由到对应页面中了。


    自己画的凑合看吧-2.png

    如上图,这个流程用文字来描述就是:

    • 你要有一个路由器框架🤣🤣🤣
    • 在每个功能模块中,准备好自己包含的路由信息(有几个页面可以被别人调,每个页面对应什么标签)。
    • app启动时,收集所有功能模块中暴露出的路由信息,写到公用路由器的路由表中。
    • 功能中需要跳转时,调用路由器提供的方法,由路由器查询路由表,并执行跳转。

    好!现在就能写一个路由框架啦!

    是的,根据上面的分析,现在真的能写出路由框架了,其实比想象的还要简单。

    只需要定义这样两个类,就算是完事了,请看下面的代码:

    • Router 单例路由器,包含路由表,提供跳转方法。
    • MetaProvider 元信息提供者接口,需要功能模块中提供实现,供app调用收集。

    路由器

    /** 没错这就是路由器本器 */
    class Router private constructor() {
        companion object {
            val instance = Router()
        }
    
        // 路由表,存着支持跳转的class信息
        private val metaMap = hashMapOf<String, Class<*>>()
    
        // 使用这个方法注入元信息,可多次使用
        fun injectMeta(provider: MetaProvider) {
            metaMap.putAll( provider.getMetaHere() )
        }
    
        fun jumpTo(context: Context, tag: String) {
            if(null == metaMap[tag]) {
                // 找不到,就打个日志好了
            } else {
                context.startActivity(Intent(context, metaMap[tag]))
            }
        }
    }
    

    元信息提供者

    interface MetaProvider {
        fun getMetaHere(): Map<String, Class<*>>
    }
    

    模块中的具体实现

    A 模块中有这么一个实现类,其他模块也是类似
    class A_MetaProvider : MetaProvider {
        // 在模块中写这个,当然可以使用自己包含的页面引用
        override fun getMetaHere(): Map<String, Class<*>> {
            return mapOf(
                "A-Activity111" to Activity111::class.java,
                "A-Activity222" to Activity222::class.java,
                "A-Activity333" to Activity333::class.java,
            )
        }
    }
    

    就这,已经可以用辣

    class SimpleApplication: Application() {
        // 分别调用几个模块中的提供者,把所有模块提供的元信息都塞到路由表里面
        override fun onCreate() {
            super.onCreate()
            Router.instance.injectMeta(A_MetaProvider())
            Router.instance.injectMeta(B_MetaProvider())
            Router.instance.injectMeta(C_MetaProvider())
        }
    }
    

    具体使用跳转的时候就像这样:

    ----------- 省略一大堆 ------------
    
    btnTest.setOnClickListener {
        Router.instance.jumpTo(this@SimpleActivity, "A-Activity222")
    }
    ----------- 省略一大堆 ------------
    

    是的没错,虽然很多情况还没有考虑,但这还真就是我们路由框架的关键流程,这个sdk中的全部,就是这少到可怜的一个类,和一个接口。

    真的,就这样吧,一滴都没有了。


    欸?好像哪里不太对,这个文章标题是啥来着???

    嘿,皮一下,就很舒服。
    🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡

    从上面的代码中可以看出,有一些代码是属于重复性的模板代码的。
    比如功能模块中的Provider实现类...们,
    比如Application中几乎完全相同的三句注入代码。

    这种规律性的重复代码,很适合在编译期生成,可以通过APT的能力,定义一个类注解,挂在Activity类上,注解的值就是tag字符串,这样开发者就不需要再写这些类似清单的蛋疼代码了。


    现在开始操刀进行APT改造

    现在我们主要的任务是如何自动生成元信息提供者,和自动化的注入逻辑。路由类和提供者接口是不需要动的。

    首先在sdk模块中定义一个注解

    • 这个当然是要写到路由框架模块中的
    @Target(AnnotationTarget.CLASS)  // 这个代表此注解可以在类上使用
    @Retention(AnnotationRetention.SOURCE)  // 这个是作用范围,只在源码期有效,编译完就没了,反射拿不到
    annotation class RouteMeta(val value: String)
    

    挂到Activity上

    @RouteMeta("A-Activity222")
    class SimpleActivity2: AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // do something
        }
    }
    

    然后跳转还是上面写过的onClick中的内容。

    因为是跨模块的开发,我们定义一个变量声明在模块的gradle中

    --- build.gradle ---  这里我们定一个 "ROUTER_NAME" 变量,值就是模块项目名,稍后APT中会使用到
    
    android {
        ......
        defaultConfig {
            ......
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [ROUTER_NAME : project.getName()]
                }
            }
        }
    }
    

    新建一个注解处理器的module,我们就叫它route-apt

    这个库中不会包含安卓资源,所以建个java library就可以了,需要添加google的auto-service库,我们的全部本事都靠它才能施展。还需要把注解类按原包结构直接复制一份过来。
    因为java-library不能依赖android-library,所以其实最好是把注解单独作为一个模块,这样sdk和apt都可以对其依赖。
    gradle全文:

    apply plugin: 'java-library'
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    
    dependencies {
        implementation 'com.google.auto.service:auto-service:1.0.1'
    }
    

    最核心的是一个处理器类,继承AbstractProcessor

    因为我抄了很多现有java逻辑,这一部分就不用kotlin了(才不是懒呢)。

    /** 这类就是我们所有处理的入口了 */
    public class MetaProcessor extends AbstractProcessor {
        private static final String KEY_ROUTER_NAME = "ROUTER_NAME";
    
        private Messager messager;
        private Elements elementUtils;
        private Types types;
    
        // 这里重写一下初始化,拿一些工具,因为这部分代码是编译时过程的一部分,没有debug的机会,
        // 只能在每个关键阶段多打一些日志来验证了。
        // 当然,processingEnv其实是父类的成员变量,这里不拿也可以在任何地方拿到。
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
            types = processingEnv.getTypeUtils();
            elementUtils = processingEnv.getElementUtils();
        }
    
        // 这个方法重写一下,返回我们要处理的注解类名,用于过滤
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            HashSet<String> supportTypes = new HashSet<>();
            supportTypes.add(RouteMeta.class.getCanonicalName());
            return supportTypes;
        }
    
        // 重点就在这里了,这是处理的入口方法,会从这里开始处理源代码,并根据逻辑生成新代码。
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            return false;
        }
    }
    

    process方法逻辑相对复杂,我们拿出来单独看

        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            String routerName = processingEnv.getOptions().get(KEY_ROUTER_NAME);
    
            messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: --------- process start --------- " + routerName);
    
            // 搞一个map在循环时找到目标暂存进去
            Map<String, TypeElement> elementMap = new HashMap<>();
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(RouteMeta.class);
            for (Element item : elements) {
                if(isActivity(item.asType())) {
                    RouteMeta routeMeta = item.getAnnotation(RouteMeta.class);
                    messager.printMessage(Diagnostic.Kind.NOTE,
                            "MetaProcessor: found activity: " + item.asType().toString()
                                    + ", path = " + routeMeta.value());
                    if(!elementMap.containsKey(routeMeta.value())) {
                        elementMap.put(routeMeta.value(), (TypeElement) item);
                    }
                }
            }
            messager.printMessage(Diagnostic.Kind.NOTE,
                    "MetaProcessor: find activity with annotation end, count is " + elementMap.size());
    
            // 已经找到了当前模块中所有带有 RouteMeta 注解的activity,开始生成对应provider类
            generateCode(routerName, elementMap);
            return true;
        }
    
        // 判断是否是activity类型
        private boolean isActivity(TypeMirror typeMirror) {
            TypeMirror activityType = elementUtils.getTypeElement("android.app.Activity").asType();
            if(!types.isSubtype(typeMirror, activityType)) {
                messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: unsupported annotation on type: " + typeMirror.toString());
                return false;
            }
            return true;
        }
    
        // 生成包含全部带注解的 activity 的 provider,这部分就是写代码了,根据拿到的信息,一句一句拼出来
        private void generateCode(String routerName, Map<String, TypeElement> elementMap) {
            if(elementMap.isEmpty()) {
                return;
            }
            String packageName = "com.example.router";
            String className = routerName + "_$_RouteProvider";
    
            StringBuilder stringBuffer = new StringBuilder("package ").append(packageName).append(";\n\n");
            stringBuffer.append("import java.util.Map;\n");
            stringBuffer.append("import java.util.HashMap;\n");
            stringBuffer.append("import com.example.route_sdk.MetaProvider;\n\n");
            stringBuffer.append("public class ").append(className).append(" implements MetaProvider {\n");
            stringBuffer.append("public Map<String, Class<?>> getMetaHere() {\n");
            stringBuffer.append("HashMap<String, Class<?>> map = new HashMap<>();\n");
            elementMap.forEach( (k, v) -> {
                stringBuffer.append("map.put(")
                        .append("\"").append(k).append("\"")
                        .append(", ")
                        .append(v.asType().toString())
                        .append(".class);\n");
            });
            stringBuffer.append("return map;\n");
            stringBuffer.append("}\n");
            stringBuffer.append("}\n");
    
            try {
                Writer writer = processingEnv.getFiler()
                        .createSourceFile(className)
                        .openWriter();
                writer.write(stringBuffer.toString());
                writer.close();
                messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: write java file done, class name is: " + className);
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: catch IOException when write java file, class name is: " + className);
                e.printStackTrace();
            }
        }
    

    为了让gradle识别我们的processor,需要添加配置文件。
    在src\main下创建文件夹resources,在其中创建 META-INF\services,
    创建一个文本文件:javax.annotation.processing.Processor
    内容是处理器全类名,如:com.example.route_apt.MetaProcessor

    好,代码写完,build一下项目,控制台中可以看到输出的日志了,代表我们的Processor生效了:

    看个截图吧1.png

    然后,在模块下的build文件夹中可以分别找到生成的java文件,和临时编译的class文件:
    build\generated\ap_generated_sources\debug\out 下的 xxx.java
    build\intermediates\javac\debug\classes\com\example\person 下的 xxx.class
    (我的studio版本是北极狐,gradle版本7.0,不同版本下的临时文件目录可能不一致。)

    看个截图吧2.png

    最后再简化一下初始注入逻辑

    APT的工作此时已经完成了,上面生成的类会随着项目代码一起打包到最终的apk中,代码中也是可见的。在初始化时可以通过直接引用来完成路由注入。
    但我们在知道其命名规则的情况下,完全可以通过类名反射的方式来使用,避免写过多重复代码,这部分的反射调用数量等于模块的数量,不会太多,性能也不会成为问题。
    在Router里加一个注入方法:

    public class Router {
    
        ..................
        ............
    
        /**
         * 通过模块名和固定的包名和类后缀,拼接要加载的Provider类名,通过反射进行路由信息收集
         * @param projectNames 要加载的模块工程名
         */
        public void injectFromProject(String... projectNames) {
            String packageName = "com.example.router.";
            String classSuffix = "_$_RouteProvider";
            try {
                for (String item : projectNames) {
                    MetaProvider provider = (MetaProvider) Class.forName(packageName + item + classSuffix).newInstance();
                    injectMeta(provider);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    这个方法需要传入模块名,还是会有一些耦合性,阿里的ARouter这部分逻辑是遍历apk中所有dex,根据包名和类型来找出对应的Provider 的。这部分代码比较多,这里就不贴了。

    最后,在Application的onCreate中,调用此方法完成初始化,就可以啦!

    Router.getInstance().injectFromProject("app", "person", "order", "xxxx");
    

    🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

    --------------------------- 完结,撒花 ---------------------------

    🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

    转载请注明出处,@via 小风风吖-我能用注解处理器APT做什么 - 手写一个路由框架 蟹蟹。

    相关文章

      网友评论

        本文标题:我能用注解处理器APT做什么 - 手写一个路由框架

        本文链接:https://www.haomeiwen.com/subject/edmntrtx.html