Android编译时注解--入门篇(AbstractProcessor、APT)

前言

对于注解的使用,想必大家都不陌生,它出现在我们的源码中,以及大部分框架中,比如ButterKnifeArouterRetrofit,但它们是有区别的,其中前2个是编译时注解,最后一个是运行时注解。我们都知道用注解可以简化代码,但大多数同学对其中的原理和实现方式还是很陌生的,下面就带大家入门编译时注解

系列文章
Android APT工作原理(annotationProcessor)

作用

可以在编译时生成额外的.java文件,在程序运行的时候调用相关方法,可以达到减少重复代码的效果。它的好处:提高开发效率,使得项目更容易维护和扩展,同时几乎不影响性能。

原理

通过APT(Annotation Processing Tool)技术,即注解处理器,在编译时扫描并处理注解,注解处理器最终生成处理注解逻辑的.java文件。

流程概要

老司机的忠告
具体实现逻辑在某些细节上会有一些难度,甚至读第一遍的时候无法理解,但是不要慌,有老司机为你指明方向。只要掌握了下面的概要,理解关键逻辑,就不会迷失方向。如果后面在阅读注解处理器的代码实现遇到障碍,请反复阅读,多敲几遍代码,终会拨开云雾见青天。

具体概要

  • 1、创建自定义注解@interface
  • 2、创建并注册注解处理器AbstractProcesso,生成处理注解逻辑的.java文件;
  • 3、封装一个供外部调用的API,用的是反射技术,具体来说就是调用第二步中生成的代码中的方法;
  • 4、在项目中使用,比如ActivityFragmentAdapter

最终效果

这里以仿写ButterKnife为例

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv1)
    TextView textView;
    @BindView(R.id.tv2)
    TextView textView2;

    @BindView(R.id.rv)
    RecyclerView recyclerView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
        textView2.setText("this is Activity  compile annotation");

        addFragment();
        initRv();
    }

这段代码,想必大家很熟悉,我们都知道最终是为每个加了注解的控件变量生成了类似textView=findViewById()代码,但是它们的职责又是怎样的呢?下面我请大家带着几个问题来阅读本文。

  • Q1:@BindView()做了什么?它是如何申明注解的?它和动态生成注解处理代码有什么联系?
  • Q2:ButterKnife.bind();做了什么?与申明的注解之间有什么直接联系吗?
  • Q3:是谁参与了生成注解处理类(代理类)?是@BindView()还是ButterKnife还是2个都参与了呢?

这些问题只是为了让大家多一点思考,如果不会也不必挠破头皮去想,跟着老司机的思路走,最后都能得到答案。

APT实现方案

这里按功能职责,分多个module来处理,具体如下:

  • annotation:自定义注解(java lib)
  • processor:注解处理器AbstractProcessor (java lib)
  • inject_api: 供外部调用的API (android lib)
  • app:项目使用 android lib)
步骤一:创建自定义注解

创建一个Java Library,名称为annotation,作用是保存所有注解。


@Retention(RetentionPolicy.CLASS) //注解生命周期是编译期,存活于.class文件,当jvm加载class时就不在了
@Target(ElementType.FIELD) //目标对象是变量 
public @interface BindView {

    /**
     * @return 控件变量的resourceId
     */
    int value();
}

关于创建注解,不做过多解释,就是通过jdk提供的4个元注解就可以定义自己的注解了,具体参考:Android 自定义注解(Annotation)

步骤二:实现注解处理器AbstractProcessor

创建一个Java Library,名称为processor,作用是扫描、解析、处理注解。

processor的Gradle配置如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation project(':annotation')
    //用于自动为 JAVA Processor 生成 META-INF 信息。
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    //快速生成.java文件的库
    implementation 'com.squareup:javapoet:1.8.0'
}

创建自己的Processor来实现注解处理器

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    /**
     * 生成文件的工具类
     */
    private Filer filer;
    /**
     * 打印信息
     */
    private Messager messager;
    /**
     * 元素相关
     */
    private Elements elementUtils;
    private Types typeUtils;
    private Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();


    /**
     * 一些初始化操作,获取一些有用的系统工具类
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
    }


    /**
     * 设置支持的版本
     *
     * @return 这里用最新的就好
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 设置支持的注解类型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //添加支持的注解
        HashSet<String> set = new HashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }

 /**
     * 注解内部逻辑的实现
     * <p>
     * Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在
     * TypeElement:变量;TypeElement:类或者接口
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){}

下面开始这个Processor类的详解:

  • 继承AbstractProcessor,需要重写process()方法,这里是处理注解内部逻辑的,也是本文的关键点之一;

  • 此外还需要实现几个简单的方法init ()getSupportedSourceVersion()getSupportedAnnotationTypes()

  • getSupportedSourceVersion():设置支持的版本,一般用最新的就好;

  • getSupportedAnnotationTypes():添加支持的注解类型,可以是单个/多个,用Set存储;

  • init ():一些初始化操作,获取一些有用的系统工具类,比如生成文件、打印信息、处理元素等;

getSupportedSourceVersion()getSupportedAnnotationTypes()这2个方法还有一种简单的方式来实现,如下:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.zx.annotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ViewInjectProcessor extends AbstractProcessor {}

是通过注解来实现的,看上去是比较简洁

  • @SupportedAnnotationTypes()可以申明一个注解数组,但是这种字符串拼接容易出错;
  • @SupportedSourceVersion:设置支持的源码版本,可以是RELEASE_0~RELEASE_8,但是不能使用latestSupported()设置最新的版本;

所以我个人还是喜欢代码实现的方案来设置

  • @AutoService(Processor.class),生成 META-INF 信息;
    这里使用 Google 的 AutoService 来创建 META-INF,并将被注解的文件声明到 META-INF 中,这里是ButterKnifeProcessor
    下面是META-INF 的文件目录:

文件内容是:

com.zx.processor.ButterKnifeProcessor

这就是我们自定义的注解处理器ButterKnifeProcessor的路径。

接下来讲解最关键的process()方法

它的工作内容是扫描处理注解,最终生成.java文件,这个文件里面就是注解的业务逻辑。

这里主要分为3步:

  • 1、通过getElementsAnnotatedWith()获取要处理的注解的元素的集合,换句话说,找到所有Class中被@BindView注解标记的变量;
  • 2、遍历第一步中的元素集合,由于这个注解可能会在多个类中使用,所以我们以类名为单元划分注解。具体说,新建一个ProxyInfo对象去保存一个类里面的所有被注解的元素;用proxyInfoMap去保存所有的ProxyInfo;大概是这个样子Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();
  • 3、在ProxyInfo中为每个使用了@BindView注解的类生成一个代理类;
  • 4、遍历proxyInfoMap,通过ProxyInfoJavaFile生成具体的代理类文件

注意:processor这部分只是部分关键代码,完整代码还请到文末查看

 /**
     * 注解内部逻辑的实现
     * <p>
     * Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在
     * TypeElement:变量;TypeElement:类或者接口
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "annotations size--->" + annotations.size());
        //1、获取要处理的注解的元素的集合
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);

        //process()方法会调用3次,只有第一次有效,第2,3次调用的话生成.java文件会发生异常
        if (elements == null || elements.size() < 1) {
            return true;
        }

        //2、按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类
        for (Element element : elements) {
            checkAnnotationValid(element, BindView.class);

            //获取被注解的成员变量
            //这里被注解的类型只能是变量,所以可以直接强转
            VariableElement variableElement = (VariableElement) element;
            //获取该元素的父元素,这里是父类
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            //获取全类名
            String className = typeElement.getQualifiedName().toString();
            //获取被注解元素的包名
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            //获取注解的参数
            int resourceId = element.getAnnotation(BindView.class).value();

            //生成ProxyInfo对象
            //一个类里面的注解都在一个ProxyInfo中处理
            ProxyInfo proxyInfo = proxyInfoMap.get(className);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(typeElement, packageName);
                proxyInfoMap.put(className, proxyInfo);
            }
            proxyInfo.viewVariableElement.put(resourceId, variableElement);
        }

        //3、生成注解逻辑处理类
        for (String key : proxyInfoMap.keySet()) {
            ProxyInfo proxyInfo = proxyInfoMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.packageName, proxyInfo.generateProxyClass())
                    .addFileComment("auto generateProxyClass code,can not modify")
                    .build();
            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

别看代码量稍微多了一点就害怕,其实每一步的注释很详细了,就不展开详解说了,只要按照上面的步骤去阅读都可以理解的。这里多次用到了Element,大家可能会比较困惑,我简单解释一下,

Element代表程序的一个元素,可以是package、class,、interface、 method、field,只在编译期存在。

具体来说,可以按如下分类:

  • PackageElement 一般代表Package
  • TypeElement 一般代表代表类
  • VariableElement 一般代表成员变量
  • ExecutableElement 一般代表类中的方法

通过javapoet生成注解逻辑代码的具体步骤如下:

 /**
     * 通过javapoet API生成代理类
     * @return
     */
    public TypeSpec generateProxyClass() {
        //代理类实现的接口
        ClassName viewInjector = ClassName.get("com.zx.inject_api", "IViewInjector");
        //原始的注解类
        ClassName className = ClassName.get(typeElement);
        //  泛型接口,implements IViewInjector<MainActivity>
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(viewInjector, className);

        //生成接口的实现方法inject()
        MethodSpec.Builder bindBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class) //添加方法注解
                .addParameter(className, "target")
                .addParameter(Object.class, "source");

        for (int id : viewVariableElement.keySet()) {
            VariableElement element = viewVariableElement.get(id);
            String fieldName = element.getSimpleName().toString();
            bindBuilder.addStatement(" if (source instanceof android.app.Activity){target.$L = ((android.app.Activity) source).findViewById( $L);}" +
                    "else{target.$L = ((android.view.View)source).findViewById($L);}", fieldName, id, fieldName, id);
        }

        MethodSpec bindMethodSpec = bindBuilder.build();

        //创建类
        TypeSpec typeSpec = TypeSpec.classBuilder(proxyClassName)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(parameterizedTypeName) //实现接口
                .addMethod(bindMethodSpec)
                .build();

        return typeSpec;
    }

这里通过javapoet的API去生成代理类的代码,大概就是TypeSpec生成类,MethodSpec生成方法。这里以MainActivity举例,具体生成处理注解的代理类如下:

package com.zx.compileannotation;

import com.zx.inject_api.IViewInjector;

import java.lang.Object;
import java.lang.Override;

public class MainActivity_ViewBinding implements IViewInjector<MainActivity> {
    @Override
    public void inject(MainActivity target, Object source) {
        if (source instanceof android.app.Activity) {
            target.textView2 = ((android.app.Activity) source).findViewById(2131230920);
        } else {
            target.textView2 = ((android.view.View) source).findViewById(2131230920);
        }
        ;
        if (source instanceof android.app.Activity) {
            target.recyclerView = ((android.app.Activity) source).findViewById(2131230849);
        } else {
            target.recyclerView = ((android.view.View) source).findViewById(2131230849);
        }
        ;
        if (source instanceof android.app.Activity) {
            target.textView = ((android.app.Activity) source).findViewById(2131230919);
        } else {
            target.textView = ((android.view.View) source).findViewById(2131230919);
        }
        ;
    }
}

注意:这里只是以MainActivity的代理类,实际上每个用到注解的类都会生成一个类似的代理类

到这一步就会为每一个使用注解的类生成对应的代理类,代理类的位置如下:app-->build-->generated-->source-->apt-->debug

步骤三:封装一个供外部调用的API

我们知道在processor中已经生成了处理注解逻辑的代理类,那接下来就是调用了。首先我们要知道代理类是在编译器动态生成的,而且会有多个,所以我们只能通过反射找到这个类,然后调用它的方法

/**
     * 根据使用注解的类和约定的命名规则,通过反射找到动态生成的代理类(处理注解逻辑)
     * @param object 调用类对象
     */
    private static IViewInjector findProxyActivity(Object object) {
        String proxyClassName = object.getClass().getName() + PROXY;
        Log.e(TAG, "findProxyActivity: "+proxyClassName);
        Class<?> proxyClass = null;
        try {
            proxyClass = Class.forName(proxyClassName);
//            Constructor<?> constructor = proxyClass.getConstructor(object.getClass());
            return (IViewInjector) proxyClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

我们都知道在使用findViewById()的时候分为2种情况,在Activity中可以直接使用;而在Fragment、Adapter中则是view. findViewById(),所以在下面的接口方法中多设计了一个Object参数,如果是Activity传入它本身;如果是Fragment、Adapter则传入View

public interface IViewInjector<T> {
    /**
     * 通过source.findViewById()
     *
     * @param target 泛型参数,调用类 activity、fragment等
     * @param source Activity、View
     */
    void inject(T target, Object source);
}

最后提供2个供外部activity、fragment等调用的方法

 /**
     * Activity调用
     */
    public static void bind(Activity activity) {
        findProxyActivity(activity).inject(activity, activity);
    }

    /**
     * Fragment、Adapter调用
     *
     * @param object
     * @param view
     */
    public static void bind(Object object, View view) {
        findProxyActivity(object).inject(object, view);
    }
步骤四:在Activity、Fragment、Adapter等使用

在app的gradle中做如下配置

 implementation project(':annotation')
    implementation project(':inject_api')
    //gradle3.0以上apt的实现方式
    annotationProcessor project(':processor')

前2个依赖都是调用相关的,比较好理解,第3个看上去就很陌生了,很多人也不太理解,我解释一下。
annotationProcessor是 APT 实现方案的一种。APT 是一种处理注解的工具,它对源代码文件进行检测找出其中的 Annotation,再根据注解自动生成代码。以前还有一种方案android-apt ,不过不再维护了;而在Gradle3.0以上推荐使用annotationProcessor,这是官方提供的,具体可以查看https://bitbucket.org/hvisser/android-apt/src/default/

app.MainActivity

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv1)
    TextView textView;
    @BindView(R.id.tv2)
    TextView textView2;

    @BindView(R.id.rv)
    RecyclerView recyclerView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
        textView2.setText("this is Activity  compile annotation");

        addFragment();
        initRv();
    }

现在回头看文章开头的几个问题,答案已经揭晓。

  • @BindView():就是创建了一个自定义的编译时注解,它的生命周期是编译期,仅存活于 .class文件,当jvm加载 .class就没了;此外,它还配合processor生成了处理注解逻辑的代理类;

  • ButterKnife.bind();通过反射去找到对应的代理类,然后找到被注解的变量的赋值语句。

总结

1、需要掌握的核心技术:自定义注解、APT、反射;
2、代理类的生成 和ButterKnife.bind();没有关系;
3、编译时注解并非完全不使用反射,只是在调用的时候会使用;
4、编译时注解对性能几乎没有影响,同时还能简化代码,提升开发效率。

完整源码地址
https://github.com/zhouxu88/CompileAnnotation

写文不易,觉得有用的朋友,麻烦在git点个star,在文章刷个666,感谢阅读!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343