ButterKnife原理分析(二)註解的處理
本文同步發表於我的微信公眾號 hesong,掃一掃文章底部的二維碼或在微信搜尋 hesong 即可關注。
上一篇我們講解了ButterKnife的設計思想,理解了ButterKnife繫結相關原始碼的實現邏輯。但是它是怎麼通過註解的方式生成的那些邏輯程式碼,這才是最讓我們迫切想知道,因此在這篇,我將說說ButterKnife中註解處理的原理。本篇主要有以下內容:
1. 註解Annotation
2. 註解處理器AbstractProcessor
3. AutoService註冊註解處理器
4. JavaPoet生成Java程式碼
5. Element元素相關
6. 編譯時註解解析
7. 例子專案
註解(Annotation)
註解(Annotation)在Java中已經是很普遍的使用了,它其實就是一種標記資訊,然後程式在編譯或者執行的時候可以讀取這個標記資訊,去執行特定的邏輯,比如@BindView(R.id.tv_text) TextView tvText,程式在編譯時會讀取到這個@BindView註解,解析出它的值R.id.tv_text,再根據它註解的這個tvText,就可以生成類似tvText = (TextView)findViewById(R.id.tv_text);的功能程式碼。
註解按生命週期可以分為
- RetentionPolicy.SOURCE(原始碼註解),只在原始碼中存在,在編譯時會被丟棄,通常用於檢查性的操作,如@Override。
- RetentionPolicy.CLASS(編譯時註解),在編譯後的class檔案中依然存在,通常用於編譯時處理,如ButterKnife的@BindView。
- RetentionPolicy.RUNTIME(執行時註解),不僅在編譯後的class檔案中存在,在被jvm虛擬機器載入之後,仍然存在,通常用於執行時處理,如Retrofit的@Get。
同時註解按使用的物件可以分為
- ElementType.TYPE(型別註解),標記在介面、類、列舉上。
- ElementType.FIELD(屬性註解),標記在屬性欄位上。
- ElementType.METHOD(方法註解),標記在方法上。
- ElementType.PARAMETER(方法引數註解),標記在方法引數上。
- ElementType.CONSTRUCTOR(構造方法註解),標記在構造方法上。
- ElementType.LOCAL_VARIABLE(本地變數註解),標記在本地變數上。
- ElementType.ANNOTATION_TYPE(註解的註解),標記在註解上。
- ElementType.PACKAGE(包註解),標記在包上。
- ElementType.TYPE_PARAMETER(型別引數註解,Java1.8加入),標記型別引數上。
- ElementType.TYPE_USE(型別使用註解,Java1.8加入),標記在類的使用上。
我們首先來認識ButterKnife的一個自定義屬性註解@BindView
/**
* 作用於View的註解,如@BindView(R.id.text) TextView tvText
*
* @Retention(RetentionPolicy.CLASS) 表示生命週期到類的編譯時期
* @Target(ElementType.FIELD) 表示註解作用在欄位上
*
* Created by Administrator on 2017/12/31 0031.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
@android.support.annotation.IdRes
int value();
}
它可以保留在類編譯之後,使用場景是作用在屬性上
關於註解的詳細介紹檢視Java註解基礎概念總結
註解處理器AbstractProcessor
註解只是一種標記資訊,所以需要我們自己去處理註解,註解的處理有編譯時註解處理和執行時註解處理。執行時註解,我們可以通過反射獲取註解資訊,進而進行相應處理。而編譯時註解就需要使用註解處理器(Annotation Processor)進行處理。那什麼是註解處理器?
註解處理器是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這些生成的Java程式碼是在新生成的.java檔案中,所以你不能修改已經存在的Java類,例如向已有的類中新增方法。這些生成的Java檔案,會同其他普通的手動編寫的Java原始碼一樣被javac編譯。
要實現一個註解處理器需要繼承AbstractProcessor,並重寫它的4個方法,同時必須要有一個無參的構造方法,以便註解工具能夠對它進行初始化。
public class ViewBindProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//提供給註解處理器使用的工具類
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//新增需要處理的註解
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
//指定使用的Java版本
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//這裡實現註解的處理,重點方法
return false;
}
}
- init,會被註解處理工具呼叫,引數ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
- getSupportedAnnotationTypes(),指定註解處理器要處理哪些註解,返回一個字串集合,包含要處理註解的全名。
- getSupportedSourceVersion, 指定使用的Java版本,通常這裡返回SourceVersion.latestSupported()
- process,相當於每個處理器的main函式,在這裡可以做掃描、評估和處理註解程式碼的操作,以及生成Java檔案。
那麼init方法中ProcessingEnvironment提供的Elements,Types,Filer,Messager 等是做什麼用的呢?
- Elements:用來處理程式元素的工具類
- Types:用來處理型別資料的工具類
- Filer:用來給註解處理器建立檔案
- Messager:用來給註解處理器報告錯誤,警告,提示等訊息。
AutoService註冊註解處理器
以前要註冊註解處理器是要在module的META-INF目錄下新建services目錄,並建立一個名為javax.annotation.processing.Processor的檔案,然後在檔案中寫入要註冊的註解處理器的全名,例如在javax.annotation.processing.Processor的檔案中加上
com.pinery.compile.ViewBindProcessor
來註冊ViewBindProcessor註解處理器。
後來Google推出了通過新增AutoService註解庫來實現註解處理器的註冊,通過在你的註解處理器上加上@AutoService(Processor.class)註解,即可在編譯時生成 META-INF/services/javax.annotation.processing.Processor 檔案。
配置AutoService需要在工程的build.gradle中新增
JavaPoet生成Java程式碼
JavaPoet是Square公司出品的生成Java原始檔庫,正如其名,會寫Java程式碼的詩人,使用它的一系列API就可以很方便的生成java原始碼了。
JavaPoet中有幾個常用的類:
- MethodSpec,代表一個建構函式或方法宣告。
- TypeSpec,代表一個類,介面,或者列舉宣告。
- FieldSpec,代表一個成員變數,一個欄位宣告。
- JavaFile,包含一個頂級類的Java檔案。
這是一個計算從1到100相加的方法
public static int caculateNum() {
int result = 0;
for(int i = 1; i < 100; i++) {
result = result + i;
}
return result;
}
我們用MethodSpec實現這個方法宣告
MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(int.class)
.addStatement("int result = 0")
.beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
.addStatement("result = result $L i", "+")
.endControlFlow()
.addStatement("return result")
.build();
可以發現,通過addModifiers新增方法修飾符,returns來定義方法的返回值型別,addStatement來新增方法中的一行語句,它會處理分號和換行,beginControlFlow和endControlFlow構成一個封閉的控制語段,適用於if,for,while等。
-
例如
addStatement("$T.out.println($S)", System.class, "Hello World"))
這是定義的一個屬性
private final String name = "Pinery";
我們用MethodSpec實現這個方法宣告
FieldSpec nameField = FieldSpec.builder(String.class, "name")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("$S", "Pinery")
.build();
下面是一個類的定義
public class MyClass{
private final String name = "Pinery";
public static int caculateNum() {
int result = 0;
for(int i = 1; i < 100; i++) {
result = result + i;
}
return result;
}
}
我們用TypeSpec實現這個方法宣告
TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
.addModifiers(Modifier.PUBLIC)
.addField(nameField)
.addMethod(caculateMethod)
.build();
通過TypeSpec添加了屬性實現和方法實現,其他常用的還有
- addTypeVariable,新增泛型宣告
- addSuperinterface,新增介面實現
- addJavadoc,添加註釋
- interfaceBuilder,生成一個介面
等,詳細使用可以參考JavaPoet官方文件
Element元素相關
註解處理工具掃描java原始檔,原始碼中的每一部分都是程式中的Element元素,如包,類,方法,欄位等。每一個元素代表一個靜態的,語言級別的結構。原始碼其實是一種結構化的文字(例如json文字就是一種結構化的文字),因此需要對它進行解析,解析的話,解析器會解析某些資訊代表某些結構,例如原始碼中的類宣告資訊代表TypeElement型別元素,方法宣告資訊代表代表ExecutableElement型別元素。有了這些結構,就能完整的表示整個原始碼資訊了。
Element元素分為以下型別:
- ExecutableElement: 可執行元素,包括類或介面的方法、構造方法或初始化程式
- PackageElement: 包元素,提供對有關包及其成員的資訊的訪問
- TypeElement: 類或介面元素,提供對有關型別及其成員的資訊的訪問
- TypeParameterElement: 表示一般類、介面、方法或構造方法元素的形式型別引數,型別引數宣告一個 TypeVariable
- VariableElement: 表示一個欄位、enum常量、方法或構造方法引數、區域性變數或異常引數。
Element元素還有個asType()可以獲取元素型別TypeMirror,TypeMirror有以下具體型別:
- ArrayType: 表示一個數組型別。多維陣列型別被表示為元件型別也是陣列型別的陣列型別。
- DeclaredType: 表示某一宣告型別,是一個類 (class) 型別或介面 (interface) 型別。這包括引數化的型別(比如 java.util.Set)和原始型別。
- ErrorType: 表示無法正常建模的類或介面型別。這可能是處理錯誤的結果。大多數對於派生於這種型別(比如其成員或其超型別)的資訊的查詢通常不會返回有意義的結果。
- ExecutableType: 表示 executable 的型別。executable 是一個方法、構造方法或初始化程式。
- NoType: 在實際型別不適合的地方使用的偽型別。NoType 的種類有:
- VOID:對應於關鍵字 void。
- PACKAGE:包元素的偽型別。
- NONE:用於實際型別不適合的其他情況中;例如,java.lang.Object 的超類。
- NullType: 表示 null 型別。此類表示式 null 的型別。
- PrimitiveType: 表示一個基本型別。這些型別包括 boolean、byte、short、int、long、char、float 和 double。
- ReferenceType: 表示一個引用型別。這些型別包括類和介面型別、陣列型別、型別變數和 null 型別。
- TypeVariable: 表示一個型別變數。型別變數可由某一型別、方法或構造方法的型別引數顯式宣告。
- WildcardType: 表示萬用字元型別引數。
編譯時註解解析
有了上面知識點的瞭解之後,下面就可以進行編譯時的註解解析了,需要以下幾個步驟:
1. 定義註解
2. 定義一個繼承自AbstractProcessor的註解處理器,重寫它4個方法中
3. 使用AutoService註冊自定義的註解處理器
4. 實現process方法,在這裡處理註解
5. 處理所有註解,得到TypeElement和註解資訊等資訊
6. 使用JavaPoet生成新的Java類
在annotations的moudle中定義一個註解
/**
* 作用於View的註解,如@BindView(R.id.text) TextView tvText
*
* @Retention(RetentionPolicy.CLASS) 表示生命週期到類的編譯時期
* @Target(ElementType.FIELD) 表示註解作用在欄位上
*
* Created by Administrator on 2017/12/31 0031.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
@android.support.annotation.IdRes
int value();
}
在compile的moudle中定義一個ViewBindProcessor註解處理器
/**
* 自定義的AbstractProcessor,用於編譯時處理註解
*/
@AutoService(Processor.class) //新增AutoService註解,自動註冊ViewBindProcessor註解處理器
public class ViewBindProcessor extends AbstractProcessor{
private Map<TypeElement, List<ViewBindInfo>> bindMap = new HashMap<>();
//用來處理型別資料的工具類
private Types typeUtils;
//用來處理程式元素的工具類
private Elements elementUtils;
//用來給註解處理器建立檔案
private Filer filer;
//用來給註解處理器報告錯誤,警告,提示等訊息。
private Messager messager;
/**
* 會被註解處理工具呼叫,引數ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
/**
* 指定註解處理器是註冊給那一個註解的,它是一個字串的集合,意味著可以支援多個型別的註解,並且字串是合法全名。
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
//新增自定義的BindView註解
annotataions.add(BindView.class.getCanonicalName());
return annotataions;
}
/**
* 指定使用的Java版本,通常這裡返回SourceVersion.latestSupported()
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 相當於每個處理器的main函式,在這裡可以做掃描、評估和處理註解程式碼的操作,以及生成Java檔案。
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
collectBindViewAnnotations(roundEnvironment);
generateJavaFilesWithJavaPoet();
return false;
}
/**
* 收集BindView註解
* @param roundEnvironment
* @return
*/
private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
//查詢所有添加了註解BindView的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
if(elements == null || elements.isEmpty()){
return false;
}
for(Element element : elements){
//註解BindView必須新增在屬性上
if(element.getKind() != ElementKind.FIELD){
error(element, "只有類的屬性可以新增@%s註解", BindView.class.getCanonicalName());
return false;
}
//獲取註解的值
int viewId = element.getAnnotation(BindView.class).value();
//這個元素是屬性型別的元素
VariableElement viewElement = (VariableElement) element;
//獲取直接包含屬性元素的元素,即類元素
TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();
//將型別元素作為key,儲存到bindMap暫存
List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
if(viewBindInfoList == null){
viewBindInfoList = new ArrayList<>();
bindMap.put(typeElement, viewBindInfoList);
}
info("註解資訊:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());
viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
}
return true;
}
/**
* 生成註解處理之後的Java檔案
*/
private void generateJavaFilesWithJavaPoet(){
if(bindMap.isEmpty()){
return;
}
//針對每個型別元素,生成一個新檔案,例如,針對MainActivity,生成MainActivity_ViewBind檔案
for(Map.Entry<TypeElement, List<ViewBindInfo>> entry : bindMap.entrySet()){
TypeElement typeElement = entry.getKey();
List<ViewBindInfo> list = entry.getValue();
//獲取當前型別元素所在的包名
String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//獲取類的全名稱
ClassName t = ClassName.bestGuess("T");
ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");
//定義方法結構
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)//Override註解
.addModifiers(Modifier.PUBLIC)//public修飾符
.returns(void.class)//返回型別void
.addParameter(t, "activity")//引數型別
;
//為方法新增實現
for(ViewBindInfo info : list){
methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
}
//定義類結構
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
.addModifiers(Modifier.PUBLIC)//public修飾符
.addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型宣告
.addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
.addMethod(methodSpecBuilder.build())//方法
.build();
//定義一個Java檔案結構
JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
try {
//寫入到filer中
javaFile.writeTo(filer);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
/**
* 錯誤提示
* @param element
* @param msg
* @param args
*/
private void error(Element element, String msg, Object... args){
//輸出錯誤提示
messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}
/**
* 資訊提示
* @param msg
* @param args
*/
private void info(String msg, Object... args) {
messager.printMessage(
Diagnostic.Kind.NOTE,
String.format(msg, args));
}
}
這裡會使用一個ViewBindInfo用於儲存view的id, 名稱,和型別的對應關係,如下:
public class ViewBindInfo {
public int viewId;
public String viewName;
public TypeMirror typeMirror;
public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
this.viewId = viewId;
this.viewName = viewName;
this.typeMirror = typeMirror;
}
}
這樣就完成了編譯時註解的處理。接下來在MainActivity中使用註解
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_text)
TextView tvText;
@BindView(R.id.btn_text)
Button btnText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindHelper.bind(this);
tvText.setText("Hello, BindView");
btnText.setText("Hello, BindView");
}
}
編譯後會生成一個MainActivity_ViewBinder.java檔案。我們在看看BindHelper的實現
public class BindHelper {
/**
* 繫結方法
* @param activity
*/
public static void bind(Activity activity) {
try {
Class<?> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
這裡就會通過反射生成MainActivity_ViewBinder的物件,呼叫bind方法作view的繫結處理。
例子專案
我的GitHub
微信掃一掃下方二維碼即可關注: