Android APT(Annotation Processing Tool) 實踐

JakePrim發表於2019-03-04

微信公眾號:Android研究院 關注可瞭解更多的Android知識,專注於移動領域。問題或建議,請公眾號留言; 如果你覺得文章對你有幫助,歡迎讚賞[^1]

[TOC]

APT 詳解

apt為何如此重要呢?現今越來越多的第三方庫使用了apt技術,Dagger2、ButterKnife、ARouter等,在編譯時根據annotation生成相關的程式碼邏輯,動態的生成Java class檔案給開發帶來了很大的便利。

首先要懂得annotation (註解)相關的基礎知識。

APT 的全稱為:Annotation Processing Tool 可以解釋為註解處理器, 它對原始碼檔案進行檢測找出其中的Annotation,使用指定的Annotation進行額外的處理。 Annotation處理器在處理Annotation時可以根據原始檔中的Annotation生成額外的原始檔和其它的檔案(檔案具體內容由Annotation處理器的編寫者決定),APT還會編譯生成的原始檔和原來的原始檔,將它們一起生成class檔案。

下面我們通過一個例子來講解apt實現的原理。本例實現gradle版本是3.0.1

  1. 新建一個名為annotationlib的java library,注意是Java library,不能是Android的library
@Target(ElementType.TYPE) // 註解作用在類上
@Retention(RetentionPolicy.CLASS)
public @interface Test {
    String path();
}
複製程式碼

build.gradle 配置

apply plugin: 'java-library'

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

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
複製程式碼
  1. 建立一個Java library 名為 TestCompiler

注意gradle的配置

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotationlib')
    implementation 'com.google.auto.service:auto-servic:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
複製程式碼

com.google.auto.service:auto-service:1.0-rc4 是註冊Processor的

com.squareup:javapoet:1.11.1 是生成class檔案的第三方庫

定義Processor類

@AutoService(Processor.class)
//指定編譯的Java版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//指定處理的註解
@SupportedAnnotationTypes({"demo.prim.com.annotationlib.Test"})
public class MyClass extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 建立方法
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC,Modifier.STATIC)
                .addParameter(String[].class,"args")
                .addStatement("$T.out.println($S)",System.class,"Hello JavaPoet")
                .build();
        //建立類
        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWord")
                .addModifiers(Modifier.PUBLIC,Modifier.FINAL)
                .addMethod(main)
                .build();

        //建立Java檔案
        JavaFile file = JavaFile.builder("com.ecample.test", typeSpec)
                .build();

        try {
            file.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }
}

複製程式碼

上述程式碼會生成如下的類

public final class HelloWord {
  public static void main(String[] args) {
    System.out.println("Hello JavaPoet");
  }
}
複製程式碼
  1. 在app中使用 這裡需要注意如果gradle版本低於3.0.0 則需要如下配置
配置專案根目錄的build.gradle
dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
複製程式碼
配置app的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
//...
dependencies {
    //..
    compile project(':annotation')
    apt project(':compiler')
}
複製程式碼

編譯使用

在隨意一個類新增@Test註解

@Test(path = "main")
public class MainActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
   }
}
複製程式碼

點選Android Studio的Rebuild Project 會生成如下檔案:

image.png

這時便可以呼叫HelloWord類了。

APT 實戰

通過apt來簡化 textView = findViewById(R.id.textView);

一般的可以這樣寫:

public class ManagerFindByMainActivity {

    public void findById(MainActivity activity) {
        activity.textView = activity.findViewById(R.id.textView);
    }
}
複製程式碼

上面一段程式碼,這樣寫和之前的沒有區別,反而每有一個Activity都要新建一個類去找到ID。那麼我們是否可以通過apt來動態生成這個類呢?

答案當然是可以的。

  1. 在annotation lib 中建立兩個註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {

}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BYView {
    int value() default 0;
}
複製程式碼
  1. 在compiler lib 中建立processor

gradle 配置:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'
    implementation project(':lib-annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
複製程式碼

核心程式碼實現:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({Constance.DIACTIVITY})
public class ByProcessor extends AbstractProcessor {

    private Elements elementUtils;

    private Types typeUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnvironment.getElementUtils();
        typeUtils = processingEnvironment.getTypeUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set != null) {
            Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(DIActivity.class);
            if (elementsAnnotatedWith != null) {//獲取設定DIActivity 註解的節點
                //判斷註解的節點是否為Activity
                TypeElement typeElement = elementUtils.getTypeElement(Constance.Activity);
                for (Element element : elementsAnnotatedWith) {

                    TypeMirror typeMirror = element.asType();//獲取註解節點的類的資訊
                    DIActivity annotation = element.getAnnotation(DIActivity.class);//獲取註解的資訊
                    if (typeUtils.isSubtype(typeMirror, typeElement.asType())) {//註解在Activity的類上面

                        TypeElement classElement = (TypeElement) element;//獲取節點的具體型別

                        //建立引數  Map<String,Class<? extends IRouteGroup>>> routes
                        ParameterSpec altlas = ParameterSpec
                                .builder(ClassName.get(typeMirror), "activity")//引數名
                                .build();

                        //建立方法
                        MethodSpec.Builder method = MethodSpec.methodBuilder
                                ("findById")
//                                .addAnnotation(Override.class)
                                .addModifiers(PUBLIC, STATIC)
                                .returns(TypeName.VOID)
                                .addParameter(altlas);

                        //建立函式體

//獲取TypeElement的所有成員變數和成員方法
                        List<? extends Element> allMembers = elementUtils.getAllMembers(classElement);//??

                        //遍歷成員變數 
                        for (Element member : allMembers) {
                        //找到被BYView註解的成員變數
                            BYView byView = member.getAnnotation(BYView.class);
                            if (byView == null) {
                                continue;
                            }
                            //構建函式體
                            method.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",
                                    member.getSimpleName(),//註解節點變數的名稱
                                    ClassName.get(member.asType()).toString(),//註解節點變數的型別
                                    byView.value()));//註解的值
                        }

                        //建立類
                        TypeSpec typeSpec = TypeSpec.classBuilder("ManagerFindBy" + element.getSimpleName())
                                .addModifiers(PUBLIC, FINAL)//作用域
                                .addMethod(method.build())//新增方法
                                .build();

                        //建立Javaclass 檔案

                        JavaFile javaFile = JavaFile.builder("com.prim.find.by", typeSpec).build();

                        try {
                            javaFile.writeTo(processingEnv.getFiler());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }

                    } else {
                        throw new IllegalArgumentException("@DIActivity must of Activity");
                    }
                }

            }
            return true;
        }
        return false;
    }
}

複製程式碼
  1. Activity 中使用註解
implementation project(':lib_annotation')

annotationProcessor project(':lib_compiler')
複製程式碼
@DIActivity
public class MainActivity extends AppCompatActivity {

    @BYView(R.id.textView)
    public TextView textView;

    @BYView(R.id.textView1)
    public TextView textView1;

    @BYView(R.id.textView2)
    public TextView textView2;

    @BYView(R.id.button)
    public Button button;
}

@DIActivity
public class SencodActivity extends AppCompatActivity {

    @BYView(R.id.sencodText)
    public TextView sencodText;
}

複製程式碼

然後RebuildProject,就會得到:

image.png

  1. 使用生成的類
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ManagerFindByMainActivity.findById(this);
        textView.setText("我是APT找到的");
        textView1.setText("我是APT找到的 --> 1");
        textView2.setText("我是APT找到的 --> 2");
        button.setText("我被APT找到我要跳轉");
    }

    public void click(View view) {
        Intent intent = new Intent(this, SencodActivity.class);
        startActivity(intent);
    }
複製程式碼

image.png

專案地址

常用方法

常用Element子類
  • TypeElement:類

  • ExecutableElement:成員方法

  • VariableElement:成員變數

  • 通過包名和類名獲取TypeName TypeName targetClassName = ClassName.get(“PackageName”, “ClassName”);

  • 通過Element獲取TypeName TypeName type = TypeName.get(element.asType());

  • 獲取TypeElement的包名 String packageName = processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();

  • 獲取TypeElement的所有成員變數和成員方法 List<? extends Element> members = processingEnv.getElementUtils().getAllMembers(typeElement);

參考文章

Android的元件化專題: 元件化配置

APT實戰

路由框架原理

模組間的業務通訊

下面的是我的公眾號二維碼,歡迎關注。

Android研究院

###讚賞 如果你覺得到Android研究院對你有幫助,歡迎讚賞,有你的支援,Android研究院一定會越來越好!

Android APT(Annotation Processing Tool) 實踐

相關文章