揭秘lOC注入框架

项目需求:

在Android开发中,我们在初始化控件、事件时常常被繁琐的代码困扰,比如这样:

Button button;
TextView textView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    button = findViewById(R.id.btn);
    textView = findViewById(R.id.tv);
}

又或者这样:

button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            
        }
    });
    
    button.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            return false;
        }
    });
    
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            
        }
    });
    
    textView.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            return false;
        }
    });

这代码看着就让人头疼,当然可以通过实现接口然后在在实现接口方法中使用switch关键字判断,但是依旧很麻烦,因为所有的使用监听器的方法都得使用setOnxxxListener(this)类似的方法。注入插件的出现就是为了简化这些问题,使得代码变得简洁,网络上现在有许多常见的开源框架,如:ButterKnife,xUtils3等,其实他们的实现并没有那么神秘。

注入框架实现

一、控件注入

1、添加注释

使用过开源项目的人都知道简化代码的手段是通过添加一个注释,便能对控件或者事件(监听器)完成设置,如:

@InjectView(R.id.btn)
private Button button;
@InjectView(R.id.tv)
private TextView textView;

于是,我自定义了1个注释:

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

@interface表示定义一个注释,@Target表示这个注释作用于类(TYPE),方法(METHOD),或是参数(FIELD)等等。@Retention表示表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)。设置为RUNTIME表示在运行时保留。注解中的value()表示注解的参数值,如:@InjectView(R,id.tv)中的R.id.tv就是value的值

2、对注释的参数进行赋值

没错,最终还是要调用findViewById方法,只是框架把这些都帮开发者做好了,而且通过for循环做好了。

新建一个InjectManager类,然后通过一个方法传入Activity对象,并分别完成对控件和事件的注入

public static void injectInto(Activity activity){
    
    injectLayout(activity);
    
    injectViews(activity);
    
    injectEvents(activity);
}

这里的injectLayout可以无视,这是对布局的注入,即省略了原本的onCreate()方法中的setContentView(),在Activity类完成类注释,比较简单。下面是注入控件的实现:

private static void injectViews(Activity activity) {
    //获取Activity类
    Class<? extends Activity> clazz = activity.getClass();
    //获取Activity类中所有的参数
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        InjectView annotation = field.getAnnotation(InjectView.class);

        //得到有InjectView注解的参数
        if(annotation != null){
            int viewId = annotation.value();//获取属性值,控件ID

            try {
                Method method = clazz.getMethod("findViewById", int.class); //获取父类的findViewById方法
                Object view = method.invoke(activity, viewId);  // 调用并返回findViewById方法返回值
                field.setAccessible(true);  //设置可访问私有属性
                field.set(activity,view);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

首先获取activity的对象clazz,然后获取它本类的所有方法(这里需要注意的是,getDeclaredFields和getFields的区别是:前者是获取当前类的所有变量,后者是获取本类,包括父类,父类的父类…一直到祖宗类的所有变量,这里只需要本类的参数即可),循环遍历所有变量,判断获取InjectView注解是否存在,存在的话获取注解的参数,即为我们自己设置的控件id值。

接下来获取findViewById方法。想想我们在activity中没有定义findViewById(…)方法,却使用可以直接使用它,因为它存在于父类AppCompatActivity中,所以可以直接调用(java基本常识).因此通过clazz.getMethod获取到父类的findViewById方法(前面已说明),第二个参数是获取方法的参数类型,findViewById()的参数是int类型,因此这里就是int.class得到方法method后,使用invoke方法进行回调。回调会得到返回参数,findViewById()的返回值是一个T类型,这里直接通过field.setAccessible(true)设置属性可以被访问,然后通过set方法完成对控件的设置,等同于view = findViewById(…),这样就完成了对控件的注册

注:

invoke方法有2个参数,简单来说,第一个参数表示该方法被调用的对象,第二个参数就是方法的参数,举例来说:

对于activity.findViewById(R.id.xx),findViewById是activity的方法(method对象是findViewById),method.invoke()的第一个参数就是activity,第二个参数就是R.id.xx

对于view.setOnClickListener(new OnClickListenr(…)),setOnClickListener是view的方法(method对象是setOnClickListener),method.invoke()的第一个参数就是view,第二个参数就是new OnClickListenr(…).

一、事件注入

1、添加事件总注解

一个事件,通常可以分解通常可以分解为3个部分:1、监听的方法名,2、监听的对象,3、监听的回调方法。
因此,定义一个注解对这3个部分进行定义:

@Target(ElementType.ANNOTATION_TYPE)   //这是一个注解的注解
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {

    String listenerSetter();                   //对应于setOn...Listener
    Class<?> listenerType();                   //对应于new On...Listener
    String callBackListener();                     //对应于接口实现方法,如:onClick
}

这个注解是一个注解的注解,顾名思义,就是对注解进行注解,它可以定义一个注解是什么类型的注解,如:

定义一个点击注解

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

这个注解有个注解是EventBase,它的3个参数决定了这个注解是个onClick注解,下面是对如何进行注解的实现:

private static void injectEvents(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        Method[] methods = clazz.getDeclaredMethods();

        for (Method method : methods) {
            //获取每个方法的注解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                //获取每个方法的注解的注解类型
                Class<? extends Annotation> annotationType = annotation.annotationType();
                if(annotationType != null){
                    EventBase eventBase = annotationType.getAnnotation(EventBase.class);
                    //判断是否是有EventBase注解的注解
                    if(eventBase != null){
                        //获取EventBase的三个参数
                        String listenerSetter = eventBase.listenerSetter();
                        Class<?> listenerType = eventBase.listenerType();
                        String callBackListener = eventBase.callBackListener();

                        //使用代理完成事件的拦截、修改
                        MyInvocationHandler handler = new MyInvocationHandler(activity);
                        handler.addMethod(callBackListener,method);
                        Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),new Class[]{listenerType},handler);


                        try {
                            //通过注解类型,获取onClick注解的value值
                            Method valueMethod = annotationType.getDeclaredMethod("value");
                            int[] viewIds = (int[]) valueMethod.invoke(annotation);
                            if(viewIds != null) {
                                for (int viewId : viewIds) {
                                    View view = activity.findViewById(viewId);
                                    //获取制定方法,例子:view如果是Button,获取它的setOnClickListener方法
                                    Method setter = view.getClass().getMethod(listenerSetter, listenerType);
                                    setter.invoke(view,listener);
                                }
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

一样的,先遍历获取activity类中所有的方法,然后继续遍历获取所有方法的所有注解,annotation.annotationType()表示注解类型,通过这个注解类型获得注解的注解,就是自定义的EventBase,如何不为空,才是符合的方法,接着通过自定义注解的对象获得它的三个参数:监听的方法名,监听的对象,监听的回调方法。接着使用Proxy完成对事件的实现。这里详细来说:Proxy.newProxyInstance()方法,3个参数,第一个:ClassLoader,没什么好说的,直接通过监听的对象获得ClassLoader,第二个参数是传入一个接口名数组,这里是callBackListener,,最后是一个InvocationHandler对象,它是一个接口,需要实现invoke方法,具体如下:

public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

        String name = method.getName();  //这里的method是onClick
        method = map.get(name);  //当onClick方法被调用时,将method替换成自定义方法

        //此时的method已经是自定义方法了
        if(method != null){
            if(method.getGenericParameterTypes().length == 0) {
                return method.invoke(target);
            }
            return method.invoke(target,objects);
        }
        return null;
    }

刚才第二个参数设置的接口,它需要实现的所有方法,都会回调这个上面的invoke方法,同样地在这里进行进行拦截事件,替换事件。下面先添加拦截方法,

//添加拦截的方法
    public void addMethod(String methodName,Method method){
        map.put(methodName,method);

    }

	handler.addMethod(callBackListener,method);

要拦截的方法通过HashMap保存,key值为原本的方法名,value值为要替换的方法,在invoke方法中,当接口的onClick方法被回调时,将它替换成我们自己的方法并返回。

回到injectEvents方法中,通过Proxy.newProxyInstance()方法获得监听器对象,然后获取所有注册此事件的控件,得到他们的父类的setOnClickListener()方法, 通过回调的方法:setter.invoke(view,listener),设置好参数,最后在activity完成注册就完成了,如下:

@onClick({R.id.btn,R.id.tv})
    public void show(){
        Toast.makeText(this, "AirPods", Toast.LENGTH_SHORT).show();
    }

以上是以setOnClickListener方法为举例,其他的方法也一样,但是!值得一提的是,注入的方法返回类型必须与接口中的方法返回类型一致,否则会报错,如:

		xxx.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                
            }
        });

@onClick(R.id.xxx)
    public void show(){
        Toast.makeText(this, "AirPods", Toast.LENGTH_SHORT).show();
    }

无返回类型,因此用void,又如:

 button.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                return false;
            }
        });

@onLongClick(R.id.tv)
    public boolean show(View view){
        Toast.makeText(this, "AirPods2", Toast.LENGTH_SHORT).show();
        return false;
    }

返回类型是boolean,因此注入的方法也必须是boolean类型,不能是:

@onLongClick(R.id.tv)
    public void show(View view){
        Toast.makeText(this, "AirPods2", Toast.LENGTH_SHORT).show();
    }

demo地址:https://github.com/lyx19970504/lOC_injector

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哒哒呵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值