一个Android事件注入框架详解

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 "";
}

注解是JDK1.5以后引入的新特性,写法就是在interface前加一个@,在编译之后会生成一个标准的interface,大家可以在代码运行时打个断点看一看。继续上代码

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;
}

这个findContentView用了递归,如果当前activity找不到@ContentView注解,则一直往上查找父类中的注解。找到注解的layout之后,用反射调用了activity的setContentView方法。至此对于layout布局文件的注入已经完成,接着往下分析View的注入。

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;
}

这个地方其实也是通过activity的findViewById来获取到注解的View实例,然后使用反射将实例注入到activity里被注解对应的view上,需要注意的是通常在activity里申明的view都是private,而private的属性没法通过反射直接初始化,所以需要调用setAccessible函数设置为true,这样属性就可以被访问了,但调用该方法并不意味着原有属性的访问权限修饰已改变,也就是该属性仍然是private。

对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) {
                    }
                }
            }
}

同样的,查找整个Activity中@Event注解,将private的注解函数访问权限做一下设置。由于这里使用的是动态代理,所以被Event注解的方法参数必须跟对应的点击事件参数一模一样。比如注解一个onClick事件,那你被注解的方法参数就必须是(View view),如果注解的是AdapterView.OnItemClickListener,被注解的方法参数必须是(AdapterView parent, View view, int position, long id),后面会讲到为什么要这么写。下面是EventListenerManager整个类的代码,这里对多点击事件缓存,还有多次点击都做了相应处理:

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 handlerRef;
        // 存放代理方法
        private final HashMap methodMap = new HashMap(1);

        private static long lastClickTime = 0;
        private final static long CLICK_TIME_SPAN = 400;

        public DynamicHandler(Object handler) {
            this.handlerRef = new WeakReference(handler);
        }

        public void addMethod(String name, Method method) {
            methodMap.put(name, method);
        }

        public Object getHandler() {
            return handlerRef.get();
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object handler = handlerRef.get();
            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) {
                        throw new RuntimeException("invoke method error:" +
                                handler.getClass().getName() + "#" + eventMethod, ex);
                    }
                }
            }
            return null;
        }
    }
}我们一步步来分析这个看上去很复杂的逻辑,首先看看@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 handlerRef  --  这里的Object就是activity实例,为了避免内存泄露,所以使用弱引用。

HashMap methodMap  --  这个map用于缓存当前activity中的所有注解函数,key就是刚才提到的event.method()

如果已经了解了AOP的同学,应该会明白invoke这个函数的作用,切面拦截就是在这里实现的。


接下来看动态代理的实现:

dynamicHandler = new DynamicHandler(handler);
dynamicHandler.addMethod(methodName, method);
// 生成的代理对象实例,比如View.OnClickListener的实例对象
listener = Proxy.newProxyInstance(
           listenerType.getClassLoader(),
           new Class[]{listenerType},
           dynamicHandler);


动态代理需要用到Proxy这个类,这里使用newProxyInstance函数初始化一个代理实例出来,跟进去源码看一下newProxyInstance函数的主要实现:

Class cl = getProxyClass(loader, interfaces);   
// 调用代理对象的构造方法(也就是$Proxy0(InvocationHandler h))  
Constructor cons = cl.getConstructor(constructorParams);  
// 生成代理类的实例并把MyInvocationHandler的实例传给它的构造方法  
return (Object) cons.newInstance(new Object[] { h });  


然后在看看getProxyClass的实现:

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;  
}

最后是 generateProxyClass,这里是真正生成代理类class字节码的地方,虚拟机加载动态代理的字节码:

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;  
}

回到正题,生成了动态代理实例之后,接着继续用反射给需要添加点击事件的View做监听注入:

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;
}

这里可以看到从methodMap取出被缓存的注解method,然后替换了invoke中的代理函数,最后做了method.invoke(handler, args)调用,这里就会触发到被注解的函数真正的调动。 那么再回过头来看上面说过一点,被注解的点击事件调用函数必须跟Listener中的函数参数一样,原因就在这里了,如果参数不一致,invoke的时候会报错,虚拟机在调用注解函数是会发现传递进来的参数不匹配。


PS:小弟第一次写博客,有写的不对的地方请各位大牛指点。另外如果使用这个框架,在打混淆包的时候需要在混淆文件中加一行

-keepclassmembers class * {
   *** *Event(...);
}

不知道怎么上传附件,如果想要整个框架的源代码可以把邮箱留言给我。


你可能感兴趣的:(Android)