Android註解及反射實戰--手寫ButterKnife

Android註解及反射實戰--手寫ButterKnife

Android進階系列java

知識點總結,整理也是學習的過程,若有錯誤,歡迎批評指出。
本文內容涉及到註解,反射,動態代理等知識點,對這部分不太熟悉的能夠看看如下文章android

Java反射以及在Android中的使用
代理模式以及在Android中的使用
Java註解基礎介紹及使用web

1、前言

本篇內容主要是對前面註解,反射及動態代理知識點的實戰,至關於進行一個簡單的總結,手寫一個簡易版本的ButterKnifeDemo,這部分用了大量的反射,確定會影響必定的性能,可是ButterKnife庫的實現是經過編譯期間生成輔助代碼來達到View注入的目的,感興趣的能夠去看看它的源碼,後面有時間,我也會整理一份出來。數組

2、ButterKnife簡單介紹

ButterKnife這個庫學習門檻不高,在項目中使用能節省不少不必的代碼,不用一直在那裏findviewByid,再結合Android ButterKnife Zelezny這個插件,真的不要太香,好了,咱們看一下下面這段代碼,基本上覆蓋了ButterKnife的使用了。app

咱們能夠看到,平時的findViewById() 操做直接經過@BindView註解替代了,各類點擊事件也被
對應的註解替代,固然,要想讓上面的代碼能實現其對應功能,下面這行代碼是關鍵。工具

1ButterKnife.bind(this);
複製代碼

經過這行代碼,對各類視圖進行綁定。佈局

3、開整

好了,直接開整,手寫一個簡單的ButterKnife,實現和第三方庫ButterKnife差很少的功能。
post

3.1 BindLayoutId

咱們先寫一個 BindLayoutId,經過註解來注入當前Activity的佈局,不用再去經過setContentView()方法實現。性能

要實現這個功能,咱們首先確定要定義一個註解,能做用在 Activity上,而且能經過屬性設置佈局Id。學習

一、定義BindLayoutId註解

首先經過Target設置這個註解使用在類上,生命週期保存到運行階段,因爲要傳入一個佈局id,因此成員變量定義一個Int類型。

二、註解使用

上面的操做,經過註解,綁定了id,可是這只是一個表象,目前尚未任何效果,由於咱們知道咱們設置佈局都是經過setContentView(xxxx) 來完成,因此,咱們須要拿到BindLayoutId裏面的id,在經過反射執行setContentView(xxxx)來執行真正的操做。

三、反射執行。

這一步是具體邏輯的實現,比較關鍵,我一步一步拆分開來講。
首先咱們要明白這一步要作什麼。

  • 確定是要拿到咱們須要處理的Activity,只有經過這個Activity,咱們才能拿到他上面的註解,並拿到註解信息,還有反射執行這個ActivitysetContentView(xxxx) 方法。
 1public class MyButterKnifeUtil {
2
3    private static final String TAG = "MyButterKnifeUtil";
4    /**
5     * 對註解信息進行處理
6     *
7     * @param activity 須要操做的Activity
8     */

9    public static void injectLayoutId(Activity activity) {
10
11   }
12 }
複製代碼

咱們定義了一個工具類,並定義injectLayoutId方法,經過參數咱們能拿到須要處理的Activity

  • Activity 拿到了,咱們確定先要拿到這個Activity上得BindLayoutId註解,並拿到註解上的屬性及佈局ID
 1// 一、反射執行,先拿到須要處理的Activity的Class的對象
2Class classzz = activity.getClass();
3// 判斷是否有BindLayoutId這個註解
4boolean isBindLayoutId = classzz.isAnnotationPresent(BindLayoutId.class);
5if (isBindLayoutId) {    
6// 獲取到註解對象    
7BindLayoutId bindLayoutIdzz = (BindLayoutId) 
8classzz.getAnnotation(BindLayoutId.class);    
9// 拿到咱們註解對象的成員變量值,及屬性id    
10int layoutId = bindLayoutIdzz.value();   
11LogUtil.d(TAG + "--injectLayoutId  layoutId=" + layoutId);   
12}
複製代碼

咱們能夠看到,咱們經過反射操做,就拿到了咱們設置的佈局Id。

  • 拿到Id後,咱們下一步確定要執行ActivitysetContentView方法,咱們已經經過傳入的參數拿到了Activity,那執行他的方法,直接經過反射不就Ok了!
1try {  
2// 反射拿到setContentView()方法
3Method setContentViewMethod = classzz.getMethod("setContentView"int.class);   
4// 執行方法    
5setContentViewMethod.invoke(activity, layoutId);
6catch ( Exception e ) {  
7LogUtil.e(TAG + "--injectLayoutId  error=" + e.getMessage());   
8 e.printStackTrace();
9}
複製代碼

貼一下完整代碼:

好了,具體執行邏輯實現了,咱們只須要在Activity裏面注入就大功告成。

注入

在對應的activity中進行注入,這樣,咱們的佈局id就經過註解的方式添加了。

1MyButterKnifeUtil.injectLayoutId(this);
複製代碼

程序運行,能夠看到咱們的佈局經過BindLayoutId成功注入。

3.2 MyBindView

上面實現了對佈局ID的注入,咱們如今來實現對控件ID的注入,基本步驟跟上面同樣。

一、定義MyBindView註解

對控件id的註解使用在屬性上,因此咱們這裏@Target使用了ElementType.FIELD

二、註解的使用

三、反射執行邏輯,思路和BindLayoutId基本一致,咱們新建方法injectViewId

 1    public static void injectViewId(Activity activity) {
2        /**
3         * 思路:
4         * 一、咱們首先要拿到當前Activity上被MyBindView這個註解註解的全部控件
5         *    而且拿到註解中的屬性信息(控件id)
6         * 二、反射執行Activity中的findViewById()方法,傳入咱們的id。
7         */

8        // 一、反射執行,先拿到須要處理的Activity的Class的對象
9        Class classzz = activity.getClass();
10        // 二、拿到當前Activity中全部的成員變量
11        Field[] fields = classzz.getDeclaredFields();
12        for (Field field : fields) {
13            // 遍歷獲取當前成員變量是否有MyBindView註解修飾
14            boolean isMyBindView = field.isAnnotationPresent(MyBindView.class);
15            LogUtil.d(TAG + "--injectViewId  isMyBindView=" + isMyBindView);
16            if (!isMyBindView) {
17                // 沒有MyBindView註解修飾的成員變量直接過濾掉。
18                continue;
19            }
20            // 經過成員變量拿到MyBindView註解對象
21            MyBindView myBindViewzz = field.getAnnotation(MyBindView.class);
22            // 拿到註解的成員變量及控件Id
23            int myViewId = myBindViewzz.value();
24            LogUtil.d(TAG + "--injectViewId  id=" + myViewId);
25            try {
26                // 經過反射,執行Activity中的findViewById()
27                Method method = classzz.getMethod("findViewById"int.class);
28                // 反射執行,並拿到返回的控件對象
29                // =View view=mainActivity.findViewById(valueId);
30                View view = (View) method.invoke(activity, myViewId);
31                // 賦值,上面咱們反射執行,已經經過id拿到了實際的控件對象,須要對咱們
32                // 獲取到的控件的成員變量進行賦值
33                field.setAccessible(true);
34                field.set(activity, view);
35            } catch (Exception e) {
36                e.printStackTrace();
37                LogUtil.e(TAG + "--injectViewId  error=" + e.getMessage());
38            }
39        }
40    }
複製代碼

經過MyButterKnifeUtil.injectViewId(this)注入到Activity中,運行結果以下。

能夠看到控件成功進行設置,說明咱們的控件注入ok。

3.3 事件處理(OnClick,onLongClick)

上面兩個處理比較簡單了,大同小異,可是事件處理這部分相對來講會複雜一點,其中也會涉及到動態代理部分,我儘可能每步往詳細了走。

固然,在開整以前,要先分析一下咱們要作的工做。

思路整理:
一、基於咱們前面MyBindView的思路,首先確定要經過註解拿到實際的控件對象(經過反射);
二、拿到控件後,要動態對應的處理執行各類事件(點擊、長按等)。
三、執行後須要將方法回調給用戶本身處理(動態代理)

上圖ABCD幾個參數,都是須要咱們處理的,其中拿到控件對象,咱們上一個示例已經走了一遍,要想讓事件處理這部分更完善,兼容不一樣的觸發事件,BCD這個三個動態的參數,咱們能夠新建一個註解來綁定。


咱們先定義一個BaseEvent註解,來動態管理這三個參數,後續方便對各類監聽事件的拓展。

注意這個註解的@TargetANNOTATION_TYPE,及註解在註解上。

新建咱們點擊事件的註解,以下:

使用:

一樣的,上面只是注入,真正的實現邏輯須要咱們來實現,一樣在MyButterKnifeUtil中新增方法來實現咱們的邏輯。

1public void injectListener(Activity activity) {}
2
3}
複製代碼

一、首先,要獲取當前Activity的全部方法,遍歷獲取方法上的全部註解,拿到註解的Class對象,就能夠經過反射獲取BaseEvent註解,來判斷當前註解是不是事件處理註解,而後對其進行操做。

二、拿到了註解的Class對象,咱們能夠反射獲取其方法,並反射執行,拿到返回值,及設置的id數組,經過id,能夠反射拿到這個控件View

三、咱們拿到了控件對象,又經過BaseEvent的屬性拿到了事件的方法等各類參數,可是有一個問題,就是咱們並不能直接經過反射Activity中的方法來執行(method.invoke(activity, view))直接回調,由於咱們須要在按鈕實際被點擊後再回調,而這個步驟就須要用到動態代理來實現了。

咱們先建立一個動態代理類。

關於動態代理知識點,這裏不作詳細介紹,不清楚的能夠先去了解一下,經過動態代理,當用戶事件觸發的時候,回調事件就會走到invoke方法來,咱們在動態代理的invoke方法中,去執行了Activity中實際的方法。

咱們將動態代理與事件進行綁定。

完整代碼

 1   public static void injectListener(Activity activity) {
2        Class<?> classzz = activity.getClass();
3        // 反射獲取全部方法
4        Method[] methods = classzz.getDeclaredMethods();
5        // 遍歷獲取當前Activity中全部方法
6        for (Method method : methods) {
7            // 拿到每一個方法上的全部註解
8            Annotation[] annotations = method.getAnnotations();
9            for (Annotation annotation : annotations) {
10                // 經過annotationType方法拿到annotation的Class對象,
11                Class<?> annotationzz = annotation.annotationType();
12                // 經過annotationClass反射獲取其BaseEvent註解
13                BaseEvent baseEvent = annotationzz.getAnnotation(BaseEvent.class);
14                if (baseEvent == null) {
15                    continue;
16                }
17                // 拿到baseEvent註解,獲取其全部成員變量。
18                String listenerSetter = baseEvent.listenerSetter();
19                Class<?> listenerType = baseEvent.listenerType();
20                String callBackMethod = baseEvent.callBackMethod();
21
22                try {
23                    // 經過annotationzz反射獲取其成員變量
24                    Method declaredMethod = annotationzz.getDeclaredMethod("value");
25                    // 反射方法執行
26                    int[] ids = (int[]) declaredMethod.invoke(annotation);
27                    if (ids == null) {
28                        continue;
29                    }
30                    for (int id : ids) {
31                        Method findViewById = classzz.getMethod("findViewById"int.class);
32                        // 拿到具體的控件View
33                        View view = (View) findViewById.invoke(activity, id);
34                        LogUtil.d(TAG + "--injectListener  id=" + id);
35                        if (view == null) {
36                            continue;
37                        }
38
39                        // 經過動態代理事件,將用戶操做後的事件交給代理類,再在代理類中讓Activity反射執行。
40                        ListenerInvocationHandler listenerInvocationHandler
41                                = new ListenerInvocationHandler(activity, method);
42                        // 作代理對象,eg:new View.OnClickListener()對象
43                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader()
44                                , new Class[]{listenerType}, listenerInvocationHandler);
45
46                        // 獲取到執行方法,eg:setOnClickListener
47                        Method listenerSetterMethod = view.getClass()
48                                .getMethod(listenerSetter, listenerType);
49                        // 方法反射執行 eg:view.setOnClickListener(new View.OnClickListener(){})
50                        listenerSetterMethod.invoke(view, proxy);
51                    }
52                } catch (Exception e) {
53                    e.printStackTrace();
54                }
55            }
56
57        }
58    }
複製代碼

邏輯處理好後,咱們進行注入,而後運行,結果以下:

若是咱們要定義長按事件,只須要更改BaseEvent裏面的事件就能夠了

結果:

4、總結

這個簡易ButterKnife的項目實戰將前面的註解,反射,動態代理的知識點都用上了,這個仍是一個很是很是簡單的demo了,咱們經常使用的第三方庫其實用了不少不少的知識點,因此,一些小的知識點咱們也不能忽略,都要去學習整理,這樣後面在看其餘優秀庫的源碼的時候,纔不會感受那麼懵逼,因此,一塊兒監督加油。

相關文章
相關標籤/搜索