深入浅出,带你撸出一个ButterKnife
来源:互联网 发布:金融软件供应商 编辑:程序博客网 时间:2024/05/18 07:51
想要写一个ButterKnife 需要了解两个方面的姿势:
- 注解
- 反射
先简单的了解下这俩玩意,就可以开始飙车撸码了,话说注解其实在日常代码中随处可见,比如Activity中onCreate头顶的那个 @Override ,
ok,就拿它开刀吧,点进去一看:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}
这是个什么鬼 @interface, @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) //保留到classpublic @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
- 自定义Toolbar样式,带你出坑
- 【前端知识点】深入浅出带你了解HTTP2.0
- 小司机带你撸一个简单的RPC框架
- 带你撸一个好玩的涂鸦View
- 深入浅出 RPC - 浅出
- Flex Alert出一个带超链接的提示框
- Flex Alert出一个带超链接的提示框(转)
- Flex Alert出一个带超链接的提示框
- 写一个 ButterKnife
- 写一个ButterKnife
- 一个程序带你了解java初始化
- 一个winform带你玩转rabbitMQ
- 一个winform带你玩转rabbitMQ
- 一个winform带你玩转rabbitMQ
- 一个FlowLayout带你学会自定义ViewGroup
- 教你吃出一个健康的秋季
- step by step 教你裁剪出一个Mini_Linux_LiveCD
- 宏定义 命名规范和常用的
- 根据滑动改变view的大小和位置
- Ubuntu 安装常用工具
- TabLayout的各种用法
- protobuf入门教程(六):导入定义(import)
- 深入浅出,带你撸出一个ButterKnife
- 搜集有意义的东西
- 团队协作及scrum sprint story编写
- lvs与nginx区别
- 画圆拖动0630
- How can I convert printStackTrace message to a string?
- 怎样将discuz所有页面的Powered by Discuz!去掉
- Kotlin-拓展功能
- 光照(light)