本文同步發(fā)布于擱置了一年的個人博客http://mrrobot97.me
? 相信絕大部分的Android開發(fā)者都曾使用過ButterKnife, 利用ButterKnife開發(fā)者可以快速的實現(xiàn)實體view與xml的綁定雷则,此外還能綁定各種資源、動畫、字符串甚至是點擊事件等霎褐。ButterKnife內(nèi)部的原理就是通過自定義注解+自定義注解解析器來動態(tài)生成代碼并為我們的view綁定id的骗奖。本文通過實現(xiàn)一個demo性質(zhì)的ButterKnife項目來展示如何自定義注解+注解解析器。
? 關(guān)于注解本身本文不多做介紹,這里給出一篇講解注解的文章一小時搞明白自定義注解(Annotation)幔妨,對注解還比較陌生的讀者可以先看一下注解的知識题造。
? 新建一個Android Studio Project傍菇,名字就叫MyButterKnife好了。MainActivity界赔、layout都直接使用自動生成的丢习,在activity_main.xml中給TextView添加一個id。
? 接下來新建一個module用于實現(xiàn)我們的自定義注解以及自定義注解解析器淮悼,注意這個module必須是java library,因為在java library中我們才可以繼承解析器AbstractProcessor咐低,android library是無法訪問的。
? 新建一個java library取名為processor.
? 然后自定義注解(Annotation)袜腥,我們只是做一個demo性質(zhì)的實驗见擦,因此只實現(xiàn)View與id的綁定功能。這里我定義了兩個注解NeedBind與BindView:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface NeedBind {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value() default -1;
}
? NeedBind的Target是TYPE說明這是一個用于修飾類和接口的注解瞧挤,這里NeedBind的作用是幫助我們快速篩選出需要處理自定義注解的類锡宋。BindView的Target是FIELD也就是成員變量,即需要綁定資源id的view成員特恬。
? 這兩個注解的Retention都是CLASS級別执俩,表示注解會被編譯保留到.class文件但是運行時(RUNTIME)不保留,因此不影響代碼運行時的性能癌刽。有一個小技巧就是將注解的變量取名為value(只有一個變量時)可以在聲明注解變量時省略變量名役首,即可以這樣使用:
@BindView(R.id.my_tv)
TextView mTV;
? 如果我們?nèi)∶麨閯e的比如id,那么注解必須向下面這樣使用:
@BindView(id = R.id.my_tv)
TextView mTv;
? 注解定義好后就可以在項目里使用了:
@NeedBind
public class MainActivity extends AppCompatActivity {
@BindView(R.id.my_tv)
TextView mTv; //不能為private
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
? 注意這里我加了兩個注解:用于修飾MainActivity的NeedBind和用于修飾mTv的BindView显拜。另外很重要的一點就是mTv變量不能用private修飾衡奥,因為我們是通過在生成的代理類中調(diào)用MainActivity.view=(View)MainActivity.findViewById()來實現(xiàn)為view綁定id的,所以mTv至少需要是package可見級別的≡盾現(xiàn)在還沒有解析我們自定義的注解矮固,因此現(xiàn)在加的注解是沒有任何作用的,那么接下來就開始實現(xiàn)我們的注解解析器吧譬淳。
? 還是在processor module下档址,新建類MyButterKnifeProcessor,繼承自AbstractProcessor.這個就是用于解析自定義注解的解析器了盹兢。不過要想讓它生效還必須在processor下新建如下的目錄結(jié)構(gòu):
并新建名為javax.annotation.processing.Processor的文本文件,內(nèi)容就一行:
me.mrrobot97.lib.MyButterKnifeProcessor
? 還需要修改app module的build.gradle文件守伸,加入:
compile project(path: ':processor')
annotationProcessor project(path: ':processor')
? 這么做是為了讓編譯器使用我們的解析器用于解析注解绎秒。
? 后面的工作都是在MyButterKnifeProcessor類里實現(xiàn)了。我們的目的是通過讀取類中的自定義注解尼摹,生成相應(yīng)的綁定視圖的代碼见芹,這就需要一個生成java代碼的庫javapoet, squre出品,質(zhì)量絕對上乘蠢涝。在processor的build.gradle里加入如下一行:
compile 'com.squareup:javapoet:1.9.0'
ps:這么實用的開源項目在github上居然才4500start,還沒有最近火的微信跳一跳小游戲輔助腳本的star多玄呛,我也是醉了』莺眨可見github的star還是很水的把鉴,看看就好故黑,千萬別用star數(shù)目判斷一個項目是否牛逼……
? MyButterKnifeProcessor里需要重寫方法process()和方法getSupportedAnnotationTypes():
public class MyButterKnifeProcessor extends AbstractProcessor{
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//為所有標注了NeedBind標注的類生成相應(yīng)代理class
for(Element element:roundEnvironment.getElementsAnnotatedWith(NeedBind.class)){
generateBinderClass((TypeElement) element); //后面實現(xiàn)
}
//return true 表示該processor處理的注解是否只由該processor處理
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//表示該注解處理器需要處理帶有這些注解的類儿咱、接口
return Collections.singleton(NeedBind.class.getCanonicalName());
}
}
? 然后就到了本文的關(guān)鍵:處理注解并生成輔助類。強烈建議讀者先閱讀javapoet的簡單使用, 不然可能難以讀懂接下來的代碼场晶。
? 先展示一下最終生成代碼的效果混埠,這是準備本文時練習(xí)的一個demo:
// This file is generated by Binder, do not edit!
package guru.mrrobot97.customannotationprocessor;
import android.view.View;
import android.widget.TextView;
public class MainActivityDeleagteBinder {
public MainActivityDeleagteBinder(final MainActivity activity) {
bindView(activity);
bindClick(activity);
}
private void bindView(final MainActivity activity) {
activity.mTv=(TextView)activity.findViewById(2131165301);
activity.mTv2=(TextView)activity.findViewById(2131165302);
}
private void bindClick(final MainActivity activity) {
activity.findViewById(2131165301).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
activity.sayHello();
}
});
}
}
? 上面所有的內(nèi)容都是javapoet生成的,下面就按照上面這個最終效果來一步一步分析要怎么生成我們的代理類诗轻。簡單起見钳宪,就不生成bindClick相關(guān)代碼了,畢竟我們也沒定義相關(guān)注解扳炬。
? 我們要為所有標注了NeedBind注解的類生成名為*DeleagteBinder的類吏颖,同樣為了簡單起見我們只做了Activity中view的綁定。DeleagteBinder類要包含一個構(gòu)造函數(shù)恨樟、一個bindView方法, bingView方法里要為Activity中綁定了BindView注解的view綁定id半醉,此外構(gòu)造函數(shù)和bindVIew方法還都有一個<? extends Activity>類型的參數(shù)。
? 我們從小到大一個一個生成劝术,首先來構(gòu)造我們的<? extends Activity>類型的方法參數(shù):
//拿到Activity的類
ClassName activityClassName=ClassName.get(element);
//構(gòu)造activity類型的參數(shù)
ParameterSpec activityParam=ParameterSpec.builder(activityClassName,"activity")
.addModifiers(Modifier.FINAL)
.build();
然后加入一個如下的方法缩多,用于查找類中所有標注了某種注解的成員變量(VariableElement):
/**
* 返回所有標注了clazz類型注解的成員變量
* @param typeElement
* @param clazz
* @return
*/
private List<VariableElement> getFieldElementsWithAnnotation(TypeElement typeElement,Class clazz){
List<VariableElement> elements=new ArrayList<>();
for(Element element:typeElement.getEnclosedElements()){
if(element.getAnnotation(clazz)!=null){
//并沒有進行類型、訪問權(quán)限檢查养晋,真實生產(chǎn)環(huán)境肯定是要檢查的
elements.add((VariableElement) element);
}
}
return elements;
}
然后是生成bindView方法內(nèi)的方法體衬吆,就是真正實現(xiàn)view=activity.findViewById的java語句:
List<VariableElement> bindViewFieldList=getFieldElementsWithAnnotation(element,BindView.class);
CodeBlock.Builder bindViewCodeBlockBuilder=CodeBlock.builder();
for(VariableElement variableElement:bindViewFieldList){
//拿到變量名
String variableName=variableElement.getSimpleName().toString();
//變量的類型
TypeName viewType=ClassName.bestGuess(variableElement.asType().toString());
//注解的值,也就是view要綁定的id
int viewId=variableElement.getAnnotation(BindView.class).value();
bindViewCodeBlockBuilder.addStatement("activity.$L=($T)activity.findViewById($L)",variableName,viewType,viewId);
}
有了bindView()的方法體绳泉,參數(shù)逊抡,該構(gòu)造bindView()方法了:
//生成bindView()方法
MethodSpec bindViewMethod=MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC)
.addParameter(activityParam)
.addCode(bindViewCodeBlockBuilder.build())
.returns(void.class)
.build();
構(gòu)造函數(shù):
//構(gòu)造函數(shù),內(nèi)部調(diào)用bindView方法
MethodSpec constructorMethod=MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(activityParam)
.addStatement("$N($L)",bindViewMethod,activityParam.name)
.build();
然后是生成*DelegateBinder這個類文件:
//生成BinderDelegate類
String binderClassName=element.getSimpleName().toString();
TypeSpec delegateType=TypeSpec.classBuilder(binderClassName+"DelegateBinder")
.addModifiers(Modifier.PUBLIC)
.addMethod(bindViewMethod)
.addMethod(constructorMethod)
.build();
JavaFile javaFile=JavaFile.builder(getPackage(element).getQualifiedName().toString(),delegateType)
.addFileComment("This file is generated by Binder, do not edit!")
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
注意這里的包名零酪,生成的類的包名盡量與需要綁定的Activity所在的包名一致冒嫡,這樣BindView修飾的成員變量只需是包內(nèi)可見就行麦射,否則的話就必須是public的了。獲取包名用如下方法:
/**
* 查找包名
* @param element
* @return
*/
public static PackageElement getPackage(Element element) {
while (element.getKind() != PACKAGE) {
element = element.getEnclosingElement();
}
return (PackageElement) element;
}
寫完上面所有這些灯谣,Make Project潜秋,你會發(fā)現(xiàn)app下的build/generated/source/apt/debug目錄下生成了MainActivityDelegateBinder類:
到這里,已經(jīng)距離成功很接近了胎许,我們還需要做的就是在MainActivity的setContentView()調(diào)用之后峻呛,new出我們的MainActivityDelegateBinder類,即完成了MainActivity中帶BindView標注的成員變量的id綁定辜窑。為了new一個MainActivityDelegateBinder钩述,我們在app module中新建一個幫助類MyButterKnife:
public class MyButterKnife {
public static final String ACTIVITY_DELEGATE_SUFFIX = "DelegateBinder";
public static void bind(Activity activity){
String activityName=activity.getClass().getName();
String delegateName=activityName+ ACTIVITY_DELEGATE_SUFFIX;
try {
Class delegateClass=activity.getClass().getClassLoader().loadClass(delegateName);
Constructor constructor=delegateClass.getConstructor(activity.getClass());
constructor.newInstance(activity);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
? 在MyButterKnife里稍微利用了一點反射new了MainActivityDelegateBinder實體,然后MainActivityDelegateBinder的構(gòu)造函數(shù)調(diào)用了bindView()最終實現(xiàn)了MainActivity中view的綁定穆碎。
? 最后在MainActivity中調(diào)用MyButterKnife.bind(this)即可:
@NeedBind
public class MainActivity extends AppCompatActivity {
@BindView(R.id.my_tv)
TextView mTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
mTv.setText("This is not hello world");
}
}
? 編譯運行牙勘,沒有NullPointerException,而且mTv的內(nèi)容也是我們設(shè)置的內(nèi)容:
? 至此,我們實現(xiàn)Demo版本ButterKnife的目的已經(jīng)基本實現(xiàn)了所禀!
? ps:如果你在你的自定義Processor中用到Modifier的地方Android Studio報紅時方面,請無視,這是Android Studio自身的bug色徘,不影響編譯.
? 再次強調(diào)恭金,本文的目的是給讀者對AnnotationProcessor一個入門的使用概念,最終實現(xiàn)的Demo也是一個十分拙劣的版本褂策,只能說可以跑通横腿,代碼里沒有做任何合法性、類型匹配斤寂、訪問權(quán)限等相關(guān)的安全性檢查耿焊,這在生產(chǎn)環(huán)境中是完全不可用的。真正的ButterKnife在這些可能發(fā)生異常的方面做了大量安全性檢查遍搞。
? 另附demo源碼地址
? 以上罗侯。