注解与反射实现Butterknife

在Android开发中,在初始化的时候需要查找控件、为控件设置监听,因此我们经常会写如下的代码。

tvInfo = findViewById(R.id.tvInfo);
findViewById(R.id.btnChange).setOnClickListener(v->{

});

这种代码初看非常合理,这不是Android开发的固有套路么。但设想一下,要是页面的控件有几十个,需要设置的监听有几十个。嗯…是不是就感觉没那么科学了呢?随着技术的发展,Butterknife横空出世,解决了需要写一堆类似的代码的问题。相信做了一定时间的大家都用过这个库吧,非常的好用。今天我们先来用注解+反射的方式实现一个我们自己的Butterknife。声明:该文章仅是为了体现注解的妙用和架构的思维,这种性能是极低的。由于需要用到注解、反射,因此对这两部分的知识比较陌生的,可以先补习下基础知识。注解反射
先确定我们想要实现的效果。对成员变量控件,我们可以通过一个注解自动注入控件。对控件的事件监听,我们可以通过对方法的注解自动绑定事件。效果如下:

@BindView(R.id.tvInfo)
TextView tvInfo;
@BindView(R.id.btnChange)
Button btnChange;
当注解后,我们就可以直接调用tvInfo.setText("我是改变后的数据");这样的代码,而不用在写findViewById了。
@OnClick(R.id.btnChange)
public void onClick(View view){
    if(view.getId() == R.id.btnChange){
         tvInfo.setText("我是改变后的数据");
    }
}
对应事件监听,我们期望的是对方法注解后就自动进行事件绑定,不用在自己去写setOnClickListener了。

效果我们已经确定了,那该怎么实现呢?由于注解是被动使用的,永远不会主动调用。那可以想像一下,我们在onCreate中调用注入的方法,由该方法发起后续的注入能力,如:InjectUtil.inject(this);这样就可以注入了。先把我们的注解类写出来先。

/**
 * 绑定控件
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

这个注解是用于控件的注入的,使用在成员变量上。注解控件的方法如下,整体比较简单,就是反射findViewById这个方法实现变量赋值。

/**
     * 绑定View
     * @param context
     */
    private static void injectView(Object context){
        Class<?> clazz = context.getClass();
        //知识点getFields() 获取某个类的所有的公共的字段,包括父类中的字段
        //getDeclaredFields() 获取某个类的所有声明的字段,包括public、private、proteced,但不包括父类的申明字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            BindView annotation = field.getAnnotation(BindView.class);
            if(annotation != null){
                try {
                    int id = annotation.value();
                    Method findViewById = clazz.getMethod("findViewById",int.class);
                    Object view = findViewById.invoke(context,id);
                    if(view != null){
                        field.setAccessible(true);
                        field.set(context,view);
                    }
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

经过这个方法的注入,就实现了控件的自动注入赋值了,不需要我们在手动去写findViewById进行控件初始化了。

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseEvent {
    //事件方法
    String listenerSetter();
    //接口的父类
    String listenerClass();
    //接口类
    String listenerInteface();
}

/**
 * 单击事件
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BaseEvent(listenerSetter = "setOnClickListener",
        listenerClass = "android.view.View",
        listenerInteface = "android.view.View.OnClickListener")
public @interface OnClick {
    int[] value() default -1;
}

/**
 * 长按事件
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BaseEvent(listenerSetter = "setOnLongClickListener",
        listenerClass = "android.view.View",
        listenerInteface = "android.view.View.OnLongClickListener")
public @interface OnLongClick {
    int[] value() default -1;
}

这里做了单击、长按事假,因为这种事件有很多,这里抽象了个基础注解类,可理解为注解的多态,用来标记事件的几个要素。

findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                
            }
        });
listenerSetter对应setOnClickListener
listenerClass 这个是为了拿接口类的父类,对应android.view.View
listenerInteface 接口类,比如OnClickListener这个接口

到这里可能比较绕的是为什么要有这3个值的存在,最主要是后面两个。其实想一下我们设置监听的方式,View.setOnClickListener(View.OnClickListener)参数是一个接口,我们在反射setOnClickListener这个方法的时候要指定参数类,对应listenerInteface,但这个类是属于View类里面的,因此我们要先找到View这个类,具体可以看下注入的代码。

/**
     * 注入Click
     * @param context
     */
    private static void injectClick(Object context){
        Class<?> clazz = context.getClass();
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                Class<? extends Annotation> aClass = annotation.annotationType();
                BaseEvent baseEvent = aClass.getAnnotation(BaseEvent.class);
                //如果不存在BaseEvent的注解,那么说明不是需要处理的Click事件
                if(baseEvent == null){
                    continue;
                }
                String listenerSetter = baseEvent.listenerSetter();
                String listenerClass = baseEvent.listenerClass();
                String listenerInteface = baseEvent.listenerInteface();
                try {
                    Method value = aClass.getDeclaredMethod("value");
                    int[] ids = (int[])value.invoke(annotation);
                    for (int id : ids) {
                        //先根据id找到对应的控件
                        Method findViewById = clazz.getMethod("findViewById",int.class);
                        Object view = findViewById.invoke(context,id);
                        if(view == null){
                            continue;
                        }
                        ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context,method);
                        //先找View类
                        Class<?> viewClass = Class.forName(listenerClass);
                        //根据View类找内部的类
                        Class<?>[] declaredClasses = viewClass.getDeclaredClasses();
                        for (Class<?> declaredClass : declaredClasses) {
                        	//匹配对应的接口类
                            if(declaredClass.getCanonicalName().equals(listenerInteface)){
                                Object proxy = Proxy.newProxyInstance(declaredClass.getClassLoader(), new Class[]{declaredClass}, listenerInvocationHandler);
                                Method click = view.getClass().getMethod(listenerSetter,declaredClass);
                                click.invoke(view,proxy);
                                break;
                            }
                        }
                    }
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }

上面这段代码整体还是很好理解的,只是多出来了一个知识点:动态代理。对这个知识点不清晰的可以进去补习下。这样我们就将事件通过一个注解就注入进去了,不用在去写setOnClickListener这样的代码了。最后我们使用的方式就很简单了。

public class MainActivity extends AppCompatActivity{
    @BindView(R.id.tvInfo)
    TextView tvInfo;
    @BindView(R.id.btnChange)
    Button btnChange;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtil.inject(this);
//        tvInfo = findViewById(R.id.tvInfo);
//        findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View view) {
//
//            }
//        });
//        btnChange.setOnClickListener(view -> {
//            tvInfo.setText("我是改变后的数据");
//        });
    }
    @OnClick(R.id.btnChange)
    public void onClick(View view){
        if(view.getId() == R.id.btnChange){
            tvInfo.setText("我是改变后的数据");
        }
    }
    @OnLongClick(R.id.btnChange)
    public boolean onLongClick(View view){
        if(view.getId() == R.id.btnChange){
            tvInfo.setText("我是长按后改变后的数据");
        }
        return true;
    }
}

本篇只实现了控件的注入、简单事件的注入,有兴趣的可以在此基础上扩展。但目的我们是为了学习这种设计理念,并且掌握注解、反射这样的知识技巧,真实的开发中是不会使用这样损耗性能的方式的哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值