android 編譯時注解


前言

我們經常使用的一些第三方框架,比如:butterknife,通過一行注解就可以實現View 的“自動賦值”。

那么,這其中的原理是什么呢?

為了帶大家更好的深入了解,本文將打造一個簡單的 Demo,來說明這其中的原理。

Demo 雖然簡單,但是完全按照 butterknife 實現的方式和原理打造。

實現思路

我們先看 Demo 的效果:

public class MainActivity extends AppCompatActivity { // 被注解的 View @BindView(R.id.tv) TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 為 tv 賦值 InjectHelper.inject(this); tv.setText("I am injected"); } } 

代碼非常簡單,分為兩步:

  1. 通過 @BindView(R.id.tv) 指定被注解 View 的 Id 值;
  2. InjectHelper.inject(this);,具體為 tv 賦值的方法。

@BindView 注解沒什么好解釋的,我們重點看下 InjectHelper.inject(this); 的實現:

public class InjectHelper { public static void inject(Activity host) { // 1、 String classFullName = host.getClass().getName() + "$$ViewInjector"; try { // 2、 Class proxy = Class.forName(classFullName); // 3、 Constructor constructor = proxy.getConstructor(host.getClass()) // 4、 constructor.newInstance(host); } catch (Exception e) { e.printStackTrace(); } } } 
  1. 獲得 View 所在 Activity 的類路徑,然后拼接一個字符串“$$ViewInjector”。這個是編譯時動態生成的 Class 的完整路徑,也就是我們需要實現的,同時也是最關鍵的部分;
  2. 根據 Class 路徑,使用 Class.forName(classFullName) 生成 Class 對象;
  3. 得到 Class 的構造函數 constructor 對象;
  4. 使用 constructor.newInstance(host) new 出一個對象,這會執行對象的構造方法,方法內部是我們為 MainActivity 的 tv 賦值的地方。

我們先看一個生成好的 “XXX$$ViewInjector” 示例:

public class MainActivity$$ViewInjector { public MainActivity$$ViewInjector(MainActivity activity) { activity.tv = (TextView)activity.findViewById(2131427422); } } 

到這里,我們大概知道了最關鍵的地方,就是如何生成 “XXX$$ViewInjector” 這個類了。

APT 實現方案

APT 是一種處理注解的工具,它對源代碼文件進行檢測找出其中的 Annotation,再根據注解自動生成代碼。

實現方案,分為兩種:

  • android-apt,個人開發者提供,現在已經停止維護,作者推薦大家使用官方提供的解決方案。
  • Android Gradle 插件:annotationProcessor,由官方提供支持。

如何由 android-apt 切換到 annotationProcessor,可以參考這里

annotationProcessor 配置起來比較簡單,另外由於是官方支持的,所以我們選擇第二種方案。

實現步驟

第一步:定義注解 @BindView

@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int value(); } 

沒什么好解釋的~

第二步:實現 AbstractProcessor

1、新建一個 Java Library,引入兩個第三方庫:

dependencies {
    // ... compile 'com.google.auto.service:auto-service:1.0-rc2' compile 'com.squareup:javapoet:1.7.0' } 
  • auto-service:Google 公司出品,用於自動為 JAVA Processor 生成 META-INF 信息。

    如果你定義了一個 Processor:

      package foo.bar; import javax.annotation.processing.Processor; @AutoService(Processor.class) final class MyProcessor implements Processor { // … } 

    auto-service 會在編譯目錄生成一個文件,路徑是:META-INF/services/javax.annotation.processing.Processor,文件內容為:

      foo.bar.MyProcessor 
  • javapoet:大名鼎鼎的 squareup 公司出品,封裝了一套生成 .java 源文件的 API。

    以 HelloWorld 類為例:

      package com.example.helloworld; public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } } 

    上面的代碼就是使用javapoet用下面的代碼進行生成的:

      MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out); 

    更多關於 javapoet 的使用,可以參考這里

2、繼承 AbstractProcessor,配置相關信息:

@AutoService(Processor.class) @SupportedAnnotationTypes({"com.example.BindView"}) @SupportedSourceVersion(SourceVersion.RELEASE_7) public class ViewInjectProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //... } } 
  • @AutoService(Processor.class),生成 META-INF 信息;
  • @SupportedAnnotationTypes({"com.example.BindView"}),聲明 Processor 處理的注解,注意這是一個數組,表示可以處理多個注解;
  • @SupportedSourceVersion(SourceVersion.RELEASE_7),聲明支持的源碼版本

補充說明一下,@SupportedAnnotationTypes 和 @SupportedSourceVersion 必須聲明,否則會報錯。具體原因看大家看一下源碼就明白了,這里不做過多解釋。

除了注解方式,你也可以通過重寫下面兩個函數實現:

@AutoService(Processor.class) public class ViewInjectProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotationTypes = new HashSet<>(); annotationTypes.add("com.example.BindView"); return annotationTypes; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7; } } 

3、實現 AbstractProcessor 的 process() 方法:

@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // 1、 collectInfo(roundEnvironment); // 2、 writeToFile(); return true; } 

process() 方法的實現,分為兩個步驟:

  1. 收集 Class 內的所有被 @BindView 注解的成員變量;
  2. 根據上一步收集的內容,生成 .java 源文件。

為此,我們聲明了兩個 Map,用於保存 collectInfo() 收集的相關信息,Map 的 key 為類的全路徑:

// 存放同一個Class下的所有注解信息 Map<String, List<VariableInfo>> classMap = new HashMap<>(); // 存放Class對應的信息:TypeElement Map<String, TypeElement> classTypeElement = new HashMap<>(); 

VariableInfo 是一個簡單的類,用於保存被注解 View 對應的一些信息:

public class VariableInfo { // 被注解 View 的 Id 值 int viewId; // 被注解 View 的信息:變量名稱、類型 VariableElement variableElement; // ... } 

4、實現 collectInfo() 方法:

void collectInfo(RoundEnvironment roundEnvironment) { classMap.clear(); classTypeElement.clear(); Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class); for (Element element : elements) { // 獲取 BindView 注解的值 int viewId = element.getAnnotation(BindView.class).value(); // 代表被注解的元素 VariableElement variableElement = (VariableElement) element; // 備注解元素所在的Class TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); // Class的完整路徑 String classFullName = typeElement.getQualifiedName().toString(); // 收集Class中所有被注解的元素 List<VariableInfo> variableList = classMap.get(classFullName); if (variableList == null) { variableList = new ArrayList<>(); classMap.put(classFullName, variableList); // 保存Class對應要素(名稱、完整路徑等) classTypeElement.put(classFullName, typeElement); } VariableInfo variableInfo = new VariableInfo(); variableInfo.setVariableElement(variableElement); variableInfo.setViewId(viewId); variableList.add(variableInfo); } } 

代碼的注釋已經很完整,這里不再說明了。

這里提一下 Element 這個元素,它的子類我們用到了以下兩個:

Element
- VariableElement:代表變量
- TypeElement:代表 class 

5、實現 writeToFile() 方法:

void writeToFile() {
    try { for (String classFullName : classMap.keySet()) { TypeElement typeElement = classTypeElement.get(classFullName); // 使用構造函數綁定數據 MethodSpec.Builder constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build()); List<VariableInfo> variableList = classMap.get(classFullName); for (VariableInfo variableInfo : variableList) { VariableElement variableElement = variableInfo.getVariableElement(); // 變量名稱(比如:TextView tv 的 tv) String variableName = variableElement.getSimpleName().toString(); // 變量類型的完整類路徑(比如:android.widget.TextView) String variableFullName = variableElement.asType().toString(); // 在構造方法中增加賦值語句,例如:activity.tv = (android.widget.TextView)activity.findViewById(215334); constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId()); } // 構建Class TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector") .addModifiers(Modifier.PUBLIC) .addMethod(constructor.build()) .build(); // 與目標Class放在同一個包下,解決Class屬性的可訪問性 String packageFullName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec) .build(); // 生成class文件 javaFile.writeTo(filer); } } catch (Exception ex) { ex.printStackTrace(); } } 

這段代碼主要就是通過 javapoet 來生成 .java 源文件,大家如果感覺陌生,建議先看一下 javapoet 的使用,參考文章: JavaPoet的基本使用

當然,你完全可以通過拼接字符串來生成 .java 源文件的內容。

還記得文章開頭提到的 “XXX$$ViewInjector” 嗎?writeToFile() 方法,就是為了生成這個 .java 源文件的。

第三步:使用 annotationProcessor

在 app 的 build.gradle 文件中,使用 APT:

dependencies {
    // ... annotationProcessor project(':lib-compiler') } 

lib-compiler:為第二步新建的 Java Library。

第四步:Activity 中使用 @BindView

文章開始已經演示過相關代碼了,這里不再貼了。

一共分為以下兩步:

  1. @BindView 注解相關 View;
  2. 調用 InjectHelper.inject(this) 方法。

然后,你嘗試去編譯項目,會發現 APT 為我們自動生成了 XXX$$ViewInjector.class 文件。

你可以在 app/build/intermediates/classes/debug(release、其他 buildType) 下對應的包中找到。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM