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

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

Android進階系列

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

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

一、前言

本篇內容主要是對前面註解,反射及動態代理知識點的實戰,相當於進行一個簡單的總結,手寫一個簡易版本的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,我們才能拿到他上面的註解,並拿到註解信息,還有反射執行這個ActivitysetContentView(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後,我們下一步肯定要執行ActivitysetContentView方法,我們已經通過傳入的參數拿到了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註解,來動態管理這三個參數,後續方便對各種監聽事件的拓展。

注意這個註解的@TargetANNOTATION_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了,我們常用的第三方庫其實用了很多很多的知識點,所以,一些小的知識點我們也不能忽略,都要去學習整理,這樣後面在看其他優秀庫的源碼的時候,纔不會感覺那麼懵逼,所以,一起監督加油。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章