butterknife是现在使用的较多的一个库了,它让我们的工作减少了相当一部分的量,今天就让我们一起来走近它 ,实现我们自己的一个简单的类似效果,让它不再显得那么神秘吧!
谈及butterknife的原理,粗略的说,它用到了反射和注解这两个方面的知识。那么我们想实现简单的butterknife效果,我们就得首先知道反射和注解是怎么回事,ok,接下来我们先来看看反射吧。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
上述就是对反射的理解,有了文字含义,我们再来写一写小案例具体看看吧:
比如我们这有个User类,想要获取到这个类中的成员变量
public class User {
private String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name == null ? "" : name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void like(String type){
Log.e("aaa","喜欢的东西是:----"+type);
}
}
那么我们想要通过反射来获得该类中的属性和方法:该怎么做呢?
从上述文字中,我们可以看到,想要解剖一个类:
第一步:就是先要获取到每一个字节码文件对应的Class类型的对象.
这个对象有三种方式可以获取:
//第一种 类名.class
Class class1= User.class;
//第二种 通过forName(全类名)
Class clazz2 = Class.forName("com.example.yinl.simplebutterknife.User");
// 第三种 通过对象的getClass()
User user = new User("zhangsan", 10);
Class clazz = user.getClass();
第二步:通过获得的对象来拿到成员属性
//这里注意 当属性为private的时候 需要使用getDeclaredField()
//如果是public 则也可使用getField()
Field name=clazz.getDeclaredField("name");
Field age=clazz.getField("age");
//private属性值 还需设置 允许暴力反射
name.setAccessible(true);
//获得属性值 从user对象中获取
String uName= (String) name.get(user);
int uAge=age.getInt(user);
//修改属性值 修改的对象 修改值
name.set(user,"yinll");
age.set(user,12);
//获得类中的方法
//一:获得所有方法
Method[] methods=clazz.getMethods();
for(Method method:methods){
//打印一下方法 做个测试
Log.e("aaa","方法-----"+method.getName());
}
//二:获得某一个方法 方法名 方法类型
Method method=clazz.getMethod("like",String.class);
//调用该方法(在user对象中调用该方法,传入参数)
method.invoke(user, "水果茶");
通过以上几个步骤,我们就能成功的获取到我们想要知道的类中的属性和方法,并对其进行修改和调用了。有木有很简单?
学会了反射之后,我们接下来看看 注解:
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。注解是以‘@注解名’在代码中存在的,根据注解参数的个数,我们可以将注解分为:标记注解、单值注解、完整注解三类。它们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过[反射机制]编程实现对这些元数据(用来描述数据的数据)的访问。另外,你可以在编译时选择代码里的注解是否只存在于源代码级,或者它也能在class文件、或者运行时中出现(SOURCE/CLASS/RUNTIME)。
首先如何创建注解:定义一个类 将其设置为@interface。下方是我定义的一个注解:
- 注解:我们需要注意的是,定义注解时我们需要给其设置 @Retention
和 @Target , 具体使用情况详细见下方代码
//表示当前注解存在于字节码中,当源码被编译成字节码时,注解不会被清除
//在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
//@Retention(RetentionPolicy.CLASS)
//表示当前注解存在于源码中,当源码别编译成字节码时,注解会被清除
//在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码
//@Retention(RetentionPolicy.SOURCE)
//表示当前注解使用在方法上
//@Target(ElementType.METHOD)
//用于描述类、接口或enum声明
//@Target(ElementType.TYPE)
//表示当前注解存在于虚拟机
//始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
@Retention(RetentionPolicy.RUNTIME)
//表示当前注解使用在属性上
@Target(ElementType.FIELD)
public @interface BindView {
// 如果注解中只有一个属性,可以直接命名为“value”,使用时无需再标明属性名。
//@BindView(R.id.y)
// int value();
//如果是其他值 比如
int age();
String name();
//则在使用时 需要表明属性名 如 @BindView(age=18,name="yinl")
}
定义好注解之后 我们就可以在类中来使用了:比如我们新建一个Test类,看看如何在这个类中来使用我们的注解
public class Test {
/**
* 使用注解 age和name在注解中有定义
* 如果注解中只有一个属性,可以直接命名为“value”
* 此时 使用@BindView时不需要像下方一样知名age或者name属性
* 直接@BindView(20)即可,@BindView(R.id.xx)就是这样,熟悉吧
*/
@BindView(age=18,name="yinl")
private String name ;
private int age ;
@Override
public String toString(){
return "name:"+name+",age:"+age;
}
}
那到了这里,我们又如何去获取注解上的值呢?这时候我们上面学到的反射就有用啦!这里我们再新建一个类,就取名叫做Butterknife吧,我们来模仿一下:在类中我们也来一个bind方法,我们将我们需要解剖的类传入到这个类中进行解析,获取到我们想要的数据(中间除了上述说道的反射只是外,需要注意的就是我们怎么样来拿到注解?)
public class Butterknife {
//这里传入需要解析的对象
public static void bind(Test test) throws NoSuchFieldException,
IllegalAccessException {
//获得字节码对象
Class c= test.getClass();
//获得属性
Field name=c.getDeclaredField("name");
Field age=c.getDeclaredField("age");
//允许暴力反射
name.setAccessible(true);
age.setAccessible(true);
/**
* 获取注解 这里对应的是我们自定义的注解类,一定不要搞错了 一一对应的哦
* 比如我们这里用到的是上方定义的BindView 注解,那我们需要获取到的注解对象
* 就是BindView ,getAnnotation(BindView.class)中传入的参数自然也就是
* BindView
*/
BindView bindView=name.getAnnotation(BindView.class);
if(bindView != null){
//如果注解不是空 则可以获取注解值
String uName=bindView.name();
int uAge=bindView.age();
//然后将值设置到我们的对象上
name.set(test,uName);
age.set(test,uAge);
}else{
//注解为空
}
}
}
//接下来我们在activity中来调用看看:
//先定义对象
Test t=new Test();
try {
//我们的Butterknife用起来 有木有很顺手,很hi。写起来都是一个感觉
Butterknife.bind(t);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//这时候我们可以来看看注解上的值是否成功的传递到了对象中
Toast.makeText(this,t.toString(),Toast.LENGTH_SHORT).show();
上方的结果经测试,是ok的,那么到这里位置,反射和注解我们就都了解啦,简单中藏着奥妙,舒服。
接下来就是重头戏啦,自己实现一个简单的类似于Butterknife的功能,废话不多说,动起来:
首先我们实现通过@BindView(R.id.xx),实现获取控件的实例,并得到控件的值(省去findviewbyid)
第一步:先定义BindView注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
// 如果注解中只有一个属性,可以直接命名为“value”,使用时无需再标明属性名。
//@BindView(R.id.y)
int value();
}
//这里可以看看跟上方BindView类中的区别,加深理解
第二步:自定义Butterknife类,在类中来获取我们需要的值,这里与上方不同的一点就是我们传入的对象应该是activity.另外就是大家可以多注意这里面是如何来找到我们的控件,做到不需要我们在activity中findviewbyid的。
public class Butterknife {
public static void bind(Activity activity) {
try {
bindView(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 绑定视图view
*/
private static void bindView(Activity activity) throws IllegalAccessException {
//获得字节码
Class clazz=activity.getClass();
//获得activity中的所有变量
Field[] fields=clazz.getDeclaredFields();
//循环获取
for(Field field:fields){
//允许暴力反射
field.setAccessible(true);
//获取变量上加的注解
BindView bindView=field.getAnnotation(BindView.class);
if(bindView != null){
//注解不为空的情况下 获取注解的值 这里实际上是找到当前控件对应的ID
int id=bindView.value();
//通过id来获取控件
View view=activity.findViewById(id);
//将控件的值赋值给变量
field.set(activity,view);
}else{
}
}
}
}
//到了这里我们就可以在activity中调用了:
Butterknife.bind(this);
//输出下是否获得到了控件的值
Toast.makeText(this,textView1.getText().toString(),Toast.LENGTH_SHORT).show();
那么这里也可以成功得到textView1这个控件的值,这样我们就成功了一 半,实现了这种简单的控件绑定效果,而不需要通过findviewbyid来获取控件实例了;
下面我们再来实现 butterknife绑定点击事件的效果(就是butterknife中的@OnClick(R.id.xx)):
首先同样的我们这里 新建一个针对点击事件的注解类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyClick {
int value();
}
接下来我们就该思考怎么通过这个注解来设置它的点击事件:我们想想上方的思路,肯定也是通过反射来得到当前控件的id,然后通过在我们自定义的Butterknfe封装类中获得该id的控件实例,有了控件实例,这时候我们是不是就可以给这个获得的实例对象添加点击事件监听啦?
所以我们回到Butterknfe类中,加入一个bindClick方法(记得bind()方法中加入调用):绑定我们的监听事件:
/**
* 点击事件
*/
private static void bindClick(final Activity activity){
Class<? extends Activity> c=activity.getClass();
//获得方法
Method[] methods=c.getDeclaredMethods();
for(final Method method:methods){
//允许暴力反射
method.setAccessible(true);
//获得注解
MyClick view= method.getAnnotation(MyClick.class);
if(view != null){
//获得控件id
int id=view.value();
View v=activity.findViewById(id);
//点击事件
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
//调用方法 这里方法没带参数 所以不用传递
method.invoke(activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}else{
}
}
}
这样就成功的设置了监听事件啦,我们看看activity中的使用:同样是跟我们熟知的Butterknife是同样的使用方式:
/**
* 自定义 点击事件注解
*/
@MyClick(R.id.button)
public void onClick(){
Log.e("aaa","onClick");
Toast.makeText(ButterknifeActivity.this,"点击了按
钮",Toast.LENGTH_SHORT).show();
}
经测试一切正常,大功告成。
一路走下来是不是发现其实特别的简单,并不复杂(当然我们工作中所使用到的Butterknife库远远不止这么多的逻辑),开不开心?希望看到这,能够让你基本的懂得了反射,注解,以及结合这两者如何实现类似于butterknife的功能。感谢您的观看,有问题欢迎留言指教!
最后附上传送门,需要的朋友可以参考下:不过建议自己写一波,这样比较好;
源码传送门.
网友评论