Android APT(注解處理器之編譯時注解)

什么是注解

注解,通俗的來說,就是像注釋一樣,是由程序員在代碼中加入的一種“標注”,不影響所編寫的原有代碼的執行。而這種標注(注解)可以被編碼用的IDE、編譯器、類加載器的代理程序、其他第三方工具以及原有代碼運行期間讀取和處理,生成一些新的輔助代碼或是提示,從而節省時間,提升效率。這些工具讀取注解的時機是根據注解的生命周期來定的,注解的生命周期就是其“存在壽命”,分為三種:

1,源注解

@Retention(RetentionPolicy.SOURCE)
注解將被編譯器丟棄。如:@Override

2,類注解(ButterKnife)

@Retention(RetentionPolicy.CLASS)
注解由編譯器記錄在類文件中,但不需要由VM在運行時保留。

3,運行時注解(EventBus)

@Retention(RetentionPolicy.RUNTIME)
注解由編譯器記錄在類文件中,并在運行時由VM保存,因此可以反射性地讀取它們。 如:@Deprecated

APT(Annotation Processing Tool)注解處理器, 是一個Gradle插件,協助Android Studio 處理annotation processors,

是一種處理注解的工具,確切的說它是javac的一個工具,可以在代碼編譯期解析注解。注解處理器以Java代碼(或者編譯過的字節碼)作為輸入,生成.java文件作為輸出。

Android Gradle插件2.2版本發布后,Android 官方提供了annotationProcessor插件來代替android-apt,annotationProcessor同時支持 javac 和 jack 編譯方式,而android-apt只支持 javac 方式。
同時android-apt作者宣布不在維護,當然目前android-apt仍然可以正常運行

總體流程:自定義注解->自定義注解處理器(會用到javapoet)->注冊注解處理器(會用到auto-service)->編譯生成java代碼

這面我只是簡單的做了findViewId和onCliclk事件!

那我們開始說起:

image.png

如圖:
apt_annotation ,一個Java Library
主要是用來自定義注解

apt_library,一個Android Library
主要是用來寫調用的編譯時期生成的java代碼的工具類

apt_processor ,一個Java Library
主要是用來處理編譯時的注解操作

為什么要建立java Library呢 ?

原因AbstractProcessor不在Android SDK里面!要是不建立 Java Library 是調用不到的!在java jre中。

首先:在apt_annotation module 建立注解
//編譯時期注解,作用目標 域生明(類,接口,成員變量,類靜態變量)
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
@Retention(RetentionPolicy.CLASS)
@Target(METHOD)
public @interface OnClick {
    int[] value();
}

那注解寫好了:

再來:apt_processor module 建立編譯時注解處理的邏輯

在moudle中添加依賴

 implementation project(':apt_annotation')
 implementation 'com.google.auto.service:auto-service:1.0-rc2'
 implementation 'com.squareup:javapoet:1.11.1'
@AutoService(Processor.class)
public class BindViewProcessorByPoet extends AbstractProcessor {

    //寫入代碼會用到
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每個類,要生成的代碼集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成對應的 類文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

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

    private Map<TypeElement, List<CodeBlock.Builder>> findAndBuilderByTargets(RoundEnvironment env) {
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();

        // 遍歷帶對應注解的元素,就是某個View對象
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

            //感覺這里面應該是VariableElement
            BindViewCreatorByPoetHelper.parseBindView(element, builderMap);
        }

        // 遍歷帶對應注解的元素,就是某個方法
        for (Element element : env.getElementsAnnotatedWith(OnClick.class)) {
            BindViewCreatorByPoetHelper.parseListenerView(element, builderMap);
        }
        return builderMap;
    }

}

這面會實現4個方法:
init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具類Elements, Types 和 Filer
getSupportedAnnotationTypes:指定這個注解處理器是注冊給哪個注解的,這里說明是注解BindView和OnClick
getSupportedSourceVersion:指定使用的Java版本,通常這里返回SourceVersion.latestSupported()
process:可以在這里寫掃描、評估和處理注解的代碼,生成Java文件
所以說主要的還是

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每個類,要生成的代碼集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成對應的 類文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }

這一方法:
我們詳細看下
因為大都數代碼里面都是有注釋的:

private Map<TypeElement, List<CodeBlock.Builder>> findAndBuilderByTargets(RoundEnvironment env) {
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();

        // 遍歷帶對應注解的元素,就是某個View對象
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

            //感覺這里面應該是VariableElement
            BindViewCreatorByPoetHelper.parseBindView(element, builderMap);
        }

        // 遍歷帶對應注解的元素,就是某個方法
        for (Element element : env.getElementsAnnotatedWith(OnClick.class)) {
            BindViewCreatorByPoetHelper.parseListenerView(element, builderMap);
        }
        return builderMap;
    }

Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();
這一個集合進行存儲,key則是其實也就是關聯Actvity對象的Element,value則是寫入的代碼集合(一個類維護一個生成的代碼塊的集合)
然后分別對兩個注解添加代碼集合:

public class BindViewCreatorByPoetHelper {

    public static void parseBindView(Element element, Map<TypeElement, List<CodeBlock.Builder>> codeBuilderMap) {
        //獲取最外層的類名,具體實際就是關聯某個Activity對象Element
        //因為此時的element是VriableElement,所以拿到的Enclosing 就應該是Activity對象
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        // 這個view是哪個類 Class(android.widget.TextView)
        String viewType = element.asType().toString();
        // 注解的值,具體實際可能就是 R.id.xxx
        int value = element.getAnnotation(BindView.class).value();
        // 這個view對象名稱(比如TextView)
        String name = element.getSimpleName().toString();

        //創建代碼塊
        //$L是占位符,會把后面的 name 參數拼接到 $L 所在的地方
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", name);
        builder.add("($L)target.findViewById($L)", viewType, value);

        List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
        if (codeList == null) {
            codeList = new ArrayList<>();
            codeBuilderMap.put(classElement, codeList);
        }
        codeList.add(builder);
    }

    public static void parseListenerView(Element element, Map<TypeElement, List<CodeBlock.Builder>> codeBuilderMap) {
        //獲取最外層的類名,具體實際就是關聯某個Activity對象Element
        TypeElement classElement = (TypeElement) element.getEnclosingElement();

        List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
        if (codeList == null) {
            codeList = new ArrayList<>();
            codeBuilderMap.put(classElement, codeList);
        }

        //注解的值
        int[] annotationValue = element.getAnnotation(OnClick.class).value();

        //因為注解@Target是Method,所以這面拿到的就是方法名字的字符串
        String name = element.getSimpleName().toString();

        //創建代碼塊
        for (int value : annotationValue) {
            CodeBlock.Builder builder = CodeBlock.builder();
            builder.add("target.findViewById($L).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.$L(v); }})", value, name);
            codeList.add(builder);
        }
    }

    public static void writeBindView(TypeElement classElement, List<CodeBlock.Builder> codeList, Filer filer) {
        // enclosingElement ,暗指 某個Activity.
        // 先拿到 Activity 所在包名( cn.citytag.aptdemo.Main3Activity)
        String packageName = classElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));//(cn.citytag.aptdemo)
        // 再拿到Activity類名(Main3Activity))
        String className = classElement.getSimpleName().toString();

        //此元素定義的類型
        TypeName type = TypeName.get(classElement.asType());

        //if (type instanceof ParameterizedTypeName) {
        // type = ((ParameterizedTypeName) type).rawType;
        //}

        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBindingPoet");
        MethodSpec.Builder methodSpecBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(type, "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "source", Modifier.FINAL);
        for (CodeBlock.Builder codeBuilder : codeList) {
            //方法里面 ,代碼是什么
            methodSpecBuilder.addStatement(codeBuilder.build());
        }
        methodSpecBuilder.build();

        // 創建類 MainActivity_ViewBinding
        TypeSpec bindClass = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        try {
            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, bindClass).build();
            //將文件寫出
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
  if (codeList == null) {
          codeList = new ArrayList<>();
          codeBuilderMap.put(classElement, codeList);
   }

都會加以判斷是否存在此TypeElemen的key,在進行put元素!

這樣的話代碼集合添加完成之后再進行寫入,
還是這個代碼,每一個TypeElemen對應一個代碼塊集合進行寫入代碼;

   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每個類,要生成的代碼集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成對應的 類文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }
 public static void writeBindView(TypeElement classElement, List<CodeBlock.Builder> codeList, Filer filer) {
        //  classElement ,就是關聯的某個Activity
        // 先拿到 Activity 所在包名( cn.citytag.aptdemo.Main3Activity)
        String packageName = classElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));//(cn.citytag.aptdemo)
        // 再拿到Activity類名(Main3Activity))
        String className = classElement.getSimpleName().toString();

        //此元素定義的類型
        TypeName type = TypeName.get(classElement.asType());

        //if (type instanceof ParameterizedTypeName) {
        // type = ((ParameterizedTypeName) type).rawType;
        //}

        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBindingPoet");
        MethodSpec.Builder methodSpecBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(type, "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "source", Modifier.FINAL);
        for (CodeBlock.Builder codeBuilder : codeList) {
            //方法里面 ,代碼是什么
            methodSpecBuilder.addStatement(codeBuilder.build());
        }
        methodSpecBuilder.build();

        // 創建類 MainActivity_ViewBinding
        TypeSpec bindClass = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        try {
            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, bindClass).build();
            //將文件寫出
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
然后:apt_library module 建立Tools類
public class BindViewByPoetTools {
    public static void bind(Activity activity) {
        //獲取activity的decorView(根view)
        View view = activity.getWindow().getDecorView();
        bind(activity, view);
    }

    private static void bind(Object obj, View view) {
        String className = obj.getClass().getName();
        //找到該activity對應的Bind類的名字
        String generateClass = className + "_ViewBindingPoet";
        //然后調用Bind類的構造方法,從而完成activity里view的初始化
        try {
            Class.forName(generateClass).getConstructor(obj.getClass(), View.class).newInstance(obj, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

最后:app module 進行綁定注解,并調用Tools類!

在app module添加依賴

   implementation project(':apt_annotation')
   implementation project(':apt_library')
   annotationProcessor project(':apt_processor')
為什么沒用apt呢!gradle高版本就不用那么麻煩了!直接annotationProcessor這個就可以在編譯時處理注解了!
public class Main3Activity extends AppCompatActivity {
    @BindView(R.id.tv_one)
    TextView mTextViewOne;
    @BindView(R.id.tv_two)
    TextView mTextViewTwo;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        BindViewByPoetTools.bind(this);
        mTextViewOne.setText("one");
        mTextViewTwo.setText("two");
    }

    @OnClick({R.id.tv_one, R.id.tv_two})
    public void onBtn1Click(View v) {
        Toast.makeText(this, "", Toast.LENGTH_SHORT).show();
    }
}

最終:ReBuild as 則會生成如下代碼:
apt_java.png
package cn.citytag.aptdemo;

import android.view.View;

public class Main3Activity_ViewBindingPoet {
  public Main3Activity_ViewBindingPoet(final Main3Activity target, final View source) {
    target.mTextViewOne = (android.widget.TextView)target.findViewById(2131165325);
    target.mTextViewTwo = (android.widget.TextView)target.findViewById(2131165326);
    target.findViewById(2131165325).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
    target.findViewById(2131165326).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
  }
}
介紹下依賴庫auto-service:

auto-service的作用是向系統注冊processor(自定義注解處理器),
在javac編譯時,才會調用到我們這個自定義的注解處理器方法。

主要是自己建立我沒有試!這個具體我也不清楚!

在使用注解處理器需要先聲明,步驟:
1、需要在 processors 庫的 main 目錄下新建 resources 資源文件夾;
2、在 resources文件夾下建立 META-INF/services 目錄文件夾;
3、在 META-INF/services 目錄文件夾下創建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;
這樣聲明下來也太麻煩了?這就是用引入auto-service的原因。
通過auto-service中的@AutoService可以自動生成AutoService注解處理器是Google開發的,用來生成 META-INF/services/javax.annotation.processing.Processor 文件的

介紹下依賴庫 javapoet:

助于在編譯期間生成java代碼,要不自己StringBuilder拼接很麻煩!
https://github.com/square/javapoet

如果在as ReBuild的時候報這個問題:

錯誤: 編碼GBK的不可映射字符
在apt_processor gradle
加入下面代碼!

tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380