Github上有很多关于事件注入的框架,大致上原理都差不多,但很少详细介绍整个框架的各个组成部分以及实现原理,本文以作者[email protected]的xUtils为例详细介绍整个框架的实现。
首先读者需要了解几个知识点:
1、反射(http://blog.csdn.net/yongjian1092/article/details/7364451)
2、注解 (http://www.cnblogs.com/linjiqin/archive/2011/02/16/1956426.html)
3、动态代理 (http://www.cnblogs.com/xiaoluo501395377/p/3383130.html)
这方面的文章很多,大家可以搜一搜,都很容易弄明白,下面开始上代码:
@ContentView(R.layout.activity_main)
public class MainActivity extends Activity {
@ViewInject(R.id.text_view)
TextView text_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjector.inject(this);
}
@Event(R.id.button)
private void onClickButton(View view){
text_view.setText("点击了按钮");
}
}
如果大家使用过事件注入,相信上面这段代码都很熟悉,这里面有三个使用注解进行标注的地方。
@ContentView替代原来在onCreate函数中写的setContentView方法。
@ViewInject替换findViewById
@Event替换点击事件的注册
以下是三个注解的代码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
/* parent view id */
int parentId() default 0;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {
int[] value();
int[] parentId() default 0;
Class> type() default View.OnClickListener.class;
String setter() default "";
String method() default "";
}
public static void inject(Activity activity) {
//获取Activity的ContentView的注解
Class> handlerType = activity.getClass();
try {
ContentView contentView = findContentView(handlerType);
if (contentView != null) {
int viewId = contentView.value();
if (viewId > 0) {
Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
setContentViewMethod.invoke(activity, viewId);
}
}
} catch (Throwable ex) {
}
injectObject(activity, handlerType, new ViewFinder(activity));
}
private static ContentView findContentView(Class> thisCls) {
if (thisCls == null || Object.class.equals(thisCls)) {
return null;
}
ContentView contentView = thisCls.getAnnotation(ContentView.class);
if (contentView == null) {
return findContentView(thisCls.getSuperclass());
}
return contentView;
}
Field[] fields = handlerType.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers())) {
continue;
}
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
try {
View view = finder.findViewById(viewInject.value(), viewInject.parentId());
if (view != null) {
field.setAccessible(true);
field.set(handler, view);
}
} catch (Throwable ex) {
}
}
}
}
public View findViewById(int id, int pid) {
View pView = null;
if (pid > 0) {
pView = this.findViewById(pid);
}
View view = null;
if (pView != null) {
view = pView.findViewById(id);
} else {
view = this.findViewById(id);
}
return view;
}
对layout和view的注入都比较简单,也很容易理解,接下来是重头戏,也就是点击事件的注入实现,需要用到动态代理,也就是AOP(面向切面编程)。做客户端的同学应该很少有接触这个的,通常在web项目中使用比较多。这部分知识大家也可以搜索一下,很多详细介绍,不废话了,上代码:
Method[] methods = handlerType.getDeclaredMethods();
if (methods != null && methods.length > 0) {
for (Method method : methods) {
if (Modifier.isStatic(method.getModifiers())
|| !Modifier.isPrivate(method.getModifiers())) {
continue;
}
//检查当前方法是否是event注解的方法
Event event = method.getAnnotation(Event.class);
if (event != null) {
method.setAccessible(true);
try {
//id参数
int[] values = event.value();
int[] parentIds = event.parentId();
int parentIdsLen = parentIds == null ? 0 : parentIds.length;
//循环所有id,生成ViewInfo并添加代理反射
for (int i = 0; i < values.length; i++) {
ViewInfo info = new ViewInfo();
info.value = values[i];
info.parentId = parentIdsLen > i ? parentIds[i] : 0;
EventListenerManager.addEventMethod(finder, info, event, handler, method);
}
} catch (Throwable ex) {
}
}
}
}
public class EventListenerManager {
private EventListenerManager() {
}
/**
* k1: viewInjectInfo
* k2: interface Type
* value: listener
*/
private final static DoubleKeyValueMap, Object>
listenerCache = new DoubleKeyValueMap, Object>();
public static void addEventMethod(
//根据页面或view holder生成的ViewFinder
ViewFinder finder,
//根据当前注解ID生成的ViewInfo
ViewInfo info,
//注解对象
Event event,
//页面或view holder对象
Object handler,
//当前注解方法
Method method) {
try {
View view = finder.findViewByInfo(info);
if (view != null) {
// 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener
Class> listenerType = event.type();
// 默认为空,注解接口对应的Set方法,比如setOnClickListener方法
String listenerSetter = event.setter();
if (TextUtils.isEmpty(listenerSetter)) {
listenerSetter = "set" + listenerType.getSimpleName();
}
String methodName = event.method();
boolean addNewMethod = false;
/*
根据View的ID和当前的接口类型获取已经缓存的接口实例对象,
比如根据View.id和View.OnClickListener.class两个键获取这个View的OnClickListener对象
*/
Object listener = listenerCache.get(info, listenerType);
DynamicHandler dynamicHandler = null;
/*
如果接口实例对象不为空
获取接口对象对应的动态代理对象
如果动态代理对象的handler和当前handler相同
则为动态代理对象添加代理方法
*/
if (listener != null) {
dynamicHandler = (DynamicHandler) Proxy.getInvocationHandler(listener);
addNewMethod = handler.equals(dynamicHandler.getHandler());
if (addNewMethod) {
dynamicHandler.addMethod(methodName, method);
}
}
// 如果还没有注册此代理
if (!addNewMethod) {
dynamicHandler = new DynamicHandler(handler);
dynamicHandler.addMethod(methodName, method);
// 生成的代理对象实例,比如View.OnClickListener的实例对象
listener = Proxy.newProxyInstance(
listenerType.getClassLoader(),
new Class>[]{listenerType},
dynamicHandler);
listenerCache.put(info, listenerType, listener);
}
Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
setEventListenerMethod.invoke(view, listener);
}
} catch (Throwable ex) {
}
}
public static class DynamicHandler implements InvocationHandler {
// 存放代理对象,比如Fragment或view holder
private WeakReference
我们一步步来分析这个看上去很复杂的逻辑,首先看看@Event这个注解,这里面的几个函数分别是:
value() -- 需要添加点击事件的View的Id
parentId() -- 需要添加点击事件的View的父ViewId,通常不需要用到
type() -- 点击事件的类型,默认值是View.OnClickListener.class
setter()和method()这两个函数作为后续扩展用,暂时不需要。
首先获取一个需要添加的事件函数名, 比如“setOnClickListener”,也就是代码listenerSetter = "set" + listenerType.getSimpleName(); 这个用于后面给View反射注入代理实例。接下来取到被注解的方法的名称event.method(),在代理切面中使用这个名称当做Key查找到真正需要被调用的Method。接下来看代理切面的实现,也就是DynamicHandler这个类,其中两个属性一个方法:
WeakReference
HashMap
如果已经了解了AOP的同学,应该会明白invoke这个函数的作用,切面拦截就是在这里实现的。
接下来看动态代理的实现:
dynamicHandler = new DynamicHandler(handler);
dynamicHandler.addMethod(methodName, method);
// 生成的代理对象实例,比如View.OnClickListener的实例对象
listener = Proxy.newProxyInstance(
listenerType.getClassLoader(),
new Class>[]{listenerType},
dynamicHandler);
Class cl = getProxyClass(loader, interfaces);
// 调用代理对象的构造方法(也就是$Proxy0(InvocationHandler h))
Constructor cons = cl.getConstructor(constructorParams);
// 生成代理类的实例并把MyInvocationHandler的实例传给它的构造方法
return (Object) cons.newInstance(new Object[] { h });
public static Class> getProxyClass(ClassLoader loader,
Class>... interfaces)
throws IllegalArgumentException
{
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 声明代理对象所代表的Class对象
Class proxyClass = null;
String[] interfaceNames = new String[interfaces.length];
Set interfaceSet = new HashSet(); // for detecting duplicates
// 遍历目标类所实现的接口
for (int i = 0; i < interfaces.length; i++) {
// 拿到目标类实现的接口的名称
String interfaceName = interfaces[i].getName();
Class interfaceClass = null;
try {
// 加载目标类实现的接口到内存中
interfaceClass = Class.forName(interfaceName, false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != interfaces[i]) {
throw new IllegalArgumentException(
interfaces[i] + " is not visible from class loader");
}
// 把目标类实现的接口代表的Class对象放到Set中
interfaceSet.add(interfaceClass);
interfaceNames[i] = interfaceName;
}
// 把目标类实现的接口名称作为缓存(Map)中的key
Object key = Arrays.asList(interfaceNames);
Map cache;
synchronized (loaderToCache) {
// 从缓存中获取cache
cache = (Map) loaderToCache.get(loader);
if (cache == null) {
// 如果获取不到,则新建地个HashMap实例
cache = new HashMap();
// 把HashMap实例和当前加载器放到缓存中
loaderToCache.put(loader, cache);
}
}
synchronized (cache) {
do {
// 根据接口的名称从缓存中获取对象
Object value = cache.get(key);
if (value instanceof Reference) {
proxyClass = (Class) ((Reference) value).get();
}
if (proxyClass != null) {
// 如果代理对象的Class实例已经存在,则直接返回
return proxyClass;
} else if (value == pendingGenerationMarker) {
try {
cache.wait();
} catch (InterruptedException e) {
}
continue;
} else {
cache.put(key, pendingGenerationMarker);
break;
}
} while (true);
}
try {
// 这里就是动态生成代理对象的最关键的地方
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces);
try {
// 根据代理类的字节码生成代理类的实例
proxyClass = defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
// add to set of all generated proxy classes, for isProxyClass
proxyClasses.put(proxyClass, null);
}
return proxyClass;
}
ProxyGenerator gen = new ProxyGenerator(name, interfaces);
// 这里动态生成代理类的字节码,由于比较复杂就不进去看了
final byte[] classFile = gen.generateClassFile();
// 如果saveGeneratedFiles的值为true,则会把所生成的代理类的字节码保存到硬盘上
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Void run() {
try {
FileOutputStream file =
new FileOutputStream(dotToSlash(name) + ".class");
file.write(classFile);
file.close();
return null;
} catch (IOException e) {
throw new InternalError(
"I/O exception saving generated file: " + e);
}
}
});
}
// 返回代理类的字节码
return classFile;
}
Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
setEventListenerMethod.invoke(view, listener);
最后回到切面拦截的地方,也就是当这个view的点击出发后,需要调用注解函数的实现,直接看DynamicHandler类的invoke方法,这个方法有三个参数:
Object proxy -- 此为动态代理实例,上面已经讲过了代理实例的生成以及注入
Method method -- 此为出发的代理实例函数(比如代理的OnClickListener,那么这里的method就应该是onClick函数)
Object[] args -- 此为代理函数的参数
再看这个方法的实现:
if (handler != null) {
String eventMethod = method.getName();
if (methodMap.size() == 1) {
for (Method v : methodMap.values()) {
method = v;
break;
}
} else {
method = methodMap.get(eventMethod);
}
if (method != null) {
if ("onClick".equals(eventMethod)) {
long timeSpan = System.currentTimeMillis() - lastClickTime;
if (timeSpan < CLICK_TIME_SPAN) {
return null;
}
lastClickTime = System.currentTimeMillis();
}
try {
return method.invoke(handler, args);
} catch (Throwable ex) {
。。。
}
return null;
}
PS:小弟第一次写博客,有写的不对的地方请各位大牛指点。另外如果使用这个框架,在打混淆包的时候需要在混淆文件中加一行
-keepclassmembers class * {
*** *Event(...);
}
不知道怎么上传附件,如果想要整个框架的源代码可以把邮箱留言给我。