曾經(jīng)看過一篇使用運行時注解來實現(xiàn)類似 ButterKnife 功能的文章另假。直到后來我自己看了ButterKnife 源碼后才發(fā)現(xiàn)并不是這樣。推薦閱讀 ButterKnife 原理解析,這是 Butterknife 源碼地址,不妨 clone 下來看一看瞧一瞧励幼。
代碼地址:android-annotation-tutorial
反射是一個我們在運行時讀取一個類及其成員屬性,并嘗試修改這些屬性的過程口柳。 這個過程雖然有助于創(chuàng)建一個通用或獨立于實現(xiàn)的程序苹粟,但是由于我們不知道運行時的確切條件,因此也容易出現(xiàn)大量異常跃闹。 通過反射進行類掃描和修改是一個緩慢的過程嵌削,也是一種孤立代碼的丑陋方式毛好。
一、示例:
為了更好的理解編譯時注解苛秕,我們先使用運行時注解來實現(xiàn)綁定控件
-
定義注釋BindView以進行映射
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
-
將BindView注釋放在具有視圖ID的View類變量上
public class MainActivity extends AppCompatActivity {
...
@BindView(R.id.txtView)
TextView txtView;
...
}
-
創(chuàng)建一個類肌访,該類使用id tv_name將XML中定義的TextView對象賦值給變量tvName
public class ViewBinder {
/*
* annotations for activity class
* @param target is the activity with annotations
*/
public static void bind(final Activity target){
bindViews(target, target.getClass().getDeclaredFields(),
}
/*
* initiate the view for the annotated public fields
* @param obj is any class instance with annotations
* @param fields list of methods in the class with annotation
* @param rootView is the inflated view from the XML
*/
private static void bindViews(final Object obj, Field[] fields, View rootView){
for(final Field field : fields) {
Annotation annotation = field.getAnnotation(BindView.class);
if (annotation != null) {
BindView bindView = (BindView) annotation;
int id = bindView.value();
View view = rootView.findViewById(id);
try {
field.setAccessible(true);
field.set(obj, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
-
將Activity實例發(fā)送到ViewBinder。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.txtView)
private TextView txtView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);
txtView.setText("Testing");
}
...
}
這種方法能正常運行艇劫,但我們剛談到反射的性能限制吼驶,使它變得不可取。
那么店煞,我們怎樣才能改進呢蟹演?
- 我們必須消除MainActivity類的運行時掃描并將其替換為方法調(diào)用。
- 我們不希望為每個Activity編寫這些方法顷蟀,并希望它們自動生成酒请。
- 我們希望消除任何運行時異常,并希望在編譯期間移動此類檢查衩椒。
編譯時注解能滿足這些情況蚌父。
二、編譯時注解如何工作毛萌?
編譯時注解在編譯周期中進行苟弛。 在每個循環(huán)遍歷中,編譯器在讀取java源文件時找到注冊用于處理的注釋并調(diào)用相應的注釋處理器阁将。 如果在該循環(huán)中沒有生成文件膏秫,則該循環(huán)繼續(xù)生成任何文件或終止。
好吧做盅。我們將學習過程分為四個部分:
- 為注釋處理創(chuàng)建一個Android項目缤削。
- 理解用于處理的注釋的定義。
- 編寫一個編譯器模塊吹榴,通過注釋處理生成代碼亭敢。
- 使用通過注釋處理生成的代碼。
三图筹、項目結(jié)構(gòu)
該項目有四個模塊:
- app:這是Android應用程序項目帅刀。
- binder:此模塊提供一個類,該類將給Activity帶注釋的視圖對象和單擊回調(diào)方法映射到XML視圖远剩。
- binder-annotations:此模塊定義注釋以便于視圖和單擊回調(diào)方法的映射扣溺。
- binder-compiler:此模塊定義生成類以幫助上述映射的處理器。
-
定義注釋
BindView:它將視圖引用映射到其XML定義瓜晤。 示例:帶有id tv_content的TextView將映射到變量tvContent锥余。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
OnClick:它將映射一個方法,當單擊具有提供的id的視圖時將調(diào)用該方法痢掠。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
@IdRes int value();
}
在這里你可以注意到@IdRes注釋驱犹。 此注釋由support-annotations庫提供嘲恍。
-
創(chuàng)建注釋處理器
方法一:
注釋處理器循環(huán)運行并與應用程序編譯并行運行。 在每個周期中着绷,處理器都提供有關(guān)正在編譯的應用程序源代碼的信息蛔钙。
處理器必須注冊到編譯器,以便在編譯應用程序時可以運行它荠医。 我們將看到如何定義這樣的編譯器吁脱。
現(xiàn)在,我們將創(chuàng)建一個類似于binder-annotations的Java庫binder-compiler彬向。 在這個模塊中兼贡,我們將不得不創(chuàng)建目錄結(jié)構(gòu):
binder-compiler/src/main/resources/META-INF/services
在services目錄中,我們將創(chuàng)建一個名為javax.annotation.processing.Processor的文件娃胆。
此文件將列出編譯器在注釋處理時編譯應用程序源代碼時將調(diào)用的類遍希。
還有一種方法是(我采用了這種):
使用 Google 提供的庫 auto-service ,gradle 添加依賴
implementation 'com.google.auto.service:auto-service:1.0-rc2'
Processor 添加如下注解
@AutoService(javax.annotation.processing.Processor.class)
public class Processor extends AbstractProcessor {
...
}
所有注釋處理器都繼承AbstractProcessor里烦,它定義了處理的基本方法凿蒜。 我們將在此庫中創(chuàng)建一個繼承AbstractProcessor的類Processor。 我們必須覆蓋三種方法來提供處理的實現(xiàn)胁黑。
- init:這里我們將獲得Filer废封,Messager和Elements。
- process:調(diào)用此方法來處理應用程序的源代碼丧蘸。 在這里漂洋,我們將定義一個類并編寫Java源代碼。
- getSupportedAnnotationTypes:它列出了我們在處理應用程序的Java文件時要查詢的注釋力喷。
另外還要了解如下類:
- Filer:它提供API來編寫生成的源代碼文件刽漂。
- Messager:用于在編譯時打印消息。 我們發(fā)送可能通過Messager處理的錯誤消息弟孟。 由于注釋處理器在其自己的獨立環(huán)境中運行贝咙,因此我們無法通過任何其他方式與應用程序通信。
- Elements:它提供了utils方法拂募,用于過濾處理器中不同類型的元素颈畸。
public class Processor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// all the magic happens in this block
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return new TreeSet<>(Arrays.asList(
BindView.class.getCanonicalName(),
OnClick.class.getCanonicalName()
));
}
}
四、 生成 Java 源代碼
Note: 如果有些方法不明白可以參照 Android Studio中調(diào)試自定義AbstractProcessor方法 把程序跑起來没讲,打斷點看下就一目了然了。
現(xiàn)在我們提供Processor的流程方法的完整實現(xiàn)礁苗,并學習使用JavaPoet定義類及其成員爬凑。
注釋處理在處理Java注釋源代碼時提供的內(nèi)容:
- Set<? extends TypeElement>:它提供注釋列表作為正在處理的Java文件中包含的元素试伙。
- RoundEnvironment:它提供對處理環(huán)境的訪問嘁信,其中包含查詢元素的工具于样。 我們將在這個環(huán)境中使用的兩個主要功能是:processingOver(它的最后一輪處理)和getRootElements(它提供了一個將被處理的元素列表。這些元素中將包含一些我們感興趣的注釋潘靖。)
因此穿剖,我們有一組注釋和一系列元素。 我們的庫將生成一個包裝類卦溢,它將幫助映射視圖并單擊活動的偵聽器糊余。
我們的注釋將映射視圖和按鈕以刪除樣板,就像ButterKnife一樣单寂。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_content)
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Binding.bind(this);
}
@OnClick(R.id.bt_1)
void bt1Click(View v) {
tvContent.setText("Button 1 Clicked");
}
@OnClick(R.id.bt_2)
void bt2Click(View v) {
tvContent.setText("Button 2 Clicked");
}
}
我們將使用MainActivity定義使用注釋處理自動生成名為MainActivity$Binding的包裝器類贬芥。處理后,將創(chuàng)建以下類宣决。編譯的時候生成在該目錄下:
[圖片上傳失敗...(image-9b18a6-1536888483860)]
打開后看到如下內(nèi)容:
public class MainActivity$Binding {
public MainActivity$Binding(MainActivity activity) {
bindViews(activity);
bindOnClicks(activity);
}
private void bindViews(MainActivity activity) {
activity.tvContent = (TextView)activity.findViewById(2131165322);
}
private void bindOnClicks(final MainActivity activity) {
activity.findViewById(2131165218).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
activity.bt1Click(view);
}
});
activity.findViewById(2131165219).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
activity.bt2Click(view);
}
});
}
}
現(xiàn)在我們知道了我們必須生成什么蘸劈,讓我們分析如何使用我們在處理時可以使用的信息來創(chuàng)建它。
我們將首先過濾掉那些在getRootElements方法提供的元素列表中使用@BindView或@OnClick的類(Type)元素尊沸。
然后威沫,我們將迭代這些過濾的元素,然后掃描其成員和方法洼专,以使用JavaPoet開發(fā)包裝類的類模式棒掠。 最后,我們將該類寫入Java文件壶熏。希望使搜索更有效句柠, 因此,我們將創(chuàng)建一個具有過濾方法的ProcessingUtils類棒假。
public class ProcessingUtils {
private ProcessingUtils() {
// not to be instantiated in public
}
public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
Set<? extends Element> supportedAnnotations) {
Set<TypeElement> typeElements = new HashSet<>();
for (Element element : elements) {
if (element instanceof TypeElement) {
boolean found = false;
for (Element subElement : element.getEnclosedElements()) {
for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
for (Element annotation : supportedAnnotations) {
if (mirror.getAnnotationType().asElement().equals(annotation)) {
typeElements.add((TypeElement) element);
found = true;
break;
}
}
if (found) break;
}
if (found) break;
}
}
}
return typeElements;
}
}
這里有兩件事需要我們理解:
- element.getEnclosedElements():封閉元素是給定元素中包含的元素溯职。 在我們的例子中,元素將是MainActivity(TypeElement)帽哑,Enclosed元素將是tvContent谜酒,onCreate,bt1Click妻枕,bt2Click和其他繼承的成員僻族。
- subElement.getAnnotationMirrors():它將提供subElement上使用的所有注釋。
JavaPoet速成課程:
JavaPoet使得定義類結(jié)構(gòu)并在處理時編寫它非常簡單屡谐。 它創(chuàng)建了非常接近手寫代碼的類述么。 它提供了自動推斷導入以及美化代碼的工具。當然也可以使用 JavaFileObject 愕掏,但是這個遠古的笨重且不切實際的東西我們就不多說了度秘,有興趣的還是建議放棄這個興趣,就使用 JavaPoet 吧饵撑。
要使用JavaPoet剑梳,我們需要將以下依賴項添加到binder-compiler模塊中唆貌。
dependencies {
implementation project(':binder-annotations')
implementation 'com.squareup:javapoet:1.11.1'
}
本教程所需的JavaPoet的基本用法(可以從其 javapoet 獲得任何提前了解。)
- TypeSpec.Builder:定義類模式垢乙。
- addModifiers(Modifier):添加private锨咙,public或protected關(guān)鍵字。
- addAnnotation:向元素添加注釋追逮。 示例:@Override on methods或@Keep on class in case酪刀。
- TypeSpec.Builder - > addMethod:向類添加方法。 示例:構(gòu)造函數(shù)或其他方法羊壹。
- MethodSpec - > addParameter:為方法添加參數(shù)類型及其名稱蓖宦。 示例:在我們的示例中,我們希望將具有變量名稱活動的MainActivity類型傳遞給方法油猫。
- MethodSpec - > addStatement:它將在方法中添加代碼塊稠茂。 在這個方法中,我們首先定義語句的占位符情妖,然后傳遞參數(shù)來映射那些占位符睬关。 示例
addStatement("$N($N)", "bindViews", "activity") //這將生成代碼bindViews(activity)
占位符:T -> type(ClassName), $L -> literals(long etc.)
參考JavaPoet的基本介紹,可以很容易地理解其他內(nèi)容毡证。剩下的就是你自己去完成了电爹。
「百聞不如一見」,百看不如一試料睛。最后再說下源碼地址吧:
android-annotation-tutorial
參考鏈接: