注解的核心是反射,就是定义了注解参数后,通过注入方法获取到注入的对象,查找相应的属性,如果包含注解信息,则可以拿到相应的value等属性,比如Filed属性 ,最终会通过field.invoke(obj1,obj2) 反射设置值,最终实现属性值的设定
1.注入View
定义一个view的注解,周期指定运行时,Filed属性
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
view注解比较简单,拿到注入的对象,直接反射获取值就可以
private static void injectView(Object object) {
try {
Class<?> mClass = object.getClass();
Field[] fields = mClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(BindView.class)) {
BindView bindView = field.getAnnotation(BindView.class);
int id = bindView.value();
Method findViewById = mClass.getMethod("findViewById", int.class);
View view = (View) findViewById.invoke(object, id);
field.set(object, view);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
2.点击事件注入
事件注入的核心是代理模式,我们常用的接口代理会这么写
private static class ProxyFactory implements InvocationHandler {
private Object object;
ProxyFactory(Object object) {
this.object = object;
}
public Object newPorxy() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), ProxyFactory.this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//dosomething
Object result = method.invoke(object, args);
//dosomething
return result;
}
}
其中object会把接口的实现类以接口形式传入,最终编译生成出的字节码文件其实就算一个实现了接口的类文件
getInterfaces获取的是该类实现的接口,当只实现一个接口获取的那么就只有一个
比如,我定义了一个接口
public interface IT{
void test(String test);
}
最终代理生成的文件格式大概是这样的
public class $Proxy0 extends Proxy implements IT{
private static Method m1,m2,m3; //会有很多,列出类及其父类的所以方法
static {
try {
m1 = Class.forName("xx.xx.IT").getMethod("test",new Class[]{Class.forName("java.lang.String")});
//所有方法都按照这个规则列出
}catch (Exception e){
e.printStackTrace();
}
}
protected $Proxy0(InvocationHandler h) {
super(h);
}
@Override
public void test(String test) {
try {
h.invoke(this,m1,new Object[]{test});
}catch (Exception e){
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
其实就是帮我们生成了静态代理处理的类,代理的对象是接口的实现类,而我们点击事件OnClickListener实际也是一个接口,那么也可以通过代理方式实现
而我们的需求就是要知道点击触发的时机,然后动态反射到我们注解标记的方法中,那么就可以通过代理方式处理
先定义一个注解的事件处理,用于标记注解
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseEvent {
String setterName(); // setOnClickListener
Class listenerType(); //View.OnClickListener
String methodName(); // onClick(View view) ->> 这个感觉不要也可以,可以通过第二个获取 实际好像也没用到,因为这事件监听都是一个回调,可能多回调能用上?
}
因为后面反射是需要知道设置点击事件的方法名,和点击事件对应的接收类对象
再定义一个点击事件的注解,用于方法标记
@BaseEvent(setterName = "setOnClickListener", listenerType = View.OnClickListener.class, methodName = "onClick")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
最后定义点击时间方法代理类
public class ListenerInvokeHandler implements InvocationHandler {
private Object activity; //这里对应到注册onClick的点击方法中
private Method onClickMethod;
public ListenerInvokeHandler(Object activity, Method onClickMethod) {
this.activity = activity;
this.onClickMethod = onClickMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
onClickMethod.invoke(activity, args);
return null;
}
}
调用方法
Object proxyInvoke = Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{listener}, invokeHandler);
这里的listener类就可以传上面的View.OnClickListener
这里等同于我们创建了一个上面OnClickListener的实现类,传入的就算最终注解标记的方法,当触发点击则去反射调用标记的方法
完整的是
private static void injectMethod(Object object) {
try {
Class<?> mClass = object.getClass();
Method methods[] = mClass.getDeclaredMethods();
for (Method method : methods) { //method最终过滤出我们的 testOnClick方法
if (method.isAnnotationPresent(OnClick.class)) {
OnClick onClick = method.getAnnotation(OnClick.class);
int[] ids = onClick.value();
//这里有些差异,是 annotationType()方法获取
BaseEvent baseEvent = onClick.annotationType().getAnnotation(BaseEvent.class);
String setterName = baseEvent.setterName();
Class<?> listener = baseEvent.listenerType();
String methodName = baseEvent.methodName();
ListenerInvokeHandler invokeHandler = new ListenerInvokeHandler(object, method);
//查找相应的view设置监听代理
for (int id : ids) {
//代理传入activity对象,和注册的点击方法 这里相当于写了一个 onClickListener的实现类
Object proxyInvoke = Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{listener}, invokeHandler);
Method findViewByID = mClass.getMethod("findViewById", int.class);
View targetView = (View) findViewByID.invoke(object, id);
Method setOnClickListener = targetView.getClass().getMethod(setterName, listener);
//给view注册代理监听,最后的点击会回调上面的invoke方法内,再回到activity中注册的点击方法内
setOnClickListener.invoke(targetView, proxyInvoke);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
使用
@BindLayout(R.layout.activity_butterknife_tst)
public class ButterknifeTestAcivity extends AppCompatActivity {
@BindView(R.id.btn_1)
Button btn1;
@BindView(R.id.btn_2)
Button btn2;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InJectUtils.injectLayout(this);
InJectUtils.injectView(this);
InJectUtils.injectMethod(this);
}
@OnClick({R.id.btn_1, R.id.btn_2})
public void myTestClick(View view) {
switch (view.getId()) {
case R.id.btn_1:
toast("点击了按钮1");
break;
case R.id.btn_2:
toast("点击了按钮2");
break;
}
}
private void toast(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
}