前提:
本文将从View的源码开始,学习google是如何利用反射执行onClick事件的,然后利用 反射+注解 ,打造一款小巧灵活的运行时注解框架。
- 在布局文件中的onClick属性是如何执行监听事件的。
- 通过反射拿到注解的值,不仅仅是省去手动的findViewById。
1、 源码分析onClick属性执行OnClick事件
不知道你有没有好奇过,下面的testClick是如何执行监听事件的?
<TextView
android:onClick="testClick"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=“Hello world!” />
onClick对应的回调方法:
public void testClick(View view) {
// …
}
看看源码中,onClick属性都为我们做了些什么?下面是View.java的一个构造方法
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
// 省略一大坨代码...
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
// 省略一大坨代码...
// 只关注onClick这个case好了
case R.styleable.View_onClick:
if (context.isRestricted()) {
throw new IllegalStateException("The android:onClick attribute cannot "
+ "be used within a restricted context");
}
final String handlerName = a.getString(attr);
if (handlerName != null) {
// 这不是我们熟悉的setOnClickListener吗?就从这里往下找吧!一定要养成看源码的习惯哦!
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}
break;
//省略一大坨代码...
}
}
}
我们直接看DeclaredOnClickListener.java 类的onClick方法
@Override
public void onClick(@NonNull View v) {
if (mResolvedMethod == null) {//通过反射去拿名为mMethodName的方法
resolveMethod(mHostView.getContext(), mMethodName);
}
try { //利用反射执行方法
mResolvedMethod.invoke(mResolvedContext, v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}
看到了吧!源码也是用的反射获动态执行的这个方法
异常部分告诉我们两件事,
1. 这个方法必须是public的;
2. 方法上的View view参数必不可少。
2、大盼带你玩注解
大家对@Override熟悉吗?估计很多人只知道它叫什么,但从来没点进去看一下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@interface:是注解类的"关键字"
@Target : 表示该注解的类型
@Retention:表示该注解的应用场景
有了以上3点的认识,还不够,我们要再深入些,看下 @Target 和 @Retention 是个什么东西!
Target.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
Retention.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
这两个注解类的共同特点就是Target的值是ANNOTATION_TYPE,也就是说注解类型,注解类型是什么意思呢?就是标注注解的注解,或者叫 元注解 。有点绕。。。
本篇所用到的是运行时注解,其他的不在此篇范围。
接下来,会用到这些东西:
- @Retention(RetentionPolicy.RUNTIME) 指明是运行时注解
- @Target(ElementType.FIELD) 说明该注解应用在成员变量上
- @Target(ElementType.METHOD) 说明该注解应用在方法上
3、再见吧,findViewById()
如何与findViewById()说再见?有了上面查看源码的经历,我们再有一点 **注解** 的经验,实现自己的注解框架指日可待! 关于注解的分类什么的,不是这里的重点。 通常情况下,为了为TextView,我们需要先找到这个View:private TextView mUsernameTv;
mUsernameTv = (TextView)findViewById(R.id.uername_tv);
然后才能设置文本,有没有什么办法,不强转,不findViewById()的方法呢?是的,这就是本篇的目的。
3.1 注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewById {
int value();
}
有了这个注解后,我们改写上面的findViewById():
@ViewById(R.id.uername_tv)
private TextView mUsernameTv;
这样,我们就可以直接进行setText()操作了。
光有这些是不行的,我们得找“帮手”,下面请出我们今天的"劳力士"--反射老大哥。
3.2 ViewUtils.java
先为大哥整个行头:ViewUtils.javapublic class ViewUtils {
public static void inject(Activity activity) {
// 1.获取类里面所用的属性
Field[] fields = activity.getClass().getDeclaredFields();
for (Field field : fields) {
// 2.获取ViewById注解的value值
ViewById viewById = field.getAnnotation(ViewById.class);
if (viewById == null) {
continue;
}
int value = viewById.value();
// 3.findViewById找到View
View view = activity.findViewById(value);
if (view != null) {
// 4.动态注入找到的View
try {
field.setAccessible(true);
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
只要记住以下四点,你就可以抛开本文,自己写注解框架啦!
- 获取类里面所用的属性
- 获取ViewById注解的value值
- findViewById找到View
- 动态注入找到的View
“劳力士”秀
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewUtils.inject(this);
}
只要在声明的变量上,添加@ViewById()注解后,就可以安心的setText()啦!
4、加强版上线
上面的ViewUtils,只能用在Activity内,并且只能代替findViewById(),这太对不起“劳力士”这个称呼啦。 在加强版本中,我们要让它适应更广(Activity/Fragment/View),还可以处理事件,像android:onClick那样。4.1 OnClick注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
4.2 婚介所上
为了适应更广,我们得找中间人,大哥只管报名,具体找人,就有中间人跑腿了。public class ViewFinder {
private Activity mActivity;
private View mView;
public ViewFinder(Activity activity) {
this.mActivity = activity;
}
public ViewFinder(View view) {
this.mView = view;
}
public View findViewById(int viewId) {
return mActivity == null ? mView.findViewById(viewId) : mActivity.findViewById(viewId);
}
}
4.3 完善后的ViewUtils.java
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);
}
public static void inject(ViewFinder viewFinder, Object object) {
injectView(viewFinder, object);
injectEvent(viewFinder, object);
}
private static void injectView(ViewFinder viewFinder, Object object) {
// 1.获取类里面所用的属性
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
// 2.获取ViewById注解的value值
ViewById viewById = field.getAnnotation(ViewById.class);
if (viewById == null) {
continue;
}
int value = viewById.value();
// 3.findViewById找到View
View view = viewFinder.findViewById(value);
if (view != null) {
// 4.动态注入找到的View
try {
field.setAccessible(true);
field.set(object, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
private static void injectEvent(ViewFinder viewFinder, Object object) {
// 1.获取类里面所用的方法
Method[] methods = object.getClass().getDeclaredMethods();
for (Method method : methods) {
// 2.获取OnClick注解的value值
OnClick onClick = method.getAnnotation(OnClick.class);
if (onClick == null) {
continue;
}
int[] value = onClick.value();
if (value != null && value.length > 0) {
for (int id : value) {
View view = viewFinder.findViewById(id);
if (view != null) {
// TODO: 这里的思路来源于源码:View.java 在布局文件中android:onClick属性注入点击事件
view.setOnClickListener(new DeclaredOnClickListener(method, object));
}
}
}
method.setAccessible(true);
}
}
private static class DeclaredOnClickListener implements View.OnClickListener {
private Method mMethod;
private Object mObject;
public DeclaredOnClickListener(Method method, Object object) {
this.mMethod = method;
this.mObject = object;
}
@Override
public void onClick(View v) {
mMethod.setAccessible(true); //这样就不需要强制用户将方法设置为public权限了
try {
mMethod.invoke(mObject, v); //注意,这里注入的方法,必须包含View v
} catch (Exception e) {
e.printStackTrace();//这句可以注释掉,如果上面的出现异常,就在下面捕获吧!
try {
mMethod.invoke(mObject); //在这里注入一个无参的方法 :)
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}
}
为我们的注解添加了OnClick事件监听的能力,相比系统的android:onClick属性,我们做了两点优化:
- 不用担心方法的访问权限(public/protected/private)
- 不用担心View view参数问题
如何获取代码?
git clone https://github.com/droid4j/anKataLite.git
本篇对应的标签 v0.1
git checkout v0.1