注解实现Butterknife注入功能

注解的核心是反射,就是定义了注解参数后,通过注入方法获取到注入的对象,查找相应的属性,如果包含注解信息,则可以拿到相应的value等属性,比如Filed属性 ,最终会通过field.invoke(obj1,obj2) 反射设置值,最终实现属性值的设定

1.注入View

定义一个view的注解,周期指定运行时,Filed属性

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

view注解比较简单,拿到注入的对象,直接反射获取值就可以

 private static void injectView(Object object) {
        try {
            Class<?> mClass = object.getClass();
            Field[] fields = mClass.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(BindView.class)) {
                    BindView bindView = field.getAnnotation(BindView.class);
                    int id = bindView.value();

                    Method findViewById = mClass.getMethod("findViewById", int.class);
                    View view = (View) findViewById.invoke(object, id);
                    field.set(object, view);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.点击事件注入

事件注入的核心是代理模式,我们常用的接口代理会这么写

 private static class ProxyFactory implements InvocationHandler {

        private Object object;

        ProxyFactory(Object object) {
            this.object = object;
        }

        public Object newPorxy() {
            return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), ProxyFactory.this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //dosomething
            Object result = method.invoke(object, args);
            //dosomething
            return result;
        }
    }

其中object会把接口的实现类以接口形式传入,最终编译生成出的字节码文件其实就算一个实现了接口的类文件

getInterfaces获取的是该类实现的接口,当只实现一个接口获取的那么就只有一个

比如,我定义了一个接口

 public interface IT{
        void test(String test);
    }

最终代理生成的文件格式大概是这样的

 public class $Proxy0 extends Proxy implements IT{

        private static Method m1,m2,m3; //会有很多,列出类及其父类的所以方法

        static {
            try {
                m1 = Class.forName("xx.xx.IT").getMethod("test",new Class[]{Class.forName("java.lang.String")});
                //所有方法都按照这个规则列出
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        protected $Proxy0(InvocationHandler h) {
            super(h);
        }

        @Override
        public void test(String test) {
            try {
                h.invoke(this,m1,new Object[]{test});
            }catch (Exception e){
                e.printStackTrace();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

其实就是帮我们生成了静态代理处理的类,代理的对象是接口的实现类,而我们点击事件OnClickListener实际也是一个接口,那么也可以通过代理方式实现

而我们的需求就是要知道点击触发的时机,然后动态反射到我们注解标记的方法中,那么就可以通过代理方式处理

先定义一个注解的事件处理,用于标记注解

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseEvent {

    String setterName();        // setOnClickListener

    Class listenerType();      //View.OnClickListener

    String methodName();     // onClick(View view)  ->> 这个感觉不要也可以,可以通过第二个获取 实际好像也没用到,因为这事件监听都是一个回调,可能多回调能用上?

}

因为后面反射是需要知道设置点击事件的方法名,和点击事件对应的接收类对象

再定义一个点击事件的注解,用于方法标记

@BaseEvent(setterName = "setOnClickListener", listenerType = View.OnClickListener.class, methodName = "onClick")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

最后定义点击时间方法代理类

public class ListenerInvokeHandler implements InvocationHandler {

    private Object activity; //这里对应到注册onClick的点击方法中
    private Method onClickMethod;

    public ListenerInvokeHandler(Object activity, Method onClickMethod) {
        this.activity = activity;
        this.onClickMethod = onClickMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        onClickMethod.invoke(activity, args);
        return null;
    }
}

调用方法

Object proxyInvoke = Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{listener}, invokeHandler);

这里的listener类就可以传上面的View.OnClickListener

这里等同于我们创建了一个上面OnClickListener的实现类,传入的就算最终注解标记的方法,当触发点击则去反射调用标记的方法

完整的是

 private static void injectMethod(Object object) {
        try {
            Class<?> mClass = object.getClass();
            Method methods[] = mClass.getDeclaredMethods();
            for (Method method : methods) { //method最终过滤出我们的  testOnClick方法
                if (method.isAnnotationPresent(OnClick.class)) {
                    OnClick onClick = method.getAnnotation(OnClick.class);
                    int[] ids = onClick.value();

                    //这里有些差异,是 annotationType()方法获取
                    BaseEvent baseEvent = onClick.annotationType().getAnnotation(BaseEvent.class);
                    String setterName = baseEvent.setterName();
                    Class<?> listener = baseEvent.listenerType();
                    String methodName = baseEvent.methodName();

                    ListenerInvokeHandler invokeHandler = new ListenerInvokeHandler(object, method);

                    //查找相应的view设置监听代理
                    for (int id : ids) {
                        //代理传入activity对象,和注册的点击方法  这里相当于写了一个 onClickListener的实现类
                        Object proxyInvoke = Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{listener}, invokeHandler);

                        Method findViewByID = mClass.getMethod("findViewById", int.class);
                        View targetView = (View) findViewByID.invoke(object, id);

                        Method setOnClickListener = targetView.getClass().getMethod(setterName, listener);

                        //给view注册代理监听,最后的点击会回调上面的invoke方法内,再回到activity中注册的点击方法内
                        setOnClickListener.invoke(targetView, proxyInvoke);

                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

使用

@BindLayout(R.layout.activity_butterknife_tst)
public class ButterknifeTestAcivity extends AppCompatActivity {

    @BindView(R.id.btn_1)
    Button btn1;
    @BindView(R.id.btn_2)
    Button btn2;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InJectUtils.injectLayout(this);
        InJectUtils.injectView(this);
        InJectUtils.injectMethod(this);

    }
    
    @OnClick({R.id.btn_1, R.id.btn_2})
    public void myTestClick(View view) {
        switch (view.getId()) {
            case R.id.btn_1:
                toast("点击了按钮1");
                break;
            case R.id.btn_2:
                toast("点击了按钮2");
                break;
        }
    }


    private void toast(String msg) {
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值