练习手写IoC框架,深入理解反射、注解、动态代理
实现功能
通过依赖注入方式引入Activity布局、控件、点击事件,类似功能有个著名的框架ButterKnife 。不过他是通过APT (Annotation Processing Tool) 技术实现的。了解过Java反射的都知道它是个耗性能的操作,但是它带来的好处也是显而易见的,比如现在我们通过反射来注入布局等,可以消除很多findViewById的模板代码,如果界面View较多的情况下,大量的findViewById显得并不优雅。而APT技术不消耗性能,但是会在buid中生成很多补充代码,使我们的包体积变大。当前使用反射技术实现注入的目的,是为了更好的理解Java反射、自定义注解、动态代理等知识点。
常用API
域 | 方法 | 说明 |
---|---|---|
obj.getClass Class.forName(“类路径”) Object.class |
获取类的Class对象 | |
class | getMethods | 获取当前类及父类的公共方法 |
getDeclaredMethods | 获取当前类的所有方法 | |
getFeilds | 获取所有public属性 | |
getDeclaredFeilds | 获取所有属性 | |
method | getAnnotations | 获取注解 |
invoke() | 调用method,一个参数是方法所在的类实例,一个是方法参数 | |
feild | setAccessible(true) | 获取访问权限(针对受保护的属性如private) |
引入布局
Activity中常规引入布局如下:
public class MainActivity extends AppCompactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
注解方式引入:
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
//......
}
为了实现它,我们首先需要自定义ContentView注解,代码很简单
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
//对应括号里R.layout.xxx
int value();
}
主要是理解@Target和@Retention这两个注解, 他们用来修饰注解,是注解的注解,称为元注解。
@Target表示ContentView注解作用的位置,比如类、方法、属性、注解类型等;
@Retention表示ContentView注解的运行周期,
source:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略
class:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
runtime:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
因为我们需要在App运行的时候动态获取注解信息注入布局,所以选择Runtime周期
现在需要一个InjectManager容器去解析ContentView注解,然后给Activity类注入layout布局,
public class InjectManager {
//......
private static void injectLayout(Activity activity) {
//获取类
Class<? extends Activity> clazz = activity.getClass();
//获取ContentView注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
//有不用注解注入布局的activity
if (contentView != null) {
//这里的方法可以拿到R.layout.xxx
int layoutId = contentView.value();
try {
//反射获取activity的setContentView方法,并调用
Method method = clazz.getMethod("setContentView", int.class);
method.invoke(activity, layoutId);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
//......
}
这样就实现了布局注入的功能,是不是没那么复杂,主要还是对注解和反射的使用。需要注意的是我们要在Activity的onCreate方法中调用InjectManager.injectLayout(this),一般我们可以把它放在BaseActivity中,这点和ButterKnife相似。
</br>
控件的注入
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@InjectView(R.id.tv_sub)
private TextView tvSubject;
//......
}
和上面一样我们来定义injectView注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
//对应的R.id.XXX
int value();
}
然后在InjectManager里面实现功能。控件的注入一定是在布局注入之后完成的,还有和布局注入不同的是它有个赋值过程。这个下面继续,先看下代码:
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获取当前类的所有属性,和getFields不同 getFields获得某个类的所有的公共(public)的字段,包括父类中的公共字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//获取属性的InjectView注解
InjectView bindView = field.getAnnotation(InjectView.class);
//不管没有InjectView注解的属性
if (bindView == null) continue;
try {
//获取Activity的findViewById方法,因为该方法在父类中,所以用getMethod才行
Method findViewById = clazz.getMethod("findViewById", int.class);
//调用findViewById
Object view = findViewById.invoke(activity, bindView.value());
//!!!拿到控件之后我们还需要给tvSubject赋值,但是它是个private属性,所以需要用setAccessible打开访问权限再赋值
field.setAccessible(true);
field.set(activity, view);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
这样控件的注入就完成了。和布局的注入类似,但是关键点就在于要给我们自己在Activity中定义的属性赋值,以及怎么给私有的属性赋值问题,在注释中已经写的很清楚了。
</br>
注入事件
注入事件相对来说比较复杂,我们先看下一般我们怎么写事件代码的:
tvSubject.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
//do your self
}
}
);
我们给控件写点击事件,其实传入的是一个接口,然后执行回调方法,和上面两种很不同。目标事件注入方式:
@OnClick(R.id.tv_sub)
public void onTvSubClick(View view) {
Toast.makeText(this, ((TextView) view).getText(), Toast.LENGTH_SHORT).show();
}
我们怎么来实现动态执行回调接口,把它变成我们定义的onTvSubClick呢?这里就需要:动态代理。例如:
SayHello sayHello = (SayHello)Proxy.newProxyInstance(SayHello.class.getClassLoader(),
new Class[]{SayHello.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Toast.makeText(getBaseContext(), (proxy instanceof SayHello) + "", Toast.LENGTH_LONG).show();
return null;
}
});
sayHello.say();
我们先实现自定义OnClick注解,因为考虑到后面还有OnLongClick的注入,所以我们在定义@OnClick和@OnLongClick的时候需要区分类型,这里就需要用到上面说的注解的注解,注解类型它作用于注解之上,我们定义一个EventBase
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
//监听方法
String listenerSetter();
//监听的类型
Class listenerType();
//回调方法
String callbackListener();
}
然后定义Onclick
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class, callbackListener = "onClick")
public @interface OnClick {
int[] value();
}
这样就能通过EventBase注解来定义事件的名称、类型、回调方法,那再通过反射就很容易实现了。View.OnClickListener是View中定义的接口,我们通过java动态代理这个接口能够在运行时执行我们想要的操作method,
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获取该activity中所有方法
Method[] methods = clazz.getDeclaredMethods();
//遍历方法
for (Method method : methods) {
//获取方法所有的注解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//获取注解的类型
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType != null) {
//获取注解的EventBase注解
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if (eventBase != null) {
//获取EventBase注解的 监听setter方法 监听类型 回调方法
String listenerSetter = eventBase.listenerSetter();
Class listenerType = eventBase.listenerType();
String callbackListener = eventBase.callbackListener();
try {
OnClick click = (OnClick) annotation;
int[] values = click.value();
assert values != null;
//获取方法注解的value方法 拿到控件id数组
InjectEventProxyHandler handler = new InjectEventProxyHandler(activity);
//给原始回调 替换成 method
handler.add(callbackListener, method);
//通过动态代理EventBase要代理的接口
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, handler);
for (int value : values) {
//获取view
View view = activity.findViewById(value);
//因为监听方法在View这个系统类中
Method setXXX = View.class.getMethod(listenerSetter, listenerType);
setXXX.invoke(view, listener);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
}
下面就是实现InvocationHandler的invoke方法
class InjectEventProxyHandler implements InvocationHandler {
//防连点时间间隔
private static final int QUICK_EVENT_TIME_SPAN = 500;
// 目标对象,这里是Activity,因为我们的onTvSubClick定义在Activity中
private Object targetObject;
private long lastClickTime;
//替换的方法对应
private ArrayMap<String, Method> map = new ArrayMap<>();
InjectEventProxyHandler(Object targetObject) {
this.targetObject = targetObject;
}
//关联的这个实现类的方法被调用时将被执行
/*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (targetObject != null) {
//获取被代理接口的方法名
String name = method.getName();
//阻塞事件 1S n次点击
long timeSpan = System.currentTimeMillis() - lastClickTime;
if (timeSpan < QUICK_EVENT_TIME_SPAN) {
return null;
}
lastClickTime = System.currentTimeMillis();
//获取对应替换的方法
method = map.get(name);
assert method != null;
//是否有参数
if (method.getGenericParameterTypes().length == 0) {
return method.invoke(targetObject);
} else {
return method.invoke(targetObject, args);
}
}
return null;
}
/*
将各种callback 方法替换成我们自己定义的方法
*/
public void add(String callback, Method method) {
map.put(callback, method);
}
}
虽然贴了很多代码,但在自己的实践过程中对反射,注解和动态代理有了更好的理解。这里代码的地址: https://github.com/jarrod-chen/IoCApplication