自定義註解處理器生成程式碼.md
自定義註解處理器生成程式碼
先上一段程式碼:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView tv;
@BindView(R.id.tv2)
TextView tv2;
@BindView(R.id.tv3)
TextView tv3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv.setText(" ViewBinder bindView success!!!");
}
}
我們經常在專案中使用 ButterKnife 來代替大量的 findViewById(),如上程式碼所示, 據我所知 ButterKnife 最新版本改用註解處理器來生成 findViewById 程式碼, 今天我就模仿 ButterKnife 造一個輪子分享給大家, 以及記錄下關於註解處理器的相關 API 以供查閱.
(我也是在百度了好多關於註解處理器的部落格, 並親自寫了Demo嘗試, 啃了好久官方 API 文件, 在此記錄共同學習, 希望這篇部落格能幫到正在看的你)
首先明確幾個概念:
-
第一: 註解處理器是由
annotationProcessor "com.jakewharton:butterknife-compiler:8.6.0"
在 build.gradle 中配置的內容, 也可以annotationProcessor project(':AnnotationProcessor')
這樣配置本地的工程. -
第二: 註解處理器在程式碼編譯時執行執行在作業系統上, 不會隨工程打入 APK 中(所以註解處理器工程需要是一個 Java Library).
-
第三: 註解處理器一般用來生成程式碼, 實際上也並非很高深, 只是一些陌生的 api 以及通過這些 api 寫檔案而已.
說下我的思路, 首先通過註解處理器生成程式碼(ViewBinding.java), 這些程式碼處理了 findViewById 的操作, 然後定義一個工具類, 通過反射方式例項化這些生成的程式碼, 並把 Activity 通過構造方法傳給 ViewBinding 以實現 findViewById
首先新建 AnnotationProcessor Module(Java Library), 需要在build.gradle中配置以下程式碼:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.google.auto.service:auto-service:1.0-rc3'//必須引用此工程才能正常使用註解處理器
api project(':AnnoLib')//Bind.java 註解庫
}
然後需要定義 MyProcessor 繼承 AbstractProcessor, 並實現process方法, 重寫init() getSupportedAnnotationTypes() getSupportedSourceVersion() 三個方法, 程式碼如下所示.
- getSupportedSourceVersion() 配置 JDK 版本, 寫死
SourceVersion.latestSupported()
就行 - getSupportedAnnotationTypes() 相當於新增一個註解過濾器, 只有配置了的註解才會走 process() 方法
- init() 方法用於初始化註解處理器, 需要在此處獲取一些工具以供之後在 process() 方法執行時使用.
- process() 方法需要在此處解析註解, 進行建立檔案的操作, 對於引數 RoundEnvironment 我也是知之甚少, 請參見程式碼中的註釋
- Bind 類在另外一個 Java Moudle 中, 是一個註解:
package annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Bind{
int value();
}
- createFile()方法是生成程式碼的主要邏輯, 生成程式碼的位置與 使用 Bind 註解 的Activity同一包下 (同包下非 private 變數可以直接訪問), 貼下要生成的目的碼:
package com.eshel.annotationprocessor;
//Auto generated by apt,do not modify!!
public class MainActivity_ViewBinding {
public MainActivity_ViewBinding(MainActivity activity){
activity.tv = activity.findViewById(2131165325);
activity.tv2 = activity.findViewById(2131165325);
activity.tv3 = activity.findViewById(2131165325);
}
}
createFile() 方法就是為了生成上述程式碼(id 是通過註解拿到的), 假設 MainActivity_ViewBinding 已經生成了, 只需要想辦法建立 MainActivity_ViewBinding 物件就可以實現對 MainActivity 中的 View 進行賦值.
MyProcessor 程式碼如下:
//必須有此註解
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor{
private Filer mFiler;
private Messager mMessager;
private Elements mElementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//用於生成原始碼檔案
mFiler = processingEnvironment.getFiler();
//用於列印日誌 (日誌如何檢視下邊會說)
mMessager = processingEnvironment.getMessager();
//工具類, 從 Element 物件中獲取 包名類名欄位名等
mElementUtils = processingEnvironment.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//新增 MyProcessor要處理的註解
Set<String> anno = new HashSet<>();
anno.add(Bind.class.getCanonicalName());
return anno;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//得到所有Bind註解元素(所有類中的@Bind)
//比如我在 MainActivity 和 SecondActivity 中使用 @Bind 註解 View , 分別有 3個 和 2個 成員變數被 @Bind註解
//那麼 elements 的 size 將是 5
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
note("elementsSize: "+elements.size());
//所以需要將所有 Element 按照(包名+類名) MainActivity 和 SecondActivity 分類
//即分類後 elementMap 有兩個集合, 集合 size() 分別為 3 和 2
HashMap<String, List<Element>> elementMap = new HashMap<>();
for (Element element : elements) {
//獲取包名+類名
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String enclosingName = enclosingElement.getQualifiedName().toString();
note(String.format("enclosindClass = %s", enclosingName));
//日誌列印結果 注: enclosindClass = com.eshel.annotationprocessor.MainActivity
//根據包名類名對所有註解進行分類
List<Element> elementList = elementMap.get(enclosingName);
if(elementList == null) {
elementList = new ArrayList<>();
elementMap.put(enclosingName, elementList);
}
elementList.add(element);
}
for (Map.Entry<String, List<Element>> entry : elementMap.entrySet()) {
//根據Activity中帶有 Bind註解的欄位建立 java 檔案 java檔案儲存在 app/build/generated/source/apt/debug或release 下
createFile(entry.getKey(), entry.getValue());
}
//必須返回 true 才能生成檔案
return true;
}
private void note(String msg) {
//列印日誌
mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
}
private void createFile(String key, List<Element> value) {
if(value.size() == 0)
return;
try {
JavaFileObject jfo = mFiler.createSourceFile(key + "_ViewBinding", new Element[]{});
Writer writer = jfo.openWriter();
//獲取包名
TypeElement enclosingElement = (TypeElement) value.get(0).getEnclosingElement();
Name pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName();
//獲取類名
String className = enclosingElement.getSimpleName().toString();
StringBuilder builder = new StringBuilder();
builder.append("package ").append(pkName).append(";\n\n");
builder.append("//Auto generated by apt,do not modify!!\n\n");
builder.append("public class ").append(className).append("_ViewBinding").append(" { \n\n");
builder.append("\tpublic ").append(className).append("_ViewBinding(").append(className).append(" activity){ \n");
for (Element element : value) {
VariableElement bindViewElement = (VariableElement) element;
String bindViewFiledName = bindViewElement.getSimpleName().toString();
String bindViewFiledClassType = bindViewElement.asType().toString();
Bind bindView = element.getAnnotation(Bind.class);
int id = bindView.value();
note(String.format(Locale.getDefault(), "%s %s = %d", bindViewFiledClassType, bindViewFiledName, id));
String info = String.format(Locale.getDefault(),
"\t\tactivity.%s = activity.findViewById(%d);\n", bindViewFiledName, id);
builder.append(info);
}
builder.append("\t}\n");
builder.append("}");
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然後定義 工具類 ViewBinder , 通過反射的方式建立 MainActivity_ViewBinding 物件:(ViewBinder 和 @Bind 註解都需要編入 apk 中, 所以需要在 主moudle 中進行依賴引用)
public class ViewBinder {
//由於我的 ViewBinder 所在工程是一個 Java工程, 所以沒法使用 Activity, 故使用 class.isInstance() 來判斷是否是 activity
public static void bind(Object activity){
if(activity == null)
return;
try {
Class<?> clazz = Class.forName("android.app.Activity");
if(clazz != null){
if(clazz.isInstance(activity)){
Class<?> activityClass = activity.getClass();
Class<?> ViewBindingClass = Class.forName(activityClass.getCanonicalName() + "_ViewBinding");
Constructor<?> constructor = ViewBindingClass.getConstructor(activityClass);
Object ViewBinding = constructor.newInstance(activity);
}
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
最後在 MainActivity 中使用(當然還少了一步使用外掛根據xml生成成員變數, 這個暫時不知道如何實現):
public class MainActivity extends AppCompatActivity {
@Bind(R.id.tv)
TextView tv;
@Bind(R.id.tv)
TextView tv2;
@Bind(R.id.tv)
TextView tv3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);
tv.setText(" ViewBinder bindView success!!!");
}
}
在主工程app下的 build.gradle 中配置:
dependencies {
annotationProcessor project(':AnnotationProcessor')//引用註解處理器
api project(':AnnoLib')//Bind註解 和 ViewBinder 所在的庫
}
注意: 除錯時 只有先clean Project , 再點小錘子按鈕編譯 或者 直接點選 rebuild Project, 才會執行註解處理器生成程式碼
效果圖如下, 成功給tv變數進行了賦值:
感興趣的可以到 Github 下載程式碼跑下, 有什麼問題請告知我, 大家共同學習!!