butterknife是現(xiàn)在使用的較多的一個庫了肺孵,它讓我們的工作減少了相當(dāng)一部分的量,今天就讓我們一起來走近它 颜阐,實現(xiàn)我們自己的一個簡單的類似效果平窘,讓它不再顯得那么神秘吧!
談及butterknife的原理凳怨,粗略的說瑰艘,它用到了反射和注解這兩個方面的知識是鬼。那么我們想實現(xiàn)簡單的butterknife效果,我們就得首先知道反射和注解是怎么回事紫新,ok均蜜,接下來我們先來看看反射吧。
JAVA反射機(jī)制是在運行狀態(tài)中芒率,對于任意一個類囤耳,都能夠知道這個類的所有屬性和方法;對于任意一個對象敲董,都能夠調(diào)用它的任意一個方法和屬性;這種動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為java語言的反射機(jī)制慰安。
要想解剖一個類,必須先要獲取到該類的字節(jié)碼文件對象腋寨。而解剖使用的就是Class類中的方法.所以先要獲取到每一個字節(jié)碼文件對應(yīng)的Class類型的對象.
上述就是對反射的理解,有了文字含義化焕,我們再來寫一寫小案例具體看看吧:
比如我們這有個User類萄窜,想要獲取到這個類中的成員變量
public class User {
private String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name == null ? "" : name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void like(String type){
Log.e("aaa","喜歡的東西是:----"+type);
}
}
那么我們想要通過反射來獲得該類中的屬性和方法:該怎么做呢?
從上述文字中撒桨,我們可以看到查刻,想要解剖一個類:
第一步:就是先要獲取到每一個字節(jié)碼文件對應(yīng)的Class類型的對象.
這個對象有三種方式可以獲取:
//第一種 類名.class
Class class1= User.class;
//第二種 通過forName(全類名)
Class clazz2 = Class.forName("com.example.yinl.simplebutterknife.User");
// 第三種 通過對象的getClass()
User user = new User("zhangsan", 10);
Class clazz = user.getClass();
第二步:通過獲得的對象來拿到成員屬性
//這里注意 當(dāng)屬性為private的時候 需要使用getDeclaredField()
//如果是public 則也可使用getField()
Field name=clazz.getDeclaredField("name");
Field age=clazz.getField("age");
//private屬性值 還需設(shè)置 允許暴力反射
name.setAccessible(true);
//獲得屬性值 從user對象中獲取
String uName= (String) name.get(user);
int uAge=age.getInt(user);
//修改屬性值 修改的對象 修改值
name.set(user,"yinll");
age.set(user,12);
//獲得類中的方法
//一:獲得所有方法
Method[] methods=clazz.getMethods();
for(Method method:methods){
//打印一下方法 做個測試
Log.e("aaa","方法-----"+method.getName());
}
//二:獲得某一個方法 方法名 方法類型
Method method=clazz.getMethod("like",String.class);
//調(diào)用該方法(在user對象中調(diào)用該方法凤类,傳入?yún)?shù))
method.invoke(user, "水果茶");
通過以上幾個步驟穗泵,我們就能成功的獲取到我們想要知道的類中的屬性和方法,并對其進(jìn)行修改和調(diào)用了谜疤。有木有很簡單佃延?
學(xué)會了反射之后,我們接下來看看 注解:
注解(Annotation)夷磕,也叫元數(shù)據(jù)履肃。一種代碼級別的說明。它是JDK1.5及以后版本引入的一個特性坐桩,與類尺棋、接口、枚舉是在同一個層次绵跷。它可以聲明在包膘螟、類、字段碾局、方法萍鲸、局部變量、方法參數(shù)等的前面擦俐,用來對這些元素進(jìn)行說明脊阴,注釋。它可以用于創(chuàng)建文檔,跟蹤代碼中的依賴性嘿期,甚至執(zhí)行基本編譯時檢查品擎。注解是以‘@注解名’在代碼中存在的,根據(jù)注解參數(shù)的個數(shù)备徐,我們可以將注解分為:標(biāo)記注解萄传、單值注解、完整注解三類蜜猾。它們都不會直接影響到程序的語義秀菱,只是作為注解(標(biāo)識)存在,我們可以通過[反射機(jī)制]編程實現(xiàn)對這些元數(shù)據(jù)(用來描述數(shù)據(jù)的數(shù)據(jù))的訪問蹭睡。另外衍菱,你可以在編譯時選擇代碼里的注解是否只存在于源代碼級,或者它也能在class文件肩豁、或者運行時中出現(xiàn)(SOURCE/CLASS/RUNTIME)脊串。
首先如何創(chuàng)建注解:定義一個類 將其設(shè)置為@interface。下方是我定義的一個注解:
- 注解:我們需要注意的是清钥,定義注解時我們需要給其設(shè)置 @Retention
和 @Target , 具體使用情況詳細(xì)見下方代碼
//表示當(dāng)前注解存在于字節(jié)碼中琼锋,當(dāng)源碼被編譯成字節(jié)碼時,注解不會被清除
//在類加載的時候丟棄祟昭。在字節(jié)碼文件的處理中有用缕坎。注解默認(rèn)使用這種方式。
//@Retention(RetentionPolicy.CLASS)
//表示當(dāng)前注解存在于源碼中篡悟,當(dāng)源碼別編譯成字節(jié)碼時念赶,注解會被清除
//在編譯階段丟棄。這些注解在編譯結(jié)束之后就不再有任何意義恰力,所以它們不會寫入字節(jié)碼
//@Retention(RetentionPolicy.SOURCE)
//表示當(dāng)前注解使用在方法上
//@Target(ElementType.METHOD)
//用于描述類叉谜、接口或enum聲明
//@Target(ElementType.TYPE)
//表示當(dāng)前注解存在于虛擬機(jī)
//始終不會丟棄,運行期也保留該注解踩萎,因此可以使用反射機(jī)制讀取該注解的信息停局。我們自定義的注解通常使用這種方式。
@Retention(RetentionPolicy.RUNTIME)
//表示當(dāng)前注解使用在屬性上
@Target(ElementType.FIELD)
public @interface BindView {
// 如果注解中只有一個屬性香府,可以直接命名為“value”董栽,使用時無需再標(biāo)明屬性名。
//@BindView(R.id.y)
// int value();
//如果是其他值 比如
int age();
String name();
//則在使用時 需要表明屬性名 如 @BindView(age=18,name="yinl")
}
定義好注解之后 我們就可以在類中來使用了:比如我們新建一個Test類企孩,看看如何在這個類中來使用我們的注解
public class Test {
/**
* 使用注解 age和name在注解中有定義
* 如果注解中只有一個屬性锭碳,可以直接命名為“value”
* 此時 使用@BindView時不需要像下方一樣知名age或者name屬性
* 直接@BindView(20)即可,@BindView(R.id.xx)就是這樣勿璃,熟悉吧
*/
@BindView(age=18,name="yinl")
private String name ;
private int age ;
@Override
public String toString(){
return "name:"+name+",age:"+age;
}
}
那到了這里擒抛,我們又如何去獲取注解上的值呢推汽?這時候我們上面學(xué)到的反射就有用啦!這里我們再新建一個類歧沪,就取名叫做Butterknife吧歹撒,我們來模仿一下:在類中我們也來一個bind方法,我們將我們需要解剖的類傳入到這個類中進(jìn)行解析诊胞,獲取到我們想要的數(shù)據(jù)(中間除了上述說道的反射只是外暖夭,需要注意的就是我們怎么樣來拿到注解?)
public class Butterknife {
//這里傳入需要解析的對象
public static void bind(Test test) throws NoSuchFieldException,
IllegalAccessException {
//獲得字節(jié)碼對象
Class c= test.getClass();
//獲得屬性
Field name=c.getDeclaredField("name");
Field age=c.getDeclaredField("age");
//允許暴力反射
name.setAccessible(true);
age.setAccessible(true);
/**
* 獲取注解 這里對應(yīng)的是我們自定義的注解類撵孤,一定不要搞錯了 一一對應(yīng)的哦
* 比如我們這里用到的是上方定義的BindView 注解迈着,那我們需要獲取到的注解對象
* 就是BindView ,getAnnotation(BindView.class)中傳入的參數(shù)自然也就是
* BindView
*/
BindView bindView=name.getAnnotation(BindView.class);
if(bindView != null){
//如果注解不是空 則可以獲取注解值
String uName=bindView.name();
int uAge=bindView.age();
//然后將值設(shè)置到我們的對象上
name.set(test,uName);
age.set(test,uAge);
}else{
//注解為空
}
}
}
//接下來我們在activity中來調(diào)用看看:
//先定義對象
Test t=new Test();
try {
//我們的Butterknife用起來 有木有很順手邪码,很hi裕菠。寫起來都是一個感覺
Butterknife.bind(t);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//這時候我們可以來看看注解上的值是否成功的傳遞到了對象中
Toast.makeText(this,t.toString(),Toast.LENGTH_SHORT).show();
上方的結(jié)果經(jīng)測試,是ok的霞扬,那么到這里位置糕韧,反射和注解我們就都了解啦枫振,簡單中藏著奧妙喻圃,舒服。
接下來就是重頭戲啦粪滤,自己實現(xiàn)一個簡單的類似于Butterknife的功能,廢話不多說斧拍,動起來:
首先我們實現(xiàn)通過@BindView(R.id.xx),實現(xiàn)獲取控件的實例,并得到控件的值(省去findviewbyid)
第一步:先定義BindView注解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
// 如果注解中只有一個屬性杖小,可以直接命名為“value”肆汹,使用時無需再標(biāo)明屬性名。
//@BindView(R.id.y)
int value();
}
//這里可以看看跟上方BindView類中的區(qū)別予权,加深理解
第二步:自定義Butterknife類昂勉,在類中來獲取我們需要的值,這里與上方不同的一點就是我們傳入的對象應(yīng)該是activity.另外就是大家可以多注意這里面是如何來找到我們的控件扫腺,做到不需要我們在activity中findviewbyid的岗照。
public class Butterknife {
public static void bind(Activity activity) {
try {
bindView(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 綁定視圖view
*/
private static void bindView(Activity activity) throws IllegalAccessException {
//獲得字節(jié)碼
Class clazz=activity.getClass();
//獲得activity中的所有變量
Field[] fields=clazz.getDeclaredFields();
//循環(huán)獲取
for(Field field:fields){
//允許暴力反射
field.setAccessible(true);
//獲取變量上加的注解
BindView bindView=field.getAnnotation(BindView.class);
if(bindView != null){
//注解不為空的情況下 獲取注解的值 這里實際上是找到當(dāng)前控件對應(yīng)的ID
int id=bindView.value();
//通過id來獲取控件
View view=activity.findViewById(id);
//將控件的值賦值給變量
field.set(activity,view);
}else{
}
}
}
}
//到了這里我們就可以在activity中調(diào)用了:
Butterknife.bind(this);
//輸出下是否獲得到了控件的值
Toast.makeText(this,textView1.getText().toString(),Toast.LENGTH_SHORT).show();
那么這里也可以成功得到textView1這個控件的值,這樣我們就成功了一 半笆环,實現(xiàn)了這種簡單的控件綁定效果攒至,而不需要通過findviewbyid來獲取控件實例了;
下面我們再來實現(xiàn) butterknife綁定點擊事件的效果(就是butterknife中的@OnClick(R.id.xx)):
首先同樣的我們這里 新建一個針對點擊事件的注解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyClick {
int value();
}
接下來我們就該思考怎么通過這個注解來設(shè)置它的點擊事件:我們想想上方的思路躁劣,肯定也是通過反射來得到當(dāng)前控件的id迫吐,然后通過在我們自定義的Butterknfe封裝類中獲得該id的控件實例,有了控件實例账忘,這時候我們是不是就可以給這個獲得的實例對象添加點擊事件監(jiān)聽啦志膀?
所以我們回到Butterknfe類中熙宇,加入一個bindClick方法(記得bind()方法中加入調(diào)用):綁定我們的監(jiān)聽事件:
/**
* 點擊事件
*/
private static void bindClick(final Activity activity){
Class<? extends Activity> c=activity.getClass();
//獲得方法
Method[] methods=c.getDeclaredMethods();
for(final Method method:methods){
//允許暴力反射
method.setAccessible(true);
//獲得注解
MyClick view= method.getAnnotation(MyClick.class);
if(view != null){
//獲得控件id
int id=view.value();
View v=activity.findViewById(id);
//點擊事件
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
//調(diào)用方法 這里方法沒帶參數(shù) 所以不用傳遞
method.invoke(activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}else{
}
}
}
這樣就成功的設(shè)置了監(jiān)聽事件啦,我們看看activity中的使用:同樣是跟我們熟知的Butterknife是同樣的使用方式:
/**
* 自定義 點擊事件注解
*/
@MyClick(R.id.button)
public void onClick(){
Log.e("aaa","onClick");
Toast.makeText(ButterknifeActivity.this,"點擊了按
鈕",Toast.LENGTH_SHORT).show();
}
經(jīng)測試一切正常梧却,大功告成奇颠。
一路走下來是不是發(fā)現(xiàn)其實特別的簡單,并不復(fù)雜(當(dāng)然我們工作中所使用到的Butterknife庫遠(yuǎn)遠(yuǎn)不止這么多的邏輯)放航,開不開心烈拒?希望看到這,能夠讓你基本的懂得了反射广鳍,注解荆几,以及結(jié)合這兩者如何實現(xiàn)類似于butterknife的功能。感謝您的觀看赊时,有問題歡迎留言指教吨铸!
最后附上傳送門,需要的朋友可以參考下:不過建議自己寫一波祖秒,這樣比較好诞吱;
源碼傳送門.