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事件!

那我們開始說起:


如圖:
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 則會生成如下代碼:
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'
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章