Android注解及反射实战--手写ButterKnife

Android注解及反射实战–手写ButterKnife

Android进阶系列

知识点总结,整理也是学习的过程,如有错误,欢迎批评指出。
本文内容涉及到注解,反射,动态代理等知识点,对这部分不太熟悉的可以看看以下文章

Java反射以及在Android中的使用
代理模式以及在Android中的使用
Java注解基础介绍及使用

一、前言

本篇内容主要是对前面注解,反射及动态代理知识点的实战,相当于进行一个简单的总结,手写一个简易版本的ButterKnifeDemo,这部分用了大量的反射,肯定会影响一定的性能,但是ButterKnife库的实现是通过编译期间生成辅助代码来达到View注入的目的,感兴趣的可以去看看它的源码,后面有时间,我也会整理一份出来。

二、ButterKnife简单介绍

ButterKnife这个库学习门槛不高,在项目中使用能节省很多没必要的代码,不用一直在那里findviewByid,再结合Android ButterKnife Zelezny这个插件,真的不要太香,好了,我们看一下下面这段代码,基本上覆盖了ButterKnife的使用了。

Android注解及反射实战--手写ButterKnife_第1张图片

我们可以看到,平时的findViewById() 操作直接通过@BindView注解替代了,各种点击事件也被
对应的注解替代,当然,要想让上面的代码能实现其对应功能,下面这行代码是关键。

ButterKnife.bind(this);

通过这行代码,对各种视图进行绑定。

三、开整

好了,直接开整,手写一个简单的ButterKnife,实现和第三方库ButterKnife差不多的功能。
Android注解及反射实战--手写ButterKnife_第2张图片

3.1 BindLayoutId

我们先写一个 BindLayoutId,通过注解来注入当前Activity的布局,不用再去通过setContentView()方法实现。

要实现这个功能,我们首先肯定要定义一个注解,能作用在 Activity上,并且能通过属性设置布局Id。

1、定义BindLayoutId注解

Android注解及反射实战--手写ButterKnife_第3张图片

首先通过Target设置这个注解使用在类上,生命周期保存到运行阶段,由于要传入一个布局id,所以成员变量定义一个Int类型。

2、注解使用

Android注解及反射实战--手写ButterKnife_第4张图片

上面的操作,通过注解,绑定了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();
}

贴一下完整代码:

Android注解及反射实战--手写ButterKnife_第5张图片

好了,具体执行逻辑实现了,我们只需要在Activity里面注入就大功告成。

注入

在对应的activity中进行注入,这样,我们的布局id就通过注解的方式添加了。

MyButterKnifeUtil.injectLayoutId(this);

程序运行,可以看到我们的布局通过BindLayoutId成功注入。
Android注解及反射实战--手写ButterKnife_第6张图片

3.2 MyBindView

上面实现了对布局ID的注入,我们现在来实现对控件ID的注入,基本步骤跟上面一样。

1、定义MyBindView注解

Android注解及反射实战--手写ButterKnife_第7张图片

对控件id的注解使用在属性上,所以我们这里@Target使用了ElementType.FIELD

2、注解的使用

Android注解及反射实战--手写ButterKnife_第8张图片

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中,运行结果如下。

Android注解及反射实战--手写ButterKnife_第9张图片

可以看到控件成功进行设置,说明我们的控件注入ok。
Android注解及反射实战--手写ButterKnife_第10张图片

3.3 事件处理(OnClick,onLongClick)

上面两个处理比较简单了,大同小异,但是事件处理这部分相对来说会复杂一点,其中也会涉及到动态代理部分,我尽量每步往详细了走。

当然,在开整之前,要先分析一下我们要做的工作。

思路整理:
1、基于我们前面MyBindView的思路,首先肯定要通过注解拿到实际的控件对象(通过反射);
2、拿到控件后,要动态对应的处理执行各种事件(点击、长按等)。
3、执行后需要将方法回调给用户自己处理(动态代理)

Android注解及反射实战--手写ButterKnife_第11张图片

上图ABCD几个参数,都是需要我们处理的,其中拿到控件对象,我们上一个示例已经走了一遍,要想让事件处理这部分更完善,兼容不同的触发事件,BCD这个三个动态的参数,我们可以新建一个注解来绑定。

Android注解及反射实战--手写ButterKnife_第12张图片
我们先定义一个BaseEvent注解,来动态管理这三个参数,后续方便对各种监听事件的拓展。

注意这个注解的@TargetANNOTATION_TYPE,及注解在注解上。

新建我们点击事件的注解,如下:
Android注解及反射实战--手写ButterKnife_第13张图片

使用:

Android注解及反射实战--手写ButterKnife_第14张图片

同样的,上面只是注入,真正的实现逻辑需要我们来实现,同样在MyButterKnifeUtil中新增方法来实现我们的逻辑。

public void injectListener(Activity activity) {}

}

1、首先,要获取当前Activity的所有方法,遍历获取方法上的所有注解,拿到注解的Class对象,就可以通过反射获取BaseEvent注解,来判断当前注解是否是事件处理注解,然后对其进行操作。

Android注解及反射实战--手写ButterKnife_第15张图片

2、拿到了注解的Class对象,我们可以反射获取其方法,并反射执行,拿到返回值,及设置的id数组,通过id,可以反射拿到这个控件View

Android注解及反射实战--手写ButterKnife_第16张图片

3、我们拿到了控件对象,又通过BaseEvent的属性拿到了事件的方法等各种参数,但是有一个问题,就是我们并不能直接通过反射Activity中的方法来执行(method.invoke(activity, view))直接回调,因为我们需要在按钮实际被点击后再回调,而这个步骤就需要用到动态代理来实现了。

我们先创建一个动态代理类。

Android注解及反射实战--手写ButterKnife_第17张图片

关于动态代理知识点,这里不做详细介绍,不清楚的可以先去了解一下,通过动态代理,当用户事件触发的时候,回调事件就会走到invoke方法来,我们在动态代理的invoke方法中,去执行了Activity中实际的方法。

我们将动态代理与事件进行绑定。
Android注解及反射实战--手写ButterKnife_第18张图片

完整代码

   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();
                }
            }

        }
    }

逻辑处理好后,我们进行注入,然后运行,结果如下:
Android注解及反射实战--手写ButterKnife_第19张图片

如果我们要定义长按事件,只需要更改BaseEvent里面的事件就可以了
Android注解及反射实战--手写ButterKnife_第20张图片

结果:
Android注解及反射实战--手写ButterKnife_第21张图片

四、总结

这个简易ButterKnife的项目实战将前面的注解,反射,动态代理的知识点都用上了,这个还是一个非常非常简单的demo了,我们常用的第三方库其实用了很多很多的知识点,所以,一些小的知识点我们也不能忽略,都要去学习整理,这样后面在看其他优秀库的源码的时候,才不会感觉那么懵逼,所以,一起监督加油。

你可能感兴趣的:(移动开发,android)