优质广告供应商

广告是为了更好地支持作者创作

Android编译时注解

之前写了注解基础和运行时注解这篇文章,里面使用运行时注解来模仿ButterKnife绑定控件ID的功能,运行时注解主要是运行时使用反射来找到注解进行一些操作;反射存在一定的性能问题,而且一般使用了注解的框架都是使用编译时注解

仔细看了一下这篇文章,Android 如何编写基于编译时注解的项目,自己尝试写了代码理解了一番,感觉还是比较有意思的;

实现原理

1.注解处理器(Annotation Processor):用来在编译时扫描和处理注解,我们需要实现自己的注解处理器,去处理我们自己的注解,一般就是去生成我们需要的代码文件;
2.我们实现的注解处理器会被打包成jar在编译的过程中调用,为了让java编译器识别出这个自定义的注解处理器,我们需要注册一下

  1. 需要使用到注解处理的插件,因为Android Studio原本是不支持注解处理器的

整个流程大概就是,我们先创建注解,创建注解处理器,然后代码中使用注解;在编译的时候注解处理插件会使用我们的注解处理器去处理注解,生成相应的代码;

具体实现

创建注解

还是以实现一个ViewById注解为例,在项目中新创建一个Java Library,模块名为annotation用来保存所有注解,然后创建一个编译时注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewById {
    int value() default -1;
}

注解处理器的创建和注册

注解处理器是必须放在一个Java Library中,所以创建一个annotator模块,用来实现注解处理器,这里通过创建类IocProcessor继承AbstractProcessor重写其中方法来实现一个注解处理器

@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor {
...

注册有比较简单的方法,只需要给IocProcessor加一个AutoService注解就可以实现注册,这个注解需要依赖一个库

implementation 'com.google.auto.service:auto-service:1.0-rc4'
implementation project(':annotation')

重写init方法,获取一些有用的变量

    /**
     * 生成代码用的
     */
    private Filer mFileUtils;

    /**
     * 跟元素相关的辅助类,帮助我们去获取一些元素相关的信息
     * - VariableElement  一般代表成员变量
     * - ExecutableElement  一般代表类中的方法
     * - TypeElement  一般代表代表类
     * - PackageElement  一般代表Package
     */
    private Elements mElementUtils;
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFileUtils = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

重写getSupportedAnnotationTypes方法,支持自己的注解

    /**
     * 添加需要支持的注解
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new LinkedHashSet<String>();
        //添加需要支持的注解
        annotationTypes.add(ViewById.class.getCanonicalName());
        return annotationTypes;
    }

这个固定写法是设置支持的版本

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

然后是最重要的process方法,这个方法就是开始处理注解,这里是先保存获取到的被注解的元素,以外部类为单元,用ProxyInfo对象去保存一个类里面的所有被注解的元素;用mProxyMap去保存所有的ProxyInfo;然后再一个个拿出来,使用了ProxyInfo对象去实现生成代码

mProxyMap.clear();
        //获取被注解的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ViewById.class);
        for (Element element : elements) {
            //检查element类型
            if (!checkAnnotationValid(element, ViewById.class)) {
                return false;
            }
            //获取到这个成员变量
            VariableElement variableElement = (VariableElement) element;
            //获取到这个变量的外部类,所在的类
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            //获取外部类的类名
            String qualifiedName = typeElement.getQualifiedName().toString();
            //一个类里面的注解都在一个ProxyInfo中处理
            ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(mElementUtils, typeElement);
                mProxyMap.put(qualifiedName, proxyInfo);
            }
            //把这个注解保存到proxyInfo里面,用于实现功能
            ViewById annotation = variableElement.getAnnotation(ViewById.class);
            int id = annotation.value();
            proxyInfo.injectVariables.put(id, variableElement);
        }


        //生成类
        for (String key : mProxyMap.keySet()) {
            ProxyInfo proxyInfo = mProxyMap.get(key);
            try {
                //创建一个新的源文件,并返回一个对象以允许写入它
                JavaFileObject jfo = mFileUtils.createSourceFile(
                        proxyInfo.getProxyClassFullName(),
                        proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        proxyInfo.getTypeElement(), e.getMessage());
            }
        }
        return true;

到这里其实都很好理解,拿到了注解的这个元素(对象),就知道它的一切信息,就可以去生成相应的代码,主要在于生成代码,是在上面代码中的这个地方实现的

//创建一个新的源文件,并返回一个对象以允许写入它
JavaFileObject jfo = mFileUtils.createSourceFile(
        proxyInfo.getProxyClassFullName(),
        proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close(); 

这里去创建Java对象,写入代码,其中创建新的源文件需要传入两个参数,一个是保存的文件的全路径,你想保存在哪里就哪里随便写,我是保存在被注解的这个变量所在类的同一个包下,另一个参数是传入一个基本元素,但是说实话,这个基本元素我在网上查了很久,不知道这个东西有什么用,删了也没问题,直接删了好了;

代码是通过proxyInfo.generateJavaCode()来获取的相应生成的代码去生成对象的,是具体生成代码的方法

    /**
     * 生成代码
     *
     * @return
     */
    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("import com.dhht.annotation.*;\n");
        builder.append("import com.dhht.annotation.R;\n");
        builder.append("import com.dhht.annotationlibrary.*;\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();
    }

就是使用字符串组建一下代码,字符串拼接倒是非常简单,这里也没有展示完全,可以看看拼接出来的代码,我稍微优化了一下格式;这里有专门生成代码的库可以使用,比字符串拼接好用,叫Javapoet

package com.dhht.annotation.activity;

import com.dhht.annotation.*;
import com.dhht.annotation.R;
import com.dhht.annotationlibrary.*;

public class MainActivity$$ViewInject implements ViewInject<com.dhht.annotation.activity.MainActivity> {
    @Override
    public void inject(com.dhht.annotation.activity.MainActivity host, Object source) {
        if (source instanceof android.app.Activity) {
            host.txtView = (android.widget.TextView) (((android.app.Activity) source).findViewById(R.id.txtView));
        } else {
            host.txtView = (android.widget.TextView) (((android.view.View) source).findViewById(R.id.txtView));
        }
    }
}

可以调用生成的这个类的inject()方法,为传入的host对象的.txtView控件进行初始化,这里只是在MainActivity里面用了,如果在其他类里面则会生成很多个这样的文件,而且当这个类不是Activity的时候需要传入这个控件的根布局进去;

调用生成的代码

里面还有一些检查参数是不是公共的呀,注解的对象属性是否正确呀,具体的代码生成可以下载源码看看,都是比较简单的;现在注解器处理器算是写完了,需要在我们的项目中使用,我们也新建一个Android Library,annotationlibrary专门用于提供API,这样注解的实现完全和我们的项目分开;

//这个依赖是用于对外暴露注解的
api project(':annotation')

需要实现的功能很简单,就是调用生成的代码,首先不同的类里面的注解生成的代码类是不一样的,而且生成的代码是编译的时候才生成的,肯定只能使用反射来获取这个生成的类,所以肯定需要传入使用注解的这个类,然后根据我们的命名规则获取到;

    /**
     * 根据使用注解的类和约定的命名规则,反射获取注解生成的类
     *
     * @param object
     * @return
     */
    private static ViewInject findProxyActivity(Object object) {
        try {
            Class clazz = object.getClass();
            Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
            return (ViewInject) injectorClazz.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
    }

再说我们这个绑定控件,从生成的代码来看,需要一个Activity或者一个View来调用findViewById方法,所以使用注解的不是Activity的类,在需要加一个Object参数,传入View进来;

public interface ViewInject<T> {
    /**
     * 提供给生成的代码去绑定id用的
     *
     * @param t
     * @param source
     */
    void inject(T t, Object source);
}

然后考虑到Activity就不用传另一个参数了,所以新建两个方法完事儿

public static void injectView(Activity activity) {
        ViewInject proxyActivity = findProxyActivity(activity);
        proxyActivity.inject(activity, activity);
    }

    public static void injectView(Object object, View view) {
        ViewInject proxyActivity = findProxyActivity(object);
        proxyActivity.inject(object, view);
    }

然后在项目中依赖一下,并且使用之前说的注解插件

implementation project(':annotationlibrary')
annotationProcessor project(':annotator')

然后就可以在项目中使用注解了

    @ViewById
    TextView txtView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.injectView(this);
        txtView.setOnClickListener(v -> Toast.makeText(MainActivity.this, "醉了", Toast.LENGTH_SHORT).show());
    }

使用了lambda 表达式简单了不少,在模块的build.gradle的android节点下面添加支持

compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }

就完成了,整体还是比较好理解的,关键在于得下载代码自己试试

项目地址:https://github.com/tyhjh/Annotation

优质广告供应商

广告是为了更好地支持作者创作