Butterknife源码
Butterknife用法
我相信学过Android开发应该基本上都用过Butterknife吧,就算没用过也听说过吧?毕竟是大名鼎鼎的JakeWharton出品的东西,如果没用过,就分享下面这篇《Java基础之注解annotation》里面虽然是讲的Annotation,但是例子就是用注解加反射实现的低级的Butterknife。哈哈!用法里面大概也说了下。
前言
从jdk5开始,Java增加了对元数据的支持,也就是Annotation,Annotation其实就是对代码的一种特殊标记,这些标记可以在编译,类加载和运行时被读取,并执行相应的处理。当然刚刚说了,Annotation只是一种标记,所以要是在代码里面不用这些标记也是能完成相应的工作的,只是有时候用注解能简化很多代码,看起来非常的简洁。
基本的Annotation
@Override——限定重写父类方法
@Deprecated——标示已过时
@SuppressWarning——抑制编译器警告
@SafeVarargs——这货与Java7里面的堆污染有关,
JDK的元Annotation
JDK除了提供上述的几种基本的Annotation外,还提供了几种Annotation,用于修饰其他的Annotation定义
@Retention这个是决定你Annotation存活的时间的,它包含一个RetationPolicy的value成员变量,用于指定它所修饰的Annotation保留时间,一般有:
Retationpolicy.CLASS:编译器将把Annotation记录在Class文件中,不过当java程序执行的时候,JVM将抛弃它。
Retationpolicy.SOURCE:Annotation只保留在原代码中,当编译器编译的时候就会抛弃它。
Retationpolicy.RUNTIME:在Retationpolicy.CLASS的基础上,JVM执行的时候也不会抛弃它,所以我们一般在程序中可以通过反射来获得这个注解,然后进行处理。
@Target这个注解一般用来指定被修饰的Annotation修饰哪些元素,这个注解也包含一个value变量:
ElementType.ANNOTATION_TYPE:指定该Annotation只能修饰Annotation。
ElementType.CONSTRUCTOR:指定只能修饰构造器。
ElementType.FIELD:指定只能成员变量。
ElementType.LOCAL_VARIABLE:指定只能修饰局部变量。
ElementType.METHOD:指定只能修饰方法。
ElementType.PACKAGE:指定只能修饰包定义。
ElementType.PARAMETER:指定只能修饰参数。
ElementType.TYPE:指定可以修饰类,接口,枚举定义。
@Document这个注解修饰的Annotation类可以被javadoc工具提取成文档
@Inherited被他修饰的Annotation具有继承性
自定义Annotation
jdk自带的Annotation来实现一些我们想要的功能。由于最近在看butterknife的源码,那么我们就一步一步地模仿butterknife的实现吧。
首先先讲一下的用法吧:
@ContentView(R.layout.activity_main)
publicclassMainActivityextendsAppCompatActivity{
@ViewInject(R.id.text_view)
privateTextViewtextView;
@OnClick(R.id.text_view)
privatevoidonClick(Viewview){
textView.setText("我是click后的textview");
}
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
ViewInjectUtils.inject(this);
textView.setText("我是click前的textview");
}
}
编码
首先我们要先定义我们要用的接口,哦不!是注解。注意和接口不一样哦!这里我们先实现@ContentView的功能,再来实现@ViewInject和@OnClick
packagecom.qhung.annotation.ioc.annotation;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
/
Createdbyqhungon2016/5/3.
/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceContentView{
intvalue();
}
啊,这里的@Target和@Retention大家应该都清楚是什么意思了哈,定义注解的方式就是@interface和接口的定义方式就少一个@哦,不要搞混了。里面有一个变量value,就是我们使用的时候@ContentView(R.layout.activity_main)指定的R.layout.activity_main布局文件,旨在自动注入布局文件。因为这里只有一个变量value,所以不用写成name=value的形式。
然后大家还记得我们在Activity里面调用的ViewInjectUtils.inject(this);?哈哈!其实我们处理注解的逻辑全在这个里面,那么我们就看看这个里面又是一番什么天地:
packagecom.qhung.annotation.ioc.annotation;
importandroid.app.Activity;
importandroid.view.View;
importjava.lang.reflect.Field;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;
/
Createdbyqhungon2016/5/3.
/
publicclassViewInjectUtils{
publicstaticvoidinject(Activityactivity){
injectContentView(activity);
}
privatestaticvoidinjectContentView(Activityactivity){
Classclazz=activity.getClass();
ContentViewcontentView=clazz.getAnnotation(ContentView.class);
if(contentView!=null){
//如果这个activity上面存在这个注解的话,就取出这个注解对应的value值,其实就是前面说的布局文件。
intlayoutId=contentView.value();
try{
MethodsetViewMethod=clazz.getMethod("setContentView",int.class);
setViewMethod.invoke(activity,layoutId);
}catch(Exceptione){
e.printStackTrace();
}
}
}
}
原来ViewInjectUtils.inject(this)里面调用了injectContentView(activity),在injectContentView(activity)里面,我们拿到了Activity的Class,然后在第23行,我们拿到了这个class的ContentView注解,然后再通过反射调用setContentView方法,完成注入。其实这里是在运行的时候完成的,所以我们在定义注解的时候,设置为Retention为RUNTIME。
好了,这个功能到这里就完了。下面继续完成第二个功能:ViewInject
同样先贴上ViewInject类:
packagecom.qhung.annotation.ioc.annotation;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
/
Createdbyqhungon2016/5/3.
/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceViewInject{
intvalue();
}
其实这个定义和上面ContentView的定义一样,然后我们再看看我们是怎么处理的:
publicstaticvoidinject(Activityactivity){
injectContentView(activity);
injectView(activity);
}
privatestaticvoidinjectView(Activityactivity){
Classclazz=activity.getClass();
//获得activity的所有成员变量
Field[]fields=clazz.getDeclaredFields();
for(Fieldfield:fields){
//获得每个成员变量上面的ViewInject注解,没有的话,就会返回null
ViewInjectviewInject=field.getAnnotation(ViewInject.class);
if(viewInject!=null){
intviewId=viewInject.value();
Viewview=activity.findViewById(viewId);
try{
field.setAccessible(true);
field.set(activity,view);
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}
}
}
获得所有属性,然后遍历带有ViewInject注解的属性,然后拿到ViewInject注解的View的id,然后通过activity.findViewById(viewId);获得这个View。然后设置给field。
最后一个功能:EventInject
这个功能稍微麻烦一点,因为我们平时设置的点击时间是用setiOnClickListener();然后View.OnClickListener是一个接口,不能用反射来获得他的实例,那么怎么办呢?
其实我们这里可以巧妙地用动态代理来完成,当view被点击的时候,我们通过动态代理来调用onclick就行。
下面是代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interfaceOnClick{
int[]value();
}
publicclassViewInjectUtils{
publicstaticvoidinject(Activityactivity){
injectContentView(activity);
injectView(activity);
injectEvent(activity);
}
privatestaticvoidinjectEvent(finalActivityactivity){
Classclazz=activity.getClass();
Method[]methods=clazz.getDeclaredMethods();
for(finalMethodmethod2:methods){
OnClickclick=method2.getAnnotation(OnClick.class);
if(click!=null){
int[]viewId=click.value();
method2.setAccessible(true);
Objectlistener=Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
newClass[]{View.OnClickListener.class},newInvocationHandler(){
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
returnmethod2.invoke(activity,args);
}
});
try{
for(intid:viewId){
Viewv=activity.findViewById(id);
MethodsetClickListener=v.getClass().getMethod("setOnClickListener",View.OnClickListener.class);
setClickListener.invoke(v,listener);
}
}catch(Exceptione){
e.printStackTrace();
}
}
}
}
listener是一个代理对象,然后我们调用setOnClickListener的时候,把这个代理对象传进去。当发生点击的时候,就会invoke方法,这时我们就可以调用带有onClick注解的method方法了。
下面看一下运行结果:
现在我们就完成了类似butterknife的功能了,不过butterknife最新版本不是通过反射来完成的,因为反射会有性能问题,虽然现在对性能影响不大,但是作为程序员,能优化的就要尽量优化,不能只停留在能用的基础上面。
Butterknife原理
讲到butterknife的原理。这里不得不提一下一般这种注入框架都是运行时注解,即声明注解的生命周期为RUNTIME,然后在运行的时候通过反射完成注入,这种方式虽然简单,但是这种方式多多少少会有性能的损耗。那么有没有一种方法能解决这种性能的损耗呢?没错,答案肯定是有的,那就是Butterknife用的APT(AnnotationProcessingTool)编译时解析技术。
APT大概就是你声明的注解的生命周期为CLASS,然后继承AbstractProcessor类。继承这个类后,在编译的时候,编译器会扫描所有带有你要处理的注解的类,然后再调用AbstractProcessor的process方法,对注解进行处理,那么我们就可以在处理的时候,动态生成绑定事件或者控件的java代码,然后在运行的时候,直接调用bind方法完成绑定。
其实这种方式的好处是我们不用再一遍一遍地写findViewById和onClick了,这个框架在编译的时候帮我们自动生成了这些代码,然后在运行的时候调用就行了。
源码解析
上面讲了那么多,其实都不如直接解析源码来得直接,下面我们就一步一步来探究大神怎样实现Butterknife的吧。
拿到源码的第一步是从我们调用的地方来突破,那我们就来看看程序里面是怎样调用它的呢?
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.setDebug(true);
ButterKnife.bind(this);
//Contrivedcodetousetheboundfields.
title.setText("ButterKnife");
subtitle.setText("FieldandmethodbindingforAndroidviews.");
footer.setText("byDaxia");
hello.setText("SayHello");
adapter=newSimpleAdapter(this);
listOfThings.setAdapter(adapter);
}
上面是github上给的例子,我们直接就从ButterKnife.bind(this)入手吧,点进来看看:
publicstaticUnbinderbind(@NonNullActivitytarget){
returnbind(target,target,Finder.ACTIVITY);
}
咦?我再点:
staticUnbinderbind(@NonNullObjecttarget,@NonNullObjectsource,@NonNullFinderfinder){
Class>targetClass=target.getClass();
try{
ViewBinder |
|