手写一个ButterKnife

想要写一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容