前提:
本文将从View的源码开始,学习google是如何利用反射执行onClick事件的,然后利用 反射+注解 ,打造一款小巧灵活的运行时注解框架。

  1. 在布局文件中的onClick属性是如何执行监听事件的。
  2. 通过反射拿到注解的值,不仅仅是省去手动的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,也就是说注解类型,注解类型是什么意思呢?就是标注注解的注解,或者叫 元注解 。有点绕。。。

本篇所用到的是运行时注解,其他的不在此篇范围。

接下来,会用到这些东西:

  1. @Retention(RetentionPolicy.RUNTIME) 指明是运行时注解
  2. @Target(ElementType.FIELD) 说明该注解应用在成员变量上
  3. @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.java

public 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属性,我们做了两点优化:

  1. 不用担心方法的访问权限(public/protected/private)
  2. 不用担心View view参数问题

如何获取代码?

git clone https://github.com/droid4j/anKataLite.git

本篇对应的标签 v0.1

git checkout v0.1