手写一个ButterKnife

作者: 杨体仁 | 来源:发表于2017-06-30 15:07 被阅读95次

    想要写一个ButterKnife 需要了解两个方面的姿势:

    • 注解
    • 反射

    先简单的了解下这俩玩意,就可以开始飙车撸码了,话说注解其实在日常代码中随处可见,比如Activity中onCreate头顶的那个 @Override
    ok,就拿它开刀吧,点进去一看:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    

    这是个什么鬼 @interface 是接口吗,nonono, @Target和@Retention又是什么鬼?

    其实这个Override 只是一个注解类,它指定了你要重写的一个父类的方法

    @Target指定的是你要注解的元素,而这个Target 本身也是一个注解类

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
        ElementType[] value();
    }
    

    ElementType 元素类型,点进去一看,是一个枚举类,里面是各种类型

    public enum ElementType {
        /** Class, interface (including annotation type), or enum declaration */
        //类,接口 包括注解类型,或者枚举
        TYPE,
    
        /** Field declaration (includes enum constants) */
        //字段或者枚举常量
        FIELD,
    
        /** Method declaration */
        //方法
        METHOD,
    
        /** Formal parameter declaration */
        //参数
        PARAMETER,
    
        /** Constructor declaration */
        //构造函数
        CONSTRUCTOR,
    
        /** Local variable declaration */
        //局部变量
        LOCAL_VARIABLE,
    
        /** Annotation type declaration */
        //注解
        ANNOTATION_TYPE,
    
        /** Package declaration */
        //包
        PACKAGE,
    
        /**
         * Type parameter declaration
         *
         * @since 1.8
         * @hide 1.8
         */
        //类型参数
        TYPE_PARAMETER,
    
        /**
         * Use of a type
         *
         * @since 1.8
         * @hide 1.8
         */
        //使用的类型
        TYPE_USE
    }
    

    @Retention 点进去之后发现又是一个注解:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
        RetentionPolicy value();
    }
    

    RetentionPolicy 顾名思义,保留政策:

    public enum RetentionPolicy {
        /**
         * 注解会在编译的时候被抛弃
         */
        SOURCE,
    
        /**
         * 注解会在class状态,即编辑状态被保留,但是在运行时可能会被抛弃 这里需要注意的是     注解和class文件在使用上是被分离的
         */
        CLASS,
    
        /**
         *注解会在编译时和运行时都被保留在vm,这样可以通过反射来获取它的信息。
         *
         */
        RUNTIME
    }
    

    你会发现Retention 和 Target本身作为一个注解,却又注解了其他的注解,原来是有四大元注解的(meta-annotation):

    • @Documented
    • @Retention
    • @Target
    • @Inherited

    补充下@Documented 和 @Inherited:

    @Documented 指定了这个注解会被javaDoc记录

    @Inherited 指定了这个注解的类型会自动的继承,具体意思是子类会自动的继承使用了这个注解的父类的注解,因此,对方法和属性无效。

    现在这几个注解的大概意思已经差不多清楚了,但是注解的原理是什么呢,为什么通过这几行简单的代码就可以实现如此神奇的效果?其实这种编程思想就是IOC,控制反转,其原则为不需要new,帮助我们注入所有的控件、布局等。

    现在我们可以将这些注解看做是一种标记,这种标记指定了它的类型以及保留政策,在javac编译,开发工具或者其他程序可以通过反射来了解你的标记元素,去做相应的事情。

    知道了这些,我们还需要知道,这简单的几行代码的写法,即注解体的语法。
    @interface是用来声明一个注解,这个注解是自动继承了Annotation接口的,这样编译程序才能知道你这个是注解。返回值类型就是参数的类型(class ,String,enum) 可以通过default来指定参数的默认值。
    注解参数的可支持数据类型:

    1.所有基本数据类型

    2.String类型

    3.Class类型

    4.enum类型

    5.Annotation类型

    6.以上所有类型的数组

    只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;   
    参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;  
    如果只有一个参数成员,最好把参数名称设为"value",后加小括号。
    ok现在就可以开始创造一个简易版的ButterKnife了
    首先创建一个注解布局的注解类SuperBindContentView

    @Target(ElementType.TYPE)         //元素类型
    @Retention(RetentionPolicy.RUNTIME)   //保留到class
    public @interface SuperBindContentView {
        int value();                    //返回布局id
    }
    

    这个类很简单 需要注意的是元素的类型和保留政策
    标记加上了 我们需要在程序执行的时候进行解析。

      public class SuperBind {
    
        //  方法名
        private static final String METHOD_FIND_VIEW_BY_ID = "findViewById";
        private static final String METHOD_SET_CONTENT_VIEW = "setContentView";
        private static final String METHOD_ON_CLICK = "onClick";
    
    
    /**
     * setContentView
     *
     * @param activity
     */
    public static void bindContentView(Activity activity) {
        //获取activity  的class
        Class<? extends Activity> clazz = activity.getClass();
        //方法的注解从class中获取
        SuperBindContentView superBindContentView = clazz.getAnnotation(SuperBindContentView.class);
        if (superBindContentView != null) {
            int value = superBindContentView.value();
            if (value != -1) {
                try {
                    Method scvMethod = clazz.getMethod(METHOD_SET_CONTENT_VIEW, int.class);
                    scvMethod.setAccessible(true);//激活
                    try {
                        scvMethod.invoke(activity, value);
    
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    }
    

    代码中的注释相对比较详细,相信看一遍就懂了,主要就是注解和反射的配合,通过注解拿到值,通过反射拿到方法,然后请求执行。
    findViewById的注解就更加简单了:

    @Target(ElementType.FIELD)         //指定元素类型为成员变量
    @Retention(RetentionPolicy.CLASS)  //保留到字节码
    public @interface SuperBindView {
        int value();                    //返回参数为int值 因为需要指定的就是资源id
    }
    

    解析方法:

     public static void bindView(Activity activity) {
        //获取activity  的class
        Class<? extends Activity> clazz = activity.getClass();
        //所有属性
        Field[] fields = clazz.getDeclaredFields();
        //遍历
        for (Field f : fields) {
            //拿到SuperBind 从而获取想要的id
            SuperBindView bind = f.getAnnotation(SuperBindView.class);
            if (bind != null) {
                int id = bind.value();
                if (id != -1) {
                    try {
                        Method fvbMethod = clazz.getMethod(METHOD_FIND_VIEW_BY_ID, int.class);
                        try {
    
                            Object mView = fvbMethod.invoke(activity, id);
                            f.setAccessible(true);
                            f.set(activity, mView);
    
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    现在有个稍显复杂一点的注解,事件的注解,比如OnClick方法,按照流程,首先创建一个注解类SuperBindOnClick

    **
     * Created by JackYang on 2017/6/29.
     * 事件注解
     * 点击事件的注解略显麻烦,我们需要声明其方法名字,事件名字,方法类型,等 所以需要写一个自定义的注解 BaseOnClick
     */
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @BaseOnClick(methodName = "onClick", listener = "setOnClickListener", listenerType = View.OnClickListener.class)
    public @interface SuperBindOnClick {
        int[] value();
    }
    

    在这里我需要一个自定义的注解来声明后文需要的这些参数,创建一个新的注解BaseOnClick,它起到桥梁的作用

    @Target(ElementType.ANNOTATION_TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BaseOnClick {
        String methodName();
    
        String listener();
    
        Class listenerType();
    }
    

    注解创建完成之后,就开始解析了。

      /**
         * 解析事件的注解
         *
         * @param activity
         */
    public static void bindOnClick(Activity activity) {
        //获取注解类
        Class<? extends Activity> clazz = activity.getClass();
        //获取所有的方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            //得到被OnClick 注解的方法
            if (method.isAnnotationPresent(SuperBindOnClick.class)) {
                SuperBindOnClick superBindOnClick = method.getAnnotation(SuperBindOnClick.class);
                //注解的值
                int[] ids = superBindOnClick.value();
                //获取baseOnClick 注解  根据注解获取注解
                BaseOnClick baseOnClick = superBindOnClick.annotationType().getAnnotation(BaseOnClick.class);
                //获取baseOnClick注解的值
                Class<?> listenerType = baseOnClick.listenerType();
                String listener = baseOnClick.listener();
                String methodName = baseOnClick.methodName();
                //这里需要用到动态代理 关于动态代理 下文详细介绍
                ProxyHandler proxyHandler = new ProxyHandler(activity);
                //指定代理什么
                Object proxyListener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, proxyHandler);
                //把方法添加进去
                proxyHandler.addMethod(methodName, method);
                //View  的点击事件
                for (int i :
                        ids) {
                    try {
                        //获取findViewById方法
                        Method findViewByIdMethod = clazz.getMethod(METHOD_FIND_VIEW_BY_ID, int.class);
                        findViewByIdMethod.setAccessible(true);
                        try {
                            //获取view
                            View view = (View) findViewByIdMethod.invoke(activity, i);
                            //获取点击事件
                            Method onClickListener = view.getClass().getMethod(listener, listenerType);
                            //对这个点击事件进行操作
                            onClickListener.setAccessible(true);
                            //对象和方法
                            onClickListener.invoke(view, proxyListener);
    
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
    
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    这里用到了动态代理的概念,之所以要用到动态代理,是因为我们需要替换view的点击事件的方法,所以通过ProxyHandler这个类来进行替换Mehtod方法,这个方法需要指定class和方法名。

    /**
     * 动态代理
     */
    static class ProxyHandler implements InvocationHandler {
        //存放方法的map
        private final HashMap<String, Method> methodMAP = new HashMap<>();
        //使用弱引用
        private WeakReference<Object> weakRef;
    
        //把Activity传进弱引用 以防内存泄漏
        public ProxyHandler(Object obj) {
            this.weakRef = new WeakReference<Object>(obj);
        }
    
        /**
         * 添加方法
         *
         * @param name
         * @param method
         */
        public void addMethod(String name, Method method) {
            methodMAP.put(name, method);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //获取activity
            Object o = weakRef.get();
            if (o != null) {
                //方法名
                String methodName = method.getName();
                //从map中获取该方法名对应的方法  此处对method进行了替换
                method = methodMAP.get(methodName);
                if (method != null) {
                    //执行
                    method.invoke(o, args);
                }
            }
    
            return null;
        }
    }
    

    至此,点击事件的注解已经完成了,神奇的地方在于注解和反射的互相配合,在合适的时机绑定给view或者替换方法,更深层次的原理则是需要明白这个时机发生的时间,本文暂不做更深的讲解。

    源码传送门:
    https://github.com/yangpin/superBinder

    相关文章

      网友评论

        本文标题:手写一个ButterKnife

        本文链接:https://www.haomeiwen.com/subject/nyxicxtx.html