Android AOP之路四 编译时注解详细讲解

一、 简介

在第一篇文章时候有说了,注解有三个功能:

  • 标记信息: 第二篇文章说了
  • 运行时候动态处理: 第三篇文章说了
  • 编译时候动态处理: 这篇文章说

简单说明一下AOP和APT的关系:

  • AOP是Aspect-oriented programming的缩写,叫做面向切面编程,例如OOP(面向对象),AOP只是一种思想的统称,实现这种思想的方法有挺多,例如面向对象的有java、c++、c#等。 AOP通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率。

  • APT是AOP的其中一个实现方法,全名Annotation Processing Tool,注解处理器。对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。总结一句话,就是在编译时候,根据注解生成对应需要的文件,这样子在app运行的时候就不会导致性能损耗。很多著名的框架都利用这种方法写的,例如Retrofit、DataBinding、Dagger2、ButterKnife、EventBus3

APT的实现方案也有两种:

  • android-apt,个人开发者提供,现在已经停止维护,AndroidStudio3.0已经不支持。
  • Gradle插件:annotationProcessor,由官方提供支持,现在AndroidStudio3.0不再支持android-apt,只支持annotationProcessor。

二、 ButterKnife使用和原理

这里也是从ButterKnife这个框架入手,首先了解一下ButterKnife的简单使用方法和原理,从原理入手。

首先给需要获取的view进行注解

    @BindView(R.id.btn_open)
    Button mBtnOpen;

然后对前的Activity进行注入:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_index);
        TPButterKnife.inject(this);
    }

问题的根本,我们就是要对注解修饰的mBtnOpen进行获取,怎么获取呢,就是在TPButterKnife.inject(this)里面进行获取。所以inject里面的代码逻辑是获取到Activity这个实例,然后调用一个注解生成类的方法对注解的控件变量进行初始化,所以这个inject注入方法的内部是这个样子的:

    public static void inject(Activity activity){

        inject(activity,activity);

    }

    public static void inject(Object host, Object root){

        Class<?> clazz = host.getClass();

        //拼接 生成类的全路径,
        String proxyClassFullName = clazz.getName()+ "$$" + "ViewInject";

        //省略try,catch相关代码
        Class<?> proxyClazz = null;

        proxyClazz = Class.forName(proxyClassFullName);
        //通过newInstance生成实例,强转,调用代理类的inject方法
        ViewInject viewInject = (ViewInject) proxyClazz.newInstance();
        //调用生成类里面的inject方法,进行findView
        viewInject.inject(host,root);

    }

这个生成类proxyClassFullName 就是通过我们利用编译时注解的形式来生成的,里面就是把Activity的控件进行findView,生成的类的代码是这样子的:

public class IndexActivity$$ViewInject implements ViewInject<com.tpnet.processordemo.IndexActivity> {
    @Override
    public void inject(com.tpnet.processordemo.IndexActivity host, Object source) {
        if (source instanceof android.app.Activity) {
            host.mBtnOpen = (android.widget.Button) (((android.app.Activity) source).findViewById(2131165219));

        } else {
            host.mBtnOpen = (android.widget.Button) (((android.view.View) source).findViewById(2131165219));

        }

    }
}

so,所以,我们编译时候注解需要做的事情,就是需要在编译的时候,生成这个类IndexActivity$$ViewInject,供给MainActivity在注入的时候进行对控件的初始化。

三、基本实现

3.1 基本套路

现在的编译时注解框架都有一个开发套路,AndropidStudio的Module的分布一般都如下:

  • api
  • annotation
  • compiler
  • sample

api: 对外公开的方法。例如ButterKnife的ButterKnife类、ViewInject接口。
这里写图片描述

annotation: 里面放的是自定义的注解,例如@BindView
这里写图片描述

compiler: 存放用于生成 生产类的 文件,一般是Processor文件。
这里写图片描述

sample/app: 使用注解框架的Demo例子

所以这个四个module的依赖方式是这个样子的,

  • sample因为需要注入,肯定需要依赖api。
  • 然后sample也要用到注解,但是因为依赖了api,所以为了减少使用的代码,在api库依赖annotation。
  • compiler库因为也需要处理注解,所以compiler也需要依赖annotation
  • 最后sample需要编译时注解,需要利用annotationProcessor project方式依赖compiler库。

所以,你这个框架写完之后,用户使用的话,需要依赖两行,一个是api,一个是compiler。 例如ButterKnif的依赖使用:
这里写图片描述

然后看ButterKnife的module分布也是按照这个套路来的:

这里写图片描述

然后看阿里巴巴的路由框架ARouter也是这个套路:
这里写图片描述

解释完基本的情况,就开始写一个简单ButterKnife框架了。

3.2 编写annotation模块

新建Module,File > New Module > Android Library或者Java Library,起名字为bind-annotation。在这里编写对外开放的类和方法。这里比较简单,一个@BindView注解即可:


@Target(ElementType.FIELD)   //代表注解的是变量
@Retention(RetentionPolicy.CLASS)   //这个注解在类期间作用
public @interface BindView {
    int value();   //可写,可不写,因为这个参数是默认的
}

3.3 编写compiler模块

新建Module,File > New Module > Java Library,起名bind-compiler,在这里编写 需要在编译时期生成的类 的实现代码。

首先是一个新建一个BindProcessor文件,继承AbstractProcessor(这里注意,如果是Android Library的Module是没有这个AbstractProcessor类的噢,这个类在jdk的rt.jar包里面), process(...)这个方法是必须实现的,就是在这个回调方法里面进行文件的生成

public class BindProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    ...
    }

}

这个process方法先不管,还有三个需要实现的方法是有用的:

    /**
     * 初始化变量
     * 当我们编译程序时注解处理器工具会调用此方法并且提供实现ProcessingEnvironment接口的对象作为参数。
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        //返回实现Messager接口的对象,用于输出控制台信息
        mMessager = processingEnv.getMessager();

        //返回实现Filer接口的对象,用于创建文件、类和辅助文件。
        mFileUtils = processingEnv.getFiler();

        //用于元素处理
        mElementUtils = processingEnv.getElementUtils();

    }

    //添加当前要处理的注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotation = new LinkedHashSet<>();
        annotation.add(BindView.class.getCanonicalName());
        return annotation;
    }

    //支持的jdk版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

两个getSuppport方法也可以利用注解来代替,:

@SupportedAnnotationTypes("com.tpnet.apt.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindProcessor extends AbstractProcessor {...}

PS: getSupportedAnnotationTypes 必须重写 或者 必须添加SupportedAnnotationTypes注解。二选一。 不然程序跑不起来。

然后继续实现process方法,这个方法作用就是根据注解,拿到对应的类、方法、变量,然后生成代理类

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        //控制台打印数据
        mMessager.printMessage(Diagnostic.Kind.NOTE,"开始处理进程Processor");

        mProxyMap.clear();

        //获取注解的 元素(变量、类、方法)
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);

        //第一步:循环处理每个注解的元素 放到Map里面
        for(Element element:elements){

            //检查element类型是否是变量VariableElement
            if (!(element instanceof VariableElement)){
                //不是变量直接返回
                return false;
            }

            //转换为变量类型,这里为修饰的变量
            VariableElement variableElement = (VariableElement)element;

            //这里为修饰的变量的所在类 - IndexActivity
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

            //获取类的全路径,作为key --com.tpnet.processordemo.IndexActivity
            String qualifiedName = typeElement.getQualifiedName().toString();

            //生成对象
            ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);

            if(proxyInfo == null){
                proxyInfo = new ProxyInfo(mElementUtils,typeElement);
                mProxyMap.put(qualifiedName,proxyInfo);
            }

            //获取注解,
            BindView annotation = variableElement.getAnnotation(BindView.class);
            //获取注解里面的value,这里是(R.id.xx)
            int id = annotation.value();  
            //保存需要处理的每个被注解的变量
            proxyInfo.injectVariables.put(id,variableElement);

        }


        //第二步骤: 遍历Map生成代理类
        for(String key: mProxyMap.keySet()){
            ProxyInfo proxyInfo = mProxyMap.get(key);

            try {
                //创建文件对象
                JavaFileObject soureFile = mFileUtils.createSourceFile(
                        proxyInfo.getProxyClassFullName(),   //文件名,全路径
                        proxyInfo.getTypeElement());
                //创建写入对象
                Writer writer = soureFile.openWriter();
                //写入内容
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        return true;
    }

PS: mMessager打印的信息,在Make Project的时候,在AS的右下角的Gradle Console可以看到输出的信息。

ProxyInfo内容为如下,用于生成代码,注释都在代码里面。

public class ProxyInfo {

    private String packageName;   //包名 --com.tpnet.processordemo

    private String proxyClassName;  // 生成的类的名称 --IndexMativity$$ViewInject

    private TypeElement typeElement;  // 类

    //存放view的id,元素
    public Map<Integer, VariableElement> injectVariables = new HashMap<>();

    public static final String PROXY = "ViewInject";   //这个名称,需要对应api的Module里面的ViewInject接口的名称

    public ProxyInfo(Elements elementUtils, TypeElement classElement) {
        this.typeElement = classElement;

        //获取包名
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        this.packageName = packageElement.getQualifiedName().toString();

        //生成 生成类的名称 -- IndexMativity$$ViewInject
        this.proxyClassName = classElement.getSimpleName() + "$$" + PROXY;
    }


    /**
     * 生成java文件代码
     * @return
     */
    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");

        //注意,这个ImPort的包路径,是api的包路径
        builder.append("import com.tpnet.apt.*;\n");
        builder.append('\n');

        builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
        builder.append(" {\n");

        generateMethods(builder);
        builder.append('\n');

        builder.append("}\n");
        return builder.toString();

    }


    /**
     * 生成方法
     * @param builder
     */
    private void generateMethods(StringBuilder builder) {

        builder.append("@Override\n ");
        builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n");

        for (int id : injectVariables.keySet()) {
            VariableElement element = injectVariables.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append(" if(source instanceof android.app.Activity){\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
            builder.append("\n}else{\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
            builder.append("\n}");
        }
        builder.append("  }\n");
    }

    public String getProxyClassFullName() {
        return packageName + "." + proxyClassName;
    }

    public TypeElement getTypeElement() {
        return typeElement;
    }

}

还有很重要的一步,创建指向文件,在bind-compiler的module的main目录创建一个resources文件夹(不是res文件夹),在resources下创建META-INF文件夹,在META-INF下创建services文件夹,然后创建一个文件javax.annotation.processing.Processor,里面填写你的Processor的全路径。作用就是告诉编译器,我的处理文件是这个路径的类。

com.tpnet.apt.BindProcessor

最后记得依赖annotation:

    compile project(':bind-annotation')

PS: 记得依赖annotation

3.4 编写api模块

File > New Module > Android Library

必须是Android Library,因为需要处理Activity,起名字为bind-api,提供对外的注入TPButterKnife.java:

public class TPButterKnife {

    private static final String SUFFIX = "$$" + ViewInject.class.getSimpleName();

    public static void inject(Activity activity){
        inject(activity,activity);
    }

    public static void inject(Object host, Object root){

        Class<?> clazz = host.getClass();

        //拼接类的全路径,
        String proxyClassFullName = clazz.getName() + SUFFIX;

        Class<?> proxyClazz = null;
        try {
            proxyClazz = Class.forName(proxyClassFullName);

            //通过newInstance生成实例,强转,调用代理类的inject方法
            ViewInject viewInject = (ViewInject) proxyClazz.newInstance();

            //调用生成类里面的inject方法,进行findView
            viewInject.inject(host,root);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}

还有个接口ViewInject.java:

public interface ViewInject<T> {
    void inject(T t, Object object);
}

PS: 记得依赖annotation

3.5 使用

在app这个module进行使用,首先是先依赖api库和注解处理库:

    compile project(':bind-api')
    annotationProcessor project(':bind-compiler')

然后,如常地使用@BindView,然后Build - Make Project,接下来你会在 app - build - generated - source - apt - debug - 看到生成的类
这里写图片描述

四、更优雅的使用

上面有些技术都是原始化,多做了工作了,我们可以利用某些库,进行更优雅的开发。

4.1 auto-service

上面的compiler Module中,我们手动创建resources文件夹进行配置,谷歌提供了一个库可以更方便的,不用手动创建,直接添加一个注解即可帮你完成创建这个javax.annotation.processing.Processor文件。

首先导包:

    compile 'com.google.auto.service:auto-service:1.0-rc3'

然后在你的Processor类上面添加注解@AutoService(Processor.class)即可,resources文件夹就可以删掉了:

@SupportedAnnotationTypes("com.tpnet.apt.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    ...
}

4.2 JavaPoet

在上面我们是通过字符串拼接的方式去组合一个类的源码,利用FileUtil来创建,这种方式是比较糟糕的,当然有困难就有解决方案:JavaPoet。square公司出品,生成java源文件的一个库,。

导包:

    compile 'com.squareup:javapoet:1.9.0'

javapoet具体的使用方法就不多说了,网上比较多。在之前的代码中,我们在第二个步骤进行生成文件,下来我们进行改版:

        //第二步骤: 遍历Map生成代理类
        for (String key : mProxyMap.keySet()) {
            ProxyInfo proxyInfo = mProxyMap.get(key);

            //方法构建器
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("inject")   //方法名
                    .addModifiers(Modifier.PUBLIC)   //方法的修饰为public
                    .addAnnotation(Override.class)   //添加注解
                    .addParameter(                   //添加方法参数
                            ParameterSpec.builder(TypeName.get(proxyInfo.typeElement.asType()), "host").build())
                    .addParameter(Object.class, "source");

            //循环添加方法体
            for (int id : proxyInfo.injectVariables.keySet()) {

                //被BindView修饰的变量
                VariableElement element = proxyInfo.injectVariables.get(id);

                //if控制流程开始
                methodBuilder.beginControlFlow("if(source instanceof $T)", mElementUtils.getTypeElement(proxyInfo.typeFullName));

                //if内部逻辑代码
                methodBuilder.addStatement("host.$L = ($T)((($L)source).findViewById($L))",
                        element.getSimpleName(),
                        element.asType(),
                        "android.app.Activity",
                        id);
                //else控制流程
                methodBuilder.nextControlFlow("else");

                //else的内部逻辑代码
                methodBuilder.addStatement("host.$L = ($T)((($L)source).findViewById($L))",
                        element.getSimpleName(),
                        element.asType(),
                        "android.view.View",
                        id);

                //结束if控制流程
                methodBuilder.endControlFlow("");

            }

            //构建方法
            MethodSpec method = methodBuilder.build();

            //类
            TypeSpec type = TypeSpec.classBuilder(proxyInfo.proxyClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(ParameterizedTypeName.get(    //实现接口,
                            ClassName.get("com.tpnet.apt", "ViewInject"),   //接口
                            ClassName.get(proxyInfo.packageName, proxyInfo.typeElement.getSimpleName().toString()))   //接口泛型
                    )
                    .addMethod(method)  //添加方法
                    .build();
            try {

                //创建Java文件
                JavaFile file = JavaFile.builder(proxyInfo.packageName, type).build();
                file.writeTo(mFileUtils);

            } catch (IOException e) {
                e.printStackTrace();
            }

        }

完整Demo: https://github.com/tpnet/ProcessorDemo

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KeepStudya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值