安卓开发博客

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

2021-11-24  本文已影响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

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


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

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

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

路由器

/** 没错这就是路由器本器 */
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做什么 - 手写一个路由框架 蟹蟹。
上一篇下一篇

猜你喜欢

热点阅读