手撸ButterKnife,主要练习反射、注解和动态代理
主要是通过反射、注解、动态代理相关的知识,实现ButterKnife的部分功能,其实用到的方法在Xutils和Dagger中都有涉及,就是IOC的依赖注入,利用依赖关系注入的方式,实现对象之间的解耦。简单点来说就是findViewById劳资写腻味了,不想写了,想通过其他的方式让他自己生成出来,当然还可以使用ATP的方式,那个是编译期的注解,新版本ButterKnife也是这么搞的,Google力推的viewbinding也是如此,这个回头再说,先来看我们运行时的注解。
项目传到github上了 地址
1、生成setContentView方法
先来看使用
@ContentViewInject(R.layout.activity_inject)
public class InJectActivity extends AppCompatActivity {
InjectUtils.ject(this);
}
看下注解类ContentViewInject,作用在Activity类上的,所以需要TYPE。然后是运行时需要用到的注解,所以是RetentionPolicy.RUNTIME
// 1.CONSTRUCTOR:用于描述构造器
// 2.FIELD:用于描述域
// 3.LOCAL_VARIABLE:用于描述局部变量
// 4.METHOD:用于描述方法
// 5.PACKAGE:用于描述包
// 6.PARAMETER:用于描述参数
// 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Target(ElementType.TYPE)
// 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
// 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
// 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentViewInject {
int value();
}
然后看我们这个统一的工具类
public class InjectUtils {
public static void ject(Context context){
injectLayout(context);// 获取setContentView 方法
}
可以在BaseActivity里声明一下,这里做示范,就直接在当前Activity里用了。
然后看一下这个injectLayout方法,注释写的很清楚了,就是通过反射获取Activity的类,然后拿到类上的注解ContentViewInject,再通过反射setContentView方法实现。
private static void injectLayout(Context context) {
int id;//当前activity的layout布局id
Class<?> clazz=context.getClass();//获取当前activity的类
ContentViewInject contentViewInject=clazz.getAnnotation(ContentViewInject.class); //获取ContentViewInject注解信息
id=contentViewInject.value();//将获取到的当前注解中的layout的id赋值
try {
Method setContentView=clazz.getMethod("setContentView",int.class); //通过反射,获取setContentView方法,有参数需要带着int类型
setContentView.invoke(context,id); //执行setContentView方法 第一个被反射类,第二个具体方法参数
} catch (Exception e) {
e.printStackTrace();
}
}
2、生成布局相关view的方法
@ViewInject(R.id.bt1)
private Button bt1;
@ViewInject(R.id.bt2)
private Button bt2;
再来看下view的方法,也是先上注解类ViewInject,这次是作用在变量上,所以是ElementType.FIELD。
@Target(ElementType.FIELD)
// 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
// 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
// 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
然后统一方法里加入
public static void ject(Context context){
injectLayout(context);// 获取setContentView 方法
injectView(context); // 获取每个view
}
我们看下这个injectView方法,还是先通过反射的方式,拿到当前的Activity类,遍历当前所有的字段,查看有无注解信息,如果能查到我们的ViewInject注解信息,然后反射findViewById这个方法,当前的这个findViewById(当前注解信息),然后交给我们的变量bt1,大概就是当前字段bt1=findViewById(当前注解信息)。
private static void injectView(Context context) {
Class<?> clazz=context.getClass();//获取当前activity的类
Field[] fields=clazz.getDeclaredFields();//获取当前所有
for (int i = 0; i < fields.length; i++) {//遍历当前所有的成员方法,查看有无注解信息,如果有取出来
ViewInject viewInject=fields[i].getAnnotation(ViewInject.class); //获取ContentViewInject注解信息
if(viewInject!=null){
try {
Method findViewById=clazz.getMethod("findViewById",int.class); //通过反射,获取setContentView方法,有参数需要带着int类型
fields[i].setAccessible(true);
View view=(View)findViewById.invoke(context,viewInject.value()); //相当于 调用findViewById,把当前的id,转换成view
fields[i].setAccessible(true);//防止用户设置私有
fields[i].set(context,view); //通过反射使用当前字段,把我们findViewById粗来的view,交给fields[i],即我们自己定义的那个bt1, private Button bt1;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
然后在Activity里跑一下,发现bt1和bt2是生效的
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.ject(this);
bt1.setText("修改成功");
bt2.setText("bt2也修改成功了");
}
3、生成view的点击交互方法
这个就比较麻烦了,会涉及到动态代理了,而且注解也麻烦一些,首先这个点击事件的方法,我们不知道用户到底是如何命名的,而且我们希望这个方法能兼容click或者longClick等其他的交互,注解这块就需要用到一个注解多态。
分解一下事件监听回调一般是用到三部分 setXXXListener
、new onXXXListener
和回调callback
方法,我们其实主要关注前边的两个方法。先看下父注解的方法,主要是
// ANNOTATION_TYPE:用在注解上
@Target(ElementType.ANNOTATION_TYPE)
// 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
// 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
// 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseClickInject {
//setXXXListener
String listenerSetter();
// onXXXListener new出来的点击事件
Class<?> listnerType();
}
再看下onClick事件和onLongClick的注解,返回数组是为了能在一个方法上调用多个字段,R.id.bt1、R.id.bt2,主要是传入点击事件所需要的信息
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
// bt2.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
//
// }
// });
// onLongClick 同样修改下边是哪个参数即可
@BaseClickInject(listenerSetter = "setOnClickListener",
listnerType = View.OnClickListener.class,
callbackMethod = "onClick" )
public @interface ClickInject {
int[] value() default -1;
}
----------
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
// bt2.setOnLongClickListener(new View.OnLongClickListener() {
//@Override
//public boolean onLongClick(View v) {
// return false;
// }
// });
@BaseClickInject(listenerSetter = "setOnLongClickListener",
listnerType = View.OnLongClickListener.class,
callbackMethod = "onLongClick" )
public @interface LongClickInject {
int[] value() default -1;
}
在InjectUtils方法里继续加入一个方法
public static void ject(Context context){
injectLayout(context);// 获取setContentView 方法
injectView(context); // 获取每个view
injectClick(context); // 获取view点击交互事件
}
重点看一下这个injectClick方法,反射和注解的方法和上边的差不多,这里注释写的也比较详细,主要是动态代理这个方法需要看一下。
private static void injectClick(Context context) {
//为了处理onClick 、onLongClick。。等等事件,需要一个统一方法
Class<?> clazz=context.getClass();//获取当前activity的类
//getMethods 方法取得所有public方法 包括继承的方法
//getDeclaredMethods 取得所有自己声明的方法 包括 public protected default private
Method []methods=clazz.getDeclaredMethods();
//遍历当前类中方法,查出有加入注解的方法
for (int i = 0; i <methods.length ; i++) {
Method method=methods[i];
//遍历当前这个方法的所有注解
Annotation[] annotations= method.getAnnotations();
for (Annotation annotation:annotations) {
Class<?> clazzAnnotation=annotation.annotationType();
//为什么不用ClickInject,因为可能还要LongClickInject等其他交互方法,所以要判断他们的父注解信息
BaseClickInject baseClickInject=clazzAnnotation.getAnnotation(BaseClickInject.class);
if(baseClickInject!=null){ //判断当前这个注解的方法,是不是我们要处理的
method.setAccessible(true);//防止有人自己定义的点击方法私有
String listenerSetter=baseClickInject.listenerSetter();
Class<?> baseClickInjectClazz=baseClickInject.listnerType();
try {
Method valueMethod=clazzAnnotation.getDeclaredMethod("value"); //获取注解上的value方法
int[] values= (int[]) valueMethod.invoke(annotation); //获取当前所有的button对象id值了,没有参数类型
for (int value:values) {//遍历当前所有的button对象,通过反射根据view的id值获取view
Method findViewById= context.getClass().getMethod("findViewById",int.class);
View view= (View) findViewById.invoke(context,value);//根据反射出的方法,拿到当前view 即button
if(view!=null){
// activity对应的是context myClick或者myLongClick对应的是我们自己定义的method 通过代理去执行我们自己定义的点击方法
MyInvokationHandler myInvokationHandler=new MyInvokationHandler(context,method);
//代理类 new View.OnClickListener()对象
// /**
// * 动态代理 能代理实现相同接口的方法 所以他代理其实就是把OnClickListener这个具体实现类代理出来
// */
// public interface OnClickListener {
// /**
// * Called when a view has been clicked.
// *
// * @param v The view that was clicked.
// */
// void onClick(View v);
// }
Object proxy= Proxy.newProxyInstance(baseClickInjectClazz.getClassLoader()
,new Class[] {baseClickInjectClazz}
,myInvokationHandler);
//执行 让proxy执行的onClick()
//参数1 setOnClickListener()
//参数2 new View.OnClickListener()对象
// view.setOnClickListener(new View.OnClickListener())其实就是 反射view上setOnClickListener方法
Method onClick=view.getClass().getMethod(listenerSetter,baseClickInjectClazz);
onClick.invoke(view,proxy);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
定义的MyInvokationHandler类
*/
public class MyInvokationHandler implements InvocationHandler {
//需要在onClick中执行activity.click();
private Object activity;
private Method activityMethod;
MyInvokationHandler(Object activity, Method activityMethod){
this.activity = activity;
this.activityMethod = activityMethod;
}
/**
* 就表示onClick的执行
* 程序执行onClick方法,就会转到这里来
* 因为框架中不直接执行onClick
* 所以在框架中必然有个地方让invoke和onClick关联上
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在这里去调用被注解了的click();
return activityMethod.invoke(activity,args);
}
}