Android註解及反射實戰–手寫ButterKnife
Android進階系列
知識點總結,整理也是學習的過程,如有錯誤,歡迎批評指出。
本文內容涉及到註解,反射,動態代理等知識點,對這部分不太熟悉的可以看看以下文章
一、前言
本篇內容主要是對前面註解,反射及動態代理知識點的實戰,相當於進行一個簡單的總結,手寫一個簡易版本的ButterKnifeDemo,這部分用了大量的反射,肯定會影響一定的性能,但是ButterKnife庫的實現是通過編譯期間生成輔助代碼來達到View注入的目的,感興趣的可以去看看它的源碼,後面有時間,我也會整理一份出來。
二、ButterKnife簡單介紹
ButterKnife
這個庫學習門檻不高,在項目中使用能節省很多沒必要的代碼,不用一直在那裏findviewByid
,再結合Android ButterKnife Zelezny
這個插件,真的不要太香,好了,我們看一下下面這段代碼,基本上覆蓋了ButterKnife
的使用了。
我們可以看到,平時的findViewById()
操作直接通過@BindView
註解替代了,各種點擊事件也被
對應的註解替代,當然,要想讓上面的代碼能實現其對應功能,下面這行代碼是關鍵。
ButterKnife.bind(this);
通過這行代碼,對各種視圖進行綁定。
三、開整
好了,直接開整,手寫一個簡單的ButterKnife,實現和第三方庫ButterKnife差不多的功能。
3.1 BindLayoutId
我們先寫一個 BindLayoutId
,通過註解來注入當前Activity的佈局,不用再去通過setContentView()
方法實現。
要實現這個功能,我們首先肯定要定義一個註解,能作用在 Activity
上,並且能通過屬性設置佈局Id。
1、定義
BindLayoutId
註解
首先通過Target
設置這個註解使用在類上,生命週期保存到運行階段,由於要傳入一個佈局id,所以成員變量定義一個Int類型。
2、註解使用
上面的操作,通過註解,綁定了id,但是這只是一個表象,目前還沒有任何效果,因爲我們知道我們設置佈局都是通過setContentView(xxxx)
來完成,所以,我們需要拿到BindLayoutId
裏面的id,在通過反射執行setContentView(xxxx)
來執行真正的操作。
3、反射執行。
這一步是具體邏輯的實現,比較關鍵,我一步一步拆分開來說。
首先我們要明白這一步要做什麼。
- 肯定是要拿到我們需要處理的
Activity
,只有通過這個Activity
,我們才能拿到他上面的註解,並拿到註解信息,還有反射執行這個Activity
的setContentView(xxxx)
方法。
public class MyButterKnifeUtil {
private static final String TAG = "MyButterKnifeUtil";
/**
* 對註解信息進行處理
*
* @param activity 需要操作的Activity
*/
public static void injectLayoutId(Activity activity) {
}
}
我們定義了一個工具類,並定義injectLayoutId
方法,通過參數我們能拿到需要處理的Activity
Activity
拿到了,我們肯定先要拿到這個Activity上得BindLayoutId
註解,並拿到註解上的屬性及佈局ID
// 1、反射執行,先拿到需要處理的Activity的Class的對象
Class classzz = activity.getClass();
// 判斷是否有BindLayoutId這個註解
boolean isBindLayoutId = classzz.isAnnotationPresent(BindLayoutId.class);
if (isBindLayoutId) {
// 獲取到註解對象
BindLayoutId bindLayoutIdzz = (BindLayoutId)
classzz.getAnnotation(BindLayoutId.class);
// 拿到我們註解對象的成員變量值,及屬性id
int layoutId = bindLayoutIdzz.value();
LogUtil.d(TAG + "--injectLayoutId layoutId=" + layoutId);
}
我們可以看到,我們通過反射操作,就拿到了我們設置的佈局Id。
- 拿到Id後,我們下一步肯定要執行
Activity
的setContentView
方法,我們已經通過傳入的參數拿到了Activity,那執行他的方法,直接通過反射不就Ok了!
try {
// 反射拿到setContentView()方法
Method setContentViewMethod = classzz.getMethod("setContentView", int.class);
// 執行方法
setContentViewMethod.invoke(activity, layoutId);
} catch ( Exception e ) {
LogUtil.e(TAG + "--injectLayoutId error=" + e.getMessage());
e.printStackTrace();
}
貼一下完整代碼:
好了,具體執行邏輯實現了,我們只需要在Activity裏面注入就大功告成。
注入
在對應的activity中進行注入,這樣,我們的佈局id就通過註解的方式添加了。
MyButterKnifeUtil.injectLayoutId(this);
程序運行,可以看到我們的佈局通過BindLayoutId
成功注入。
3.2 MyBindView
上面實現了對佈局ID的注入,我們現在來實現對控件ID的注入,基本步驟跟上面一樣。
1、定義
MyBindView
註解
對控件id的註解使用在屬性上,所以我們這裏@Target
使用了ElementType.FIELD
2、註解的使用
3、反射執行邏輯,思路和
BindLayoutId
基本一致,我們新建方法injectViewId
public static void injectViewId(Activity activity) {
/**
* 思路:
* 1、我們首先要拿到當前Activity上被MyBindView這個註解註解的所有控件
* 並且拿到註解中的屬性信息(控件id)
* 2、反射執行Activity中的findViewById()方法,傳入我們的id。
*/
// 1、反射執行,先拿到需要處理的Activity的Class的對象
Class classzz = activity.getClass();
// 2、拿到當前Activity中所有的成員變量
Field[] fields = classzz.getDeclaredFields();
for (Field field : fields) {
// 遍歷獲取當前成員變量是否有MyBindView註解修飾
boolean isMyBindView = field.isAnnotationPresent(MyBindView.class);
LogUtil.d(TAG + "--injectViewId isMyBindView=" + isMyBindView);
if (!isMyBindView) {
// 沒有MyBindView註解修飾的成員變量直接過濾掉。
continue;
}
// 通過成員變量拿到MyBindView註解對象
MyBindView myBindViewzz = field.getAnnotation(MyBindView.class);
// 拿到註解的成員變量及控件Id
int myViewId = myBindViewzz.value();
LogUtil.d(TAG + "--injectViewId id=" + myViewId);
try {
// 通過反射,執行Activity中的findViewById()
Method method = classzz.getMethod("findViewById", int.class);
// 反射執行,並拿到返回的控件對象
// =View view=mainActivity.findViewById(valueId);
View view = (View) method.invoke(activity, myViewId);
// 賦值,上面我們反射執行,已經通過id拿到了實際的控件對象,需要對我們
// 獲取到的控件的成員變量進行賦值
field.setAccessible(true);
field.set(activity, view);
} catch (Exception e) {
e.printStackTrace();
LogUtil.e(TAG + "--injectViewId error=" + e.getMessage());
}
}
}
通過
MyButterKnifeUtil.injectViewId(this)
注入到Activity中,運行結果如下。
可以看到控件成功進行設置,說明我們的控件注入ok。
3.3 事件處理(OnClick,onLongClick)
上面兩個處理比較簡單了,大同小異,但是事件處理這部分相對來說會複雜一點,其中也會涉及到動態代理部分,我儘量每步往詳細了走。
當然,在開整之前,要先分析一下我們要做的工作。
思路整理:
1、基於我們前面MyBindView
的思路,首先肯定要通過註解拿到實際的控件對象(通過反射);
2、拿到控件後,要動態對應的處理執行各種事件(點擊、長按等)。
3、執行後需要將方法回調給用戶自己處理(動態代理)
上圖ABCD幾個參數,都是需要我們處理的,其中拿到控件對象,我們上一個示例已經走了一遍,要想讓事件處理這部分更完善,兼容不同的觸發事件,BCD這個三個動態的參數,我們可以新建一個註解來綁定。
我們先定義一個BaseEvent註解,來動態管理這三個參數,後續方便對各種監聽事件的拓展。
注意這個註解的
@Target
爲ANNOTATION_TYPE
,及註解在註解上。
新建我們點擊事件的註解,如下:
使用:
同樣的,上面只是注入,真正的實現邏輯需要我們來實現,同樣在MyButterKnifeUtil
中新增方法來實現我們的邏輯。
public void injectListener(Activity activity) {}
}
1、首先,要獲取當前Activity的所有方法,遍歷獲取方法上的所有註解,拿到註解的Class對象,就可以通過反射獲取BaseEvent註解,來判斷當前註解是否是事件處理註解,然後對其進行操作。
2、拿到了註解的Class對象,我們可以反射獲取其方法,並反射執行,拿到返回值,及設置的id數組,通過id,可以反射拿到這個控件View
3、我們拿到了控件對象,又通過BaseEvent的屬性拿到了事件的方法等各種參數,但是有一個問題,就是我們並不能直接通過反射Activity中的方法來執行(method.invoke(activity, view))直接回調,因爲我們需要在按鈕實際被點擊後再回調,而這個步驟就需要用到動態代理來實現了。
我們先創建一個動態代理類。
關於動態代理知識點,這裏不做詳細介紹,不清楚的可以先去了解一下,通過動態代理,當用戶事件觸發的時候,回調事件就會走到invoke方法來,我們在動態代理的invoke方法中,去執行了Activity中實際的方法。
我們將動態代理與事件進行綁定。
完整代碼
public static void injectListener(Activity activity) {
Class<?> classzz = activity.getClass();
// 反射獲取所有方法
Method[] methods = classzz.getDeclaredMethods();
// 遍歷獲取當前Activity中所有方法
for (Method method : methods) {
// 拿到每個方法上的所有註解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
// 通過annotationType方法拿到annotation的Class對象,
Class<?> annotationzz = annotation.annotationType();
// 通過annotationClass反射獲取其BaseEvent註解
BaseEvent baseEvent = annotationzz.getAnnotation(BaseEvent.class);
if (baseEvent == null) {
continue;
}
// 拿到baseEvent註解,獲取其所有成員變量。
String listenerSetter = baseEvent.listenerSetter();
Class<?> listenerType = baseEvent.listenerType();
String callBackMethod = baseEvent.callBackMethod();
try {
// 通過annotationzz反射獲取其成員變量
Method declaredMethod = annotationzz.getDeclaredMethod("value");
// 反射方法執行
int[] ids = (int[]) declaredMethod.invoke(annotation);
if (ids == null) {
continue;
}
for (int id : ids) {
Method findViewById = classzz.getMethod("findViewById", int.class);
// 拿到具體的控件View
View view = (View) findViewById.invoke(activity, id);
LogUtil.d(TAG + "--injectListener id=" + id);
if (view == null) {
continue;
}
// 通過動態代理事件,將用戶操作後的事件交給代理類,再在代理類中讓Activity反射執行。
ListenerInvocationHandler listenerInvocationHandler
= new ListenerInvocationHandler(activity, method);
// 做代理對象,eg:new View.OnClickListener()對象
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader()
, new Class[]{listenerType}, listenerInvocationHandler);
// 獲取到執行方法,eg:setOnClickListener
Method listenerSetterMethod = view.getClass()
.getMethod(listenerSetter, listenerType);
// 方法反射執行 eg:view.setOnClickListener(new View.OnClickListener(){})
listenerSetterMethod.invoke(view, proxy);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
邏輯處理好後,我們進行注入,然後運行,結果如下:
如果我們要定義長按事件,只需要更改BaseEvent裏面的事件就可以了
結果:
四、總結
這個簡易ButterKnife的項目實戰將前面的註解,反射,動態代理的知識點都用上了,這個還是一個非常非常簡單的demo了,我們常用的第三方庫其實用了很多很多的知識點,所以,一些小的知識點我們也不能忽略,都要去學習整理,這樣後面在看其他優秀庫的源碼的時候,纔不會感覺那麼懵逼,所以,一起監督加油。