Android編譯時注解--入門篇(AbstractProcessor、APT)

前言

對于注解的使用,想必大家都不陌生,它出現在我們的源碼中,以及大部分框架中,比如ButterKnife、Arouter、Retrofit,但它們是有區(qū)別的,其中前2個是編譯時注解,最后一個是運行時注解。我們都知道用注解可以簡化代碼,但大多數同學對其中的原理和實現方式還是很陌生的,下面就帶大家入門編譯時注解

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

作用

可以在編譯時生成額外的.java文件,在程序運行的時候調用相關方法,可以達到減少重復代碼的效果。它的好處:提高開發(fā)效率,使得項目更容易維護和擴展,同時幾乎不影響性能。

原理

通過APT(Annotation Processing Tool)技術,即注解處理器,在編譯時掃描并處理注解,注解處理器最終生成處理注解邏輯的.java文件。

流程概要

老司機的忠告
具體實現邏輯在某些細節(jié)上會有一些難度,甚至讀第一遍的時候無法理解,但是不要慌,有老司機為你指明方向。只要掌握了下面的概要,理解關鍵邏輯,就不會迷失方向。如果后面在閱讀注解處理器的代碼實現遇到障礙,請反復閱讀,多敲幾遍代碼,終會撥開云霧見青天。

具體概要

  • 1、創(chuàng)建自定義注解@interface;
  • 2、創(chuàng)建并注冊注解處理器AbstractProcesso,生成處理注解邏輯的.java文件;
  • 3、封裝一個供外部調用的API,用的是反射技術,具體來說就是調用第二步中生成的代碼中的方法;
  • 4、在項目中使用,比如Activity、Fragment、Adapter

最終效果

這里以仿寫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()代碼,但是它們的職責又是怎樣的呢?下面我請大家?guī)е鴰讉€問題來閱讀本文。

  • Q1:@BindView()做了什么?它是如何申明注解的?它和動態(tài)生成注解處理代碼有什么聯系?
  • Q2:ButterKnife.bind();做了什么?與申明的注解之間有什么直接聯系嗎?
  • Q3:是誰參與了生成注解處理類(代理類)?是@BindView()還是ButterKnife還是2個都參與了呢?

這些問題只是為了讓大家多一點思考,如果不會也不必撓破頭皮去想,跟著老司機的思路走,最后都能得到答案。

APT實現方案

這里按功能職責,分多個module來處理,具體如下:

  • annotation:自定義注解(java lib)
  • processor:注解處理器AbstractProcessor (java lib)
  • inject_api: 供外部調用的API (android lib)
  • app:項目使用 android lib)
步驟一:創(chuàng)建自定義注解

創(chuàng)建一個Java Library,名稱為annotation,作用是保存所有注解。


@Retention(RetentionPolicy.CLASS) //注解生命周期是編譯期,存活于.class文件,當jvm加載class時就不在了
@Target(ElementType.FIELD) //目標對象是變量 
public @interface BindView {

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

關于創(chuàng)建注解,不做過多解釋,就是通過jdk提供的4個元注解就可以定義自己的注解了,具體參考:Android 自定義注解(Annotation)

步驟二:實現注解處理器AbstractProcessor

創(chuàng)建一個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'
}

創(chuàng)建自己的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<>();


    /**
     * 一些初始化操作,獲取一些有用的系統(tǒng)工具類
     *
     * @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 ():一些初始化操作,獲取一些有用的系統(tǒng)工具類,比如生成文件、打印信息、處理元素等;

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 來創(chuàng)建 META-INF,并將被注解的文件聲明到 META-INF 中,這里是ButterKnifeProcessor。
    下面是META-INF 的文件目錄:

文件內容是:

com.zx.processor.ButterKnifeProcessor

這就是我們自定義的注解處理器ButterKnifeProcessor的路徑。

接下來講解最關鍵的process()方法

它的工作內容是掃描處理注解,最終生成.java文件,這個文件里面就是注解的業(yè)務邏輯。

這里主要分為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文件會發(fā)生異常
        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();

        //創(chuàng)建類
        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中已經生成了處理注解邏輯的代理類,那接下來就是調用了。首先我們要知道代理類是在編譯器動態(tài)生成的,而且會有多個,所以我們只能通過反射找到這個類,然后調用它的方法

/**
     * 根據使用注解的類和約定的命名規(guī)則,通過反射找到動態(tài)生成的代理類(處理注解邏輯)
     * @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():就是創(chuàng)建了一個自定義的編譯時注解,它的生命周期是編譯期,僅存活于 .class文件,當jvm加載 .class就沒了;此外,它還配合processor生成了處理注解邏輯的代理類;

  • ButterKnife.bind();通過反射去找到對應的代理類,然后找到被注解的變量的賦值語句。

總結

1、需要掌握的核心技術:自定義注解、APT、反射;
2、代理類的生成 和ButterKnife.bind();沒有關系;
3、編譯時注解并非完全不使用反射,只是在調用的時候會使用;
4、編譯時注解對性能幾乎沒有影響,同時還能簡化代碼,提升開發(fā)效率。

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

寫文不易,覺得有用的朋友,麻煩在git點個star,在文章刷個666,感謝閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容