通過反射實現的仿ButterKnife功能Demo [復制鏈接]

2019-6-17 10:14
EmailLi 閱讀:370 評論:0 贊:0
Tag:  ButterKnife Demo 反射
用過ButterKnife的朋友都知道,ButterKnife可以使用@BindView和@OnClick等注解就可以省略掉繁瑣的findViewById和setOnClickListener等代碼,使得業務代碼更加簡潔清晰。對ButterKnife不熟悉的朋友可以看下:(http://jakewharton.github.io/butterknife/)

在參考過一些資料過后,我也模仿地寫了一個通過反射實現依賴注入的例子(ButterKnife是通過編譯期的注解實現的注入,下一次再寫一個編譯期注解實現的例子)

本文從布局注入、控件注入、事件注入從易到難來講解。

布局注入 
首先定義注解: 
(如果對于注解不熟悉,可以看下:http://www.lpfmpm.tw/blog-822721-80228.html)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BindLayout {
    int value();
}

@Retention指定為RUNTIME:表示這個注解會被JVM獲取,并在運行時通過反射獲取。 
@Target指定為TYPE表明該注解作用于類。 
int value()是表示該注解接受一個int參數,這里是布局的id。

定義一個方法解析該注解:

private static void injectLayout(Context context) {
        int layoutId;
        Class<? extends Context> contextClass = context.getClass();
        //得到注解
        BindLayout bindLayout = contextClass.getAnnotation(BindLayout.class);
        if (bindLayout != null) {
            //如果context對象有被BindLayout 注解
            //獲得被注解的布局id
            layoutId = bindLayout.value();
            try {
                //反射調用setContentView將布局設置進去
                Method method = contextClass.getMethod("setContentView", int.class);
                method.invoke(context, layoutId);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

看代碼加注釋很容易明白。

使用該注解:

 @BindLayout(R.layout.activity_main)
public class MainActivity extends BaseActivity {

給MainActivity 加個@BindLayout注解并指定布局的id,然后在BaseActivity 的onCreate加入 InjectUtil.injectLayout(this);(把injectLayout方法放到注入工具類InjectUtil中):

protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtil.injectLayout(this);
    }

之所以放在BaseActivity 是為了復用這行代碼。

這樣就完成了布局的注入,實際上就是通過反射解析注解再調用setContentView方法取代了原來的一行setContentView方法。

控件的注入: 
基本和布局注入差不多。 
首先定義注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

可以看出該注解通過運行時反射獲取,并作用于成員變量上。

相應的注解解析方法:

 private static void injectView(Context context) {
        Class<?> contextClass = context.getClass();
        Field[] fields = contextClass.getDeclaredFields();
        //遍歷context對象的所有成員變量,找到有注解InjectView的變量
        for (Field field : fields) {
            int fieldId;
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                //得到該控件的id
                fieldId = bindView.value();
                try {
                    //反射調用findViewById得到控件的對象
                    Method method = contextClass.getMethod("findViewById", int.class);
                    View view = (View) method.invoke(context, fieldId);
                    field.setAccessible(true);
                    field.set(context, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

也是解析注解再通過反射調用findViewById完成控件的注入。

注意該方法要在布局注入之后才可以調用,否則拿不到控件。就像findViewById要在setContentView之后調用一樣。

使用該注解很簡單:

 @BindView(R.id.text)
 TextView mTextView;

當然BaseActivity需要添加注入代碼:

protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtil.injectLayout(this);
        InjectUtil.injectView(this);
    }

控件注入完事~

事件注入: 
最難的一部分到了。 
難點主要有二: 
1.事件種類多種,比如點擊、長按、點擊列表項等,如何統一處理這些事件的注解。 
2.如何在反射中完成設置事件監聽接口并回調被注解的方法。

針對問題1,解決方法是自定義一個元注解(注解的注解)來聲明事件要素。

事件一般分為三要素:事件源、事件、回調 
比如:

Button btn = (Button) findViewById(R.id.myButton);
btn .setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
//do something
         }
    });

這里事件源為setOnClickListener,事件為View.OnClickListener,回調為onClick方法。

所以根據這三要素去定義的元注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {
    String listenerSetter();
    Class<?> listenType();
    String callBackMethod();
}

所需要的參數為這三要素。 
@Target為ANNOTATION_TYPE表示用來作為注解的注解。

然后比如點擊事件可以這樣定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",listenType = View.OnClickListener.class,callBackMethod = "onClick")
public @interface OnClick {
    //設置該事件監聽的view的id
    int[] value() default -1;
}

將事件三要素拆分開的目的是為了通過反射注解得到事件三要素,再通過三要素去通過反射完成類似view.setOnClickListener(new View.OnClickListener()…的代碼。

但是如何在Activity沒有實現任何類似OnClickListener接口的情況下在點擊了對應的View的時候去回調相應的onClick方法呢?

這里看了XX學院(避免廣告之嫌。。)的視頻得到了啟發,使用動態代理巧妙解決。

(不熟悉動態代理的可以看下這文章 Java 動態代理機制分析及擴展)

我們知道動態代理的代理對像一旦被調用就會調用對應的InvocationHandler實現類對象的invoke方法,所以這里可以創建一個實現OnClickListener的對象去代理context對象(一般是Activity),然后作為參數傳入view.setOnClickListener中。然后在view被點擊的時候就會調用代理對像的onClick方法,從而調用自定義的InvocationHandler的invoke方法。所以只要在invoke方法中添加context對象中的擁有我們的事件注解的方法就可以了。

注入事件的代碼:

 private static void injectListeners(Context context) {
        Class<?> contextClass = context.getClass();
        //獲取當前類(Activity)中所有的方法
        Method[] methods = contextClass.getDeclaredMethods();
        for (Method method : methods) {
            //遍歷每個方法的注解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                Class<?> annotationType = annotation.annotationType();
                EventBase evenBase = annotationType.getAnnotation(EventBase.class);
                if (evenBase != null) {
                    //找出有EventBase的注解的方法,取出對應的EventBase中的事件三要素
                    String listenerSetter = evenBase.listenerSetter();
                    Class<?> listenerType = evenBase.listenType();
                    String callBackMethodName = evenBase.callBackMethod();

                    //將注解的方法和相應的callback方法名綁定。例如:@OnClick注解運用在a方法上,則將a方法的Method對象和“onClick”綁定一起
                    HashMap<String, Method> methodMap = new HashMap<>();
                    methodMap.put(callBackMethodName, method);

                    //動態代理的InvocationHandler
                    ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, methodMap);
                    try {
                        //取出事件注解的方法注入的控件id
                        Method methodGetIds = annotationType.getDeclaredMethod("value");
                        int[] ids = (int[]) methodGetIds.invoke(annotation);
                        for (int viewId : ids) {
                            //反射得到對應的控件對象
                            Method methodFindViewById = contextClass.getMethod("findViewById", int.class);
                            View view = (View) methodFindViewById.invoke(context, viewId);
                            //得到對應的setter方法(比如setOnClickListener)
                            Method methodSetListener = view.getClass().getMethod(listenerSetter, listenerType);
                            //通過動態代理生成一個代理Context對象的代理對象,使得當view被點擊的時候,觸發代理對象調用context對象對應的
                            // 那個被注解的方法
                            Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType},
                                    listenerInvocationHandler);
                            //假如注解為@OnClick,則相當于view(id對應的控件).setOnClickListener(proxy)
                            // ,這里proxy為一個實現了OnClickListener的代理類。以此類推
                            methodSetListener.invoke(view, proxy);
                        }
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

其中ListenerInvocationHandler的代碼:

public class ListenerInvocationHandler implements InvocationHandler{
    private Context mContext;
    private Map<String,Method> mMethodMap;

    public ListenerInvocationHandler(Context context, Map<String, Method> methodMap) {
        mContext = context;
        mMethodMap = methodMap;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Method methodClick = mMethodMap.get(methodName);
        if (methodClick != null){
            return methodClick.invoke(mContext,args);
        }
        return null;
    }
}

使用事件注入: 
首先BaseActivity需要添加注入代碼:

protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtil.injectLayout(this);
        InjectUtil.injectView(this);
        InjectUtil.injectListeners(this);
    }

MainActivity中:
 @OnClick(R.id.text)
    public void click(View view){
        Toast.makeText(MainActivity.this, "點到了", Toast.LENGTH_SHORT).show();
    }

同理長按事件也可以這樣定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnLongClickListener", listenType = View.OnLongClickListener.class, callBackMethod = "onLongClick")
public @interface OnLongClick {
    //設置該事件監聽的view的id
    int[] value() default -1;
}

使用和OnClick基本一致。

這樣子簡單的ButterKnife功能Demo就完成了~


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

黑龙江彩票网