自己动手打造一个注解框架,彻底摆脱FindViewById
每次写代码的时候,看到一大堆
FindViewById()
都挺烦的,没有没什么方法能帮我摆脱这种烦恼呢?当然是有的,目前比较流行的时注解框架是Xutils3
和ButterKnife
,接下来我们来分析一下它们是如何实现的.
Xutils3注解实现原理
- 首先看一下
Xutils3
的简单用法
@ViewInject(R.id.tv_test)
private TextView mTvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
x.view().inject(this);
mTvTest.setText("hello ioc");
}
用法是相当的简单,通过@ViewInject
绑定id,然后通过inject(this)
注入当前对象,就完成了findViewById
的过程.
- 查看Xutils原码,看看
inject(this)
做了什么
// inject view
Field[] fields = handlerType.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
Class<?> fieldType = field.getType();
if (
/* 不注入静态字段 */ Modifier.isStatic(field.getModifiers()) ||
/* 不注入final字段 */ Modifier.isFinal(field.getModifiers()) ||
/* 不注入基本类型字段 */ fieldType.isPrimitive() ||
/* 不注入数组类型字段 */ fieldType.isArray()) {
continue;
}
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
try {
View view = finder.findViewById(viewInject.value(), viewInject.parentId());
if (view != null) {
field.setAccessible(true);
field.set(handler, view);
} else {
throw new RuntimeException("Invalid @ViewInject for "
+ handlerType.getSimpleName() + "." + field.getName());
}
} catch (Throwable ex) {
LogUtil.e(ex.getMessage(), ex);
}
}
}
} // end inject view
我们发现这里主要是通过反射,先获取该类的所有的属性,然后遍历,找到ViewInject
注解,然后获取value值,即对应的viewId
,然后再调用FindViewById
找到对应的View
,
field.setAccessible(true);
field.set(handler, view);
然后通过上述代码注入属性,就完成了FindViewById
的全过程.仔细一分析,是不是也不是那么难.主要就是利用了java的反射机制,通过动态获取Annotation
,然后实现View
的注入.
属性注入 : 利用反射去 获取Annotation --> value --> findViewById --> 反射注入属性
事件注入 :利用反射去 获取Annotation --> value --> findViewById --> setOnclickListener --> 动态代理反射执行方法
ButterKnife注解实现原理
- 同样的,我们来看一下ButterKnife的简单用法
@Bind(R.id.tv_test)
private TextView mTvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mTvTest.setText("hello ioc");
}
也非常简洁,两行代码完成属性注入.(这里使用的7.0.1所以是@Bind
而不是@BindView
);
- 查看源码
ButterKnife
的源码看起来就比较费劲了,主要是通过ButterKnifeProcessor
这个类动态解析Annotation注解,在编译时生成xxxxx$$ViewBinder
这个类,然后实现FindViewById
过程.
public class MainActivity$$ViewBinder<T extends com.example.wenjian.eassyjoke.MainActivity> implements ViewBinder<T> {
@Override public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131427415, "field 'mTvTest'");
target.mTvTest = finder.castView(view, 2131427415, "field 'mTvTest'");
}
@Override public void unbind(T target) {
target.mTvTest = null;
}
}
在这里我们也可以看到为什么声明属性的时候不能private
了
两者实现的方式有差异,
ButterKnife
基于编译时注解,相对较为高效
打造自己的注解框架
看了别人的代码,自己也可以动手撸一下了
- 首先我们得了解java的
Annatation
,在这里我们先参考一下java的@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这里我们可以知道如何自定义注解
@Target
指定注解作用目标,以下是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,包
@Retention
指定何时生效,有以下三种场景
* Annotations are to be discarded by the compiler.
*/
SOURCE,源码
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,编译时
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME,运行时
2.开始码自己的注解框架
自定义@ViewById
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) //什么时候生效:运行时
public @interface ViewById {
int value();
}
实现ViewUtils
兼容各种场景
public class ViewUtils {
public static void inject(Activity activity) {
inject(new ViewFinder(activity),activity);
}
public static void inject(View view) {
inject(new ViewFinder(view),view);
}
public static void inject(View view, Object object) {
inject(new ViewFinder(view),object);
}
private static void inject(ViewFinder finder, Object object) {
injectField(finder, object);
injectEvent(finder, object);
}
通过反射注入属性
/**
* 注入属性
* @param finder
* @param object
*/
private static void injectField(ViewFinder finder, Object object) {
//1.获取class
Class<?> clazz = object.getClass();
//2.获取所有的属性
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ViewById viewById = field.getAnnotation(ViewById.class);
if (viewById != null) {
int viewId = viewById.value();
View view = finder.findViewById(viewId);
if (view != null) {
//设置可以访问所有修饰符 包括private
field.setAccessible(true);
try {
//注入属性
field.set(object, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
到此已经完成了,我们来验证一下吧
@ViewById(R.id.tv_test)
private TextView mTvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewUtils.inject(this);
mTvTest.setText("hello ioc");
}
和Xutils
的实现方式差不多,同样是基于反射,重要的是不必依赖第三方库了.你也自己动手实现setOnclickListener
,原理是一样的.