0.導(dǎo)語
Java 作為一門低語法糖的語言嚎杨,核心在其虛擬機(jī)的實(shí)現(xiàn),語言層面提供的“黑科技”并不多氧腰,而注解就是其中比較重要的一點(diǎn)枫浙。注解在 Java5 中開始加入,在 Java6 中對(duì)外暴露出注解處理器的接口供程序員來按需處理注解」潘現(xiàn)如今箩帚,不論是安卓客戶端還是Java后端技術(shù)棧都使用到了大量注解相關(guān)的庫和框架,甚至可以說是到了“泛濫成災(zāi)”的地步。在其中相對(duì)比較重要的便是自定義注解處理器了,本文將從兩個(gè)方面來進(jìn)行介紹:
- 注解的通俗解釋及使用
- 注解的處理方式惜姐,以安卓中的 ButterKnife 庫為例厢洞,通過運(yùn)行時(shí)期處理注解和編譯時(shí)期通過注解處理器來處理注解這兩種方式,實(shí)現(xiàn)和 ButterKnife 庫相似的功能
1.什么是注解
注解,通俗的來說,就是像注釋一樣,是由程序員在代碼中加入的一種“標(biāo)注”叠纷,不影響所編寫的原有代碼的執(zhí)行。而這種標(biāo)注(注解)可以被編碼用的IDE潦嘶、編譯器涩嚣、類加載器的代理程序、其他第三方工具以及原有代碼運(yùn)行期間讀取和處理掂僵,生成一些新的輔助代碼或是提示航厚,從而節(jié)省時(shí)間,提升效率锰蓬。這些工具讀取注解的時(shí)機(jī)是根據(jù)注解的生命周期來定的,注解的生命周期就是其“存在壽命”幔睬,分為三種:
-
源代碼時(shí)期注解,即注解只出現(xiàn)在 .java 文件中芹扭,編譯后便不再出現(xiàn)在生成的.class 文件中麻顶,這一階段赦抖,對(duì)注解的處理有兩種方式:
- 被 IDE 中代碼檢查工具讀取,如圖 1-1中的 1 處辅肾,實(shí)時(shí)地提示程序員縮寫代碼中的錯(cuò)誤队萤,例如 @Override 注解就是用來檢查程序員所復(fù)寫的父類方法的簽名一致性的;
- 在原代碼編譯期間矫钓,被程序員在編譯器中所注冊(cè)的注解處理器所讀取要尔,如圖 1-1中的 2 處,用于生成新的源代碼文件參與編譯新娜,省去重復(fù)地書寫樣板代碼的成本赵辕,提升效率,編譯之后便被去除杯活,不再出現(xiàn)在 .class 文件中匆帚;
-
字節(jié)碼時(shí)期注解,即出現(xiàn)在 .class 文件中的注解旁钧,對(duì)這類注解的處理主要涉及字節(jié)碼的修改,需要使用ASM等類庫互拾,根據(jù)處理時(shí)機(jī)的不同歪今,也可分為兩種方式:
- 在源代碼編譯后處理:如果當(dāng)前工程采用了Gradle、Maven等構(gòu)建工具來構(gòu)建颜矿,則在源代碼被編譯為 .class 文件后寄猩,可以通過相應(yīng)地腳本調(diào)用使用了ASM庫編寫的第三方工具來讀取文件中的注解并修改 .class 文件中的虛擬機(jī)指令,沒有采用構(gòu)建工具骑疆,也可以通過命令行來手動(dòng)更新 .class 文件田篇,虛擬機(jī)加載.class 文件時(shí)不會(huì)加載字節(jié)碼級(jí)注解,如圖 1-1中的 3 處所示箍铭;
- 在類加載時(shí)處理:通過代理程序泊柬,在類加載器加載.class 文件前,讀取文件中的注解修改字節(jié)碼诈火,但并不保存兽赁,即原有的.class文件內(nèi)容不變,內(nèi)存中處理過的字節(jié)碼的類被類加載器直接加載到虛擬機(jī)中冷守,同樣的并不加載.class 文件中的字節(jié)碼級(jí)注解刀崖,如圖 1-1中的 4 處所示;
運(yùn)行時(shí)期注解拍摇,即注解被類加載器加載到內(nèi)存當(dāng)中亮钦,和類的其他構(gòu)成元素一樣被放置于元數(shù)據(jù)區(qū),供堆區(qū)中相應(yīng)的 class 對(duì)象訪問充活,換句話說蜂莉,此時(shí)的注解可以通過反射的方式來讀取和處理蜡娶,這時(shí)的處理過程往往是由程序員在編碼期間就確定的,是運(yùn)行效率最低但卻是最容易實(shí)現(xiàn)的一種注解處理方式巡语,如圖 1-1中的 5 處所示翎蹈;
2.處理注解
由前所述,對(duì)注解的處理由處理時(shí)機(jī)的不同可以有不同的實(shí)現(xiàn)方式男公。這里介紹開發(fā)中最常用的兩種方式荤堪,運(yùn)行時(shí)期處理注解和編譯時(shí)期通過自定義注解處理器來處理,口說無憑枢赔,先設(shè)立一個(gè)目標(biāo)澄阳,ButterKnife 是安卓中很有名的利用注解來進(jìn)行控件綁定的庫,我將通過上述兩種注解處理方式來實(shí)現(xiàn)類似 ButterKnife 庫的功能踏拜。
2.1 ButterKnife 的簡單介紹
ButterKnife 這個(gè)庫主要用于安卓端控件綁定的問題碎赢。在很多流程類似的GUI程序中,都是先將 xml 等標(biāo)簽語言書寫的界面文件讀入并解析速梗,在內(nèi)存中生成視圖界面中所有控件所對(duì)應(yīng)的控件樹對(duì)象肮塞,之后將控件樹中的控件實(shí)體對(duì)象和邏輯控制類(安卓中的Activity)中的對(duì)象引用進(jìn)行綁定,需要由程序員手工進(jìn)行姻锁,如下所示:
public class SourceTestActivity extends AppCompatActivity {
private static final String TAG = SourceTestActivity.class.getSimpleName();
// 控件引用
private TextView mTextView;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 讀入文件枕赵,解析生成對(duì)象樹
setContentView(R.layout.activity_source_test);
// 根據(jù)控件id在控件樹中找到對(duì)應(yīng)的實(shí)體控件對(duì)象并綁定
mTextView = findViewById(R.id.tv);
mButton = findViewById(R.id.btn);
// 綁定按鍵控件的點(diǎn)擊事件處理回調(diào)
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
}
});
}
}
界面中有一個(gè)文本控件和一個(gè)按鈕控件,都需要程序員通過 findViewById 和 setOnClickListner 方法來綁定實(shí)體 View 和其對(duì)應(yīng)的回調(diào)方法位隶。通過 ButterKnife 庫可以將代碼簡化成如下所示:
public class SourceTestActivity extends AppCompatActivity {
private static final String TAG = SourceTestActivity.class.getSimpleName();
// 用注解標(biāo)記引用需要綁定的控件的id
@BindView(R.id.tv)
TextView mTextView;
@BindView(R.id.btn)
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_source_test);
// 執(zhí)行綁定過程
ButterKnife.bind(this);
}
// 用注解標(biāo)記事件回調(diào)需要綁定的控件id
@OnClick(R.id.btn)
public void test(View v) {
Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
}
}
在需要綁定實(shí)體 View 的引用上加上 @BindView 注解拷窜,在需要綁定的回調(diào)方法上加上 @OnClick 注解,最后在代碼中通過 ButterKnife.bind() 方法來實(shí)現(xiàn)綁定涧黄。上述例子由于需要綁定的控件和方法較少篮昧,看不出優(yōu)勢,但當(dāng)需要綁定的對(duì)象很多是笋妥,ButterKnife 庫可以幫我們節(jié)省很多重復(fù)的 findViewById 和 setOnClickListner 方法的書寫懊昨。
2.2 反射處理運(yùn)行時(shí)注解實(shí)現(xiàn) “ButterKnife”
2.2.1 自定義注解
首先我們需要自定義注解 @BindView 和 @OnClick
BindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
@IdRes int value();
}
@Target 元注解中的ElementType.FIELD參數(shù)設(shè)定了@BindView 注解所適用的場景為 Field ,即屬性字段挽鞠;@Retention 元注解中的RetentionPolicy.RUNTIME 參數(shù)設(shè)定了@BIndView 注解的生命周期為運(yùn)行時(shí)疚颊,即 .java .class 加載到內(nèi)存中三個(gè)時(shí)期,@BindView 注解一直都存在信认。
OnClick
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
@IdRes int[] value() default {View.NO_ID};
}
OnClick 注解生命周期和 BindView 一致材义,不同在于適用的目標(biāo)為方法(ElementType.METHOD)
2.2.2 運(yùn)行時(shí)反射綁定
public class ButterKnife {
/**
* 負(fù)責(zé) 將容器類中的監(jiān)聽方法綁定到容器類中聲明的指定 view 上
*
* @param container 容器類,待綁定方法和被綁定的 view 都位于其中, 一般是 Activity or Fragment
*/
public static void bind(final Activity container) {
// key: view's resId; value: view
SparseArray<View> viewsMap = new SparseArray<>(8);
Class<?> cls = container.getClass();
// 獲取已注解的 view 域, 并且?guī)推?findViewById
for (Field viewField: cls.getDeclaredFields()){
BindView fieldAnnotation = viewField.getAnnotation(BindView.class);
if (fieldAnnotation != null) {
viewField.setAccessible(true);
try {
// 進(jìn)行 View 綁定
View viewRef = container.findViewById(fieldAnnotation.value());
viewField.set(container, viewRef);
viewsMap.put(fieldAnnotation.value(), viewRef);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
// 獲取已注解的方法域, 并且?guī)推渫ㄟ^動(dòng)態(tài)代理 setOnClickListener
for(final Method onClickMethod: cls.getDeclaredMethods()){
OnClick methodAnnotation = onClickMethod.getAnnotation(OnClick.class);
if (methodAnnotation != null) {
int[] viewResIds = methodAnnotation.value();
for (int resId : viewResIds) {
View viewToAttach;
if ((viewToAttach = viewsMap.get(resId)) != null) {
onClickMethod.setAccessible(true);
viewToAttach.setOnClickListener(
(View.OnClickListener) Proxy.newProxyInstance(null, new Class[]{View.OnClickListener.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
InvocationTargetException, IllegalAccessException {
return onClickMethod.invoke(container, args);
}
})
);
}
}
}
}
}
}
綁定方法主要是假的ButterKnife中的靜態(tài)bind方法嫁赏,其中做了兩件事:
- 首先通過反射遍歷了所有的屬性字段的其掂,找出其中附加了@BindView注解的域,即View的引用潦蝇,通過@BindView注解中設(shè)置的View的resId找到View實(shí)體款熬,并綁定到View引用上深寥,最后將其緩存到SparseArray中。
- 之后贤牛,通過反射遍歷所有方法域惋鹅,找出其中附加了@OnClick注解的方法,由于Java中方法不是對(duì)象殉簸,不能獨(dú)立存在闰集,必須存在于類中,所以需要通過動(dòng)態(tài)代理機(jī)制來代理View.OnClickListener接口般卑,“包裹”所注解的方法武鲁,最后通過SparseArray的鍵值resId找到View實(shí)體進(jìn)行回調(diào)方法的綁定。
最后是Activity中的使用,界面中一共是兩個(gè)按鈕控件蝠检,綁定同一個(gè)回調(diào)方法沐鼠,使用的形式上和ButterKnife庫是類似的:
RuntimeTestActivity.java
public class RuntimeTestActivity extends AppCompatActivity {
@BindView(R.id.btn_one)
private Button btnTest1;
@BindView(R.id.btn_two)
private Button btnTest2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_runtime_test);
ButterKnife.bind(this);
}
@OnClick({R.id.btn_one, R.id.btn_two})
public void onClick(View v) {
Toast.makeText(v.getContext(), ((TextView)v).getText(), Toast.LENGTH_SHORT).show();
}
}
綜上,雖然使用的形式上和 ButterKnife 相類似叹谁,但原理上卻完全不同饲梭,我們所寫的偽ButterKnife庫中的注解都是運(yùn)行時(shí)注解,即源文件中的注解也會(huì)出現(xiàn)在最后的運(yùn)行時(shí)內(nèi)存中焰檩,可以通過反射的方式拿到注解排拷,從而獲得被注解的域或方法,最后在運(yùn)行時(shí)再進(jìn)行綁定锅尘,由于待綁定的元素都需要通過反射來拿到,故效率較低布蔗。
2.3 編譯時(shí)注解處理器處理注解實(shí)現(xiàn) “ButterKnife”
反射處理運(yùn)行時(shí)注解的效率較低藤违,那我們有沒有辦法減少反射的調(diào)用呢?回顧真ButterKnife庫的作用纵揍,主要是減少書寫findViewById之類的樣板代碼的書寫顿乒,如果我們可以提前生成這些綁定方法的模板代碼,只是通過反射調(diào)用唯一的bind()方法泽谨,那就可以適當(dāng)提升效率璧榄,實(shí)際上,真ButterKnife庫中也是這么做的吧雹。首先看偽ButterKnife庫工程的結(jié)構(gòu):
- app為安卓的測試module,用于測試自定義的偽ButterKnife庫骨杂;
- butterknife-annotation 為包含自定義注解的module;
- butterknife-compiler 為自定義注解處理器的module,這里將注解處理器和所能處理的注解分在兩個(gè)不同的module,主要是因?yàn)樽⒔馓幚砥鲗?shí)際上只是被編譯器在編譯時(shí)調(diào)用,并不屬于工程雄卷,所以在打包時(shí)可以將其不打入apk中以節(jié)省空間搓蚪,如果自定義的注解和注解處理器處在同一module,那么使用時(shí),app module中會(huì)找不到自定義注解丁鹉,所以需要將注解獨(dú)立成butterknife-annotation module 妒潭,在 app 和注解處理器 butterknife-compiler module 中分別引用悴能,自定義注解處理器在編譯時(shí)讀取源文件中的注解,生成新的源代碼文件參與編譯雳灾;
- butterknife-library module 負(fù)責(zé)通過反射調(diào)用 butterknife-compiler 中自動(dòng)生成的代碼
2.3.1 自定義注解
BindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
OnClick
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
int value();
}
可以看到和反射式偽ButterKnife庫中的自定義注解所不同的是注解生命周期的定義漠酿,@Retention(RetentionPolicy.SOURCE)聲明了這些注解都是源碼級(jí)注解,即編譯后在 .class 文件中便不存在了谎亩,注意建立butterknife-annotation 庫時(shí)炒嘲,選擇該庫為 java library,如下圖所示:
最后的module工程結(jié)構(gòu)如下:
2.3.2 自定義注解處理器
自定義注解處理器的基本原理是將程序員自定義的注解處理器注冊(cè)到編譯器中,編譯器在編譯源文件時(shí)調(diào)用該注解處理器處理源文件中的注解团驱,處理這些注解時(shí)是無法修改源文件的摸吠,只能生成新的源文件,生成的新文件會(huì)繼續(xù)調(diào)用注解處理器處理嚎花,這是一個(gè)遞歸的過程寸痢,一輪一輪地處理,直到?jīng)]有新的源文件生成紊选,我們正是利用這點(diǎn)來自動(dòng)生成綁定控件的模板代碼啼止。大致原理如下圖所示:
編譯器編譯源文件后生成語法樹(AST),即結(jié)構(gòu)化后的源文件元素兵罢,并將其傳入自定義的注解處理器献烦,自定義的注解處理器可以通過RoundEnvironment獲得這些語法元素來處理,最后生成字節(jié)碼文件卖词。想要自己實(shí)現(xiàn)注解處理器并注冊(cè)到編譯器中的具體步驟如下:
- 創(chuàng)建java lib butterknife-cmplier和創(chuàng)建 butterknife-annotation的方式一致巩那;
- 創(chuàng)建Processor,代碼如下:
BindViewProcessor.java
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({Constants.FULL_NAME_ANNO_BINDVIEW, Constants.FULL_NAME_ANNO_ONCLICK})
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Messager mMessager;
private Elements mElementUtils;
private Map<String, Creator> mProxyMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
// @Override
// public Set<String> getSupportedAnnotationTypes() {
// HashSet<String> supportTypes = new LinkedHashSet<>();
// supportTypes.add(BindView.class.getCanonicalName());
// return supportTypes;
// }
//
// @Override
// public SourceVersion getSupportedSourceVersion() {
// return SourceVersion.latestSupported();
// }
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
mProxyMap.clear();
// 獲取源文件中帶有 BindView 注解的域
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
Creator proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new Creator(mElementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxy.putFieldElement(id, variableElement);
}
// 獲取源文件中帶有 OnClick 注解的方法
elements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
for(Element element: elements){
ExecutableElement methodElement = (ExecutableElement) element;
TypeElement classElement = (TypeElement) methodElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
Creator proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new Creator(mElementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
OnClick bindAnnotation = methodElement.getAnnotation(OnClick.class);
int id = bindAnnotation.value();
proxy.putMethodElement(id, methodElement);
}
// 生成java代碼
for (String key : mProxyMap.keySet()) {
Creator proxyInfo = mProxyMap.get(key);
JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode())
.indent(" ")
.addFileComment("generate file, do not modify!")
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true;
}
}
Constants.java
public class Constants {
public static final String FULL_NAME_ANNO_BINDVIEW = "com.danielchen.demo.butterknife_annotation.BindView";
public static final String FULL_NAME_ANNO_ONCLICK = "com.danielchen.demo.butterknife_annotation.OnClick";
}
從代碼中可以看出,注解處理器主要是繼承AbstractProcessor并實(shí)現(xiàn) process() 方法此蜈, 可選擇實(shí)現(xiàn) init()即横、getSupportedAnnotationTypes() 、
getSupportedSourceVersion()三個(gè)方法裆赵,其中东囚,getSupportedAnnotationTypes() 、
getSupportedSourceVersion()是返回本注解處理器所支持的注解和源代碼版本战授,可以在注解處理器上加上@SupportedSourceVersion()
@SupportedAnnotationTypes()注解實(shí)現(xiàn)同樣的效果,init()方法主要是為了初始化一些全局的工具页藻。
- 向編譯器注冊(cè)本注解處理器,可以手動(dòng)注冊(cè):在main文件夾下植兰,創(chuàng)建路徑 META-INF/services/,在其中創(chuàng)建文件javax.annotation.processing.Processor,在文件中加入注解處理器的全限定名份帐;這樣做很麻煩,更簡便的方法是引入google的auto-service庫钉跷,gradle 文件中遠(yuǎn)程依賴 'com.google.auto.service:auto-service:1.0-rc4'弥鹦,在注解處理器類上加上@AutoService(Processor.class)即可,這樣會(huì)自動(dòng)創(chuàng)建上述路徑和文件,如下圖所示:
4.實(shí)現(xiàn)注解處理器的process()方法彬坏,形參Set是本處理器可處理的注解類型朦促,就是@SupportedAnnotationTypes()中所設(shè)置的類型,RoundEnvironment 則提供了本輪次的語法樹元素栓始,如果方法返回true,則后續(xù)其他處理器不能處理這些注解务冕,否則可以處理,類似點(diǎn)擊事件的攔截幻赚。上述代碼中將讀取到的屬性字段和方法對(duì)應(yīng)的語法元素存入 Creator,Creator 代碼如下:
Creator.java
public class Creator {
private String mBindingClassName;
private String mFullPackageName;
private ClassName mClassName;
private Map<Integer, VariableElement> mVariableElementMap;
private Map<Integer, ExecutableElement> mExecutableElementMap;
public Creator(PackageElement pkgElement, TypeElement classElement) {
mVariableElementMap = new HashMap<>(8);
mExecutableElementMap = new HashMap<>(8);
this.mClassName = ClassName.get(classElement);
this.mFullPackageName = pkgElement.getQualifiedName().toString();
this.mBindingClassName = classElement.getSimpleName().toString() + "_ViewBinding";
}
public String getFullPackageName() {
return mFullPackageName;
}
public void putFieldElement(int id, VariableElement element) {
mVariableElementMap.put(id, element);
}
public void putMethodElement(int id, ExecutableElement element) {
mExecutableElementMap.put(id, element);
}
public TypeSpec generateJavaCode() {
return TypeSpec.classBuilder(mBindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateBindMethod())
.build();
}
private MethodSpec generateBindMethod() {
String paramName = "activity";
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(mClassName, paramName, Modifier.FINAL);
for (int resId : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(resId);
String viewName = element.getSimpleName().toString();
String viewType = element.asType().toString();
methodBuilder.addStatement("$L.$L = ($L)$L.findViewById($L)",
paramName, viewName, viewType, paramName, resId);
if (mExecutableElementMap.containsKey(resId)) {
ExecutableElement methodElement = mExecutableElementMap.get(resId);
String methodName = methodElement.getSimpleName().toString();
ClassName viewClass = ClassName.get("android.view", "View");
ClassName clickClass = ClassName.get("android.view.View", "OnClickListener");
TypeSpec onClickListener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(clickClass)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(viewClass, "v")
.returns(TypeName.VOID)
.addStatement("$L.$L(v)", paramName, methodName)
.build())
.build();
methodBuilder.addStatement("$L.$L.setOnClickListener($L)", paramName, viewName, onClickListener);
}
}
return methodBuilder.build();
}
}
Creator 中提供了根據(jù)緩存的語法元素生成源代碼的方法禀忆,這里可以采用用字符串自己拼裝的形式,但這樣太過復(fù)雜且不易維護(hù)落恼,所以推薦使用JavaPoet庫來生成源代碼箩退,可讀性更強(qiáng)。
最終佳谦, butterknife-compiler 的工程結(jié)構(gòu)如下:
依賴關(guān)系如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 遠(yuǎn)程依賴 auto-service 庫, 用于注冊(cè)自定義注解處理器到編譯器中
implementation 'com.google.auto.service:auto-service:1.0-rc4'
// 遠(yuǎn)程以來 javapoet 庫戴涝,用于動(dòng)態(tài)生成代碼
implementation 'com.squareup:javapoet:1.11.1'
// 本地依賴 聲明了自定義注解的 module
implementation project(':butterknife-annotation')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
最終自動(dòng)生成的源代碼如下:
// generate file, do not modify!
package com.danielchen.demo.demo_reflect;
import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;
public class SourceTestActivity_ViewBinding {
public void bind(final SourceTestActivity activity) {
activity.mTextView = (android.widget.TextView)activity.findViewById(2131230914);
activity.mButton = (android.widget.Button)activity.findViewById(2131230755);
activity.mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
activity.test(v);
}
});
}
}
2.3.3 反射調(diào)用生成的代碼
最終通過butterknife-libraray中的 ButterKnife 類來調(diào)用自動(dòng)生成的 SourceTestActivity_ViewBinding 的 bind() 方法,ButterKnfie 代碼如下:
public class ButterKnife {
public static void bind(Activity activity) {
try {
Class<?> bindViewClass = Class.forName(activity.getClass().getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
由于自動(dòng)生成的類名和方法名bind都是約定好的钻蔑,所以可以這么用啥刻,最后在測試代碼中調(diào)用ButterKnife.bind()即可完成綁定,如下:
public class SourceTestActivity extends AppCompatActivity {
private static final String TAG = SourceTestActivity.class.getSimpleName();
@BindView(R.id.tv)
TextView mTextView;
@BindView(R.id.btn)
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_source_test);
// butterknife-library bind方法中通過反射調(diào)用 SourceTestActivity_ViewBinding 中的bind方法完成綁定
ButterKnife.bind(this);
}
@OnClick(R.id.btn)
public void test(View v) {
Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
}
}
綜上咪笑,實(shí)際上可帽,仿照真ButterKnife庫的原理,我們是將控件綁定的模板代碼先自動(dòng)生成窗怒,運(yùn)行時(shí)在用過一次反射調(diào)用即完成綁定映跟,比運(yùn)行時(shí)注解處理的方式的多次反射綁定要好那么一點(diǎn),當(dāng)然真ButterKnife庫中的實(shí)際代碼要更復(fù)雜些扬虚,不過大致原理是這樣申窘。
3.總結(jié)
本文順著注解的生命周期大致描述了處理注解的不同時(shí)機(jī)和處理方法。參照ButterKnife庫的原理孔轴,對(duì)運(yùn)行時(shí)動(dòng)態(tài)處理注解和編譯時(shí)根據(jù)注解生成代碼這兩種處理注解的方式進(jìn)行了代碼演示,限于本人的水平碎捺,寫的比較冗長路鹰,很多細(xì)節(jié)也沒能描述清楚,之后會(huì)將代碼傳至github,感覺還是看代碼理解更快收厨。晋柱。。