反射分析一

获取函数
使用伪代码来简化源码的复杂逻辑,只关注主线逻辑。

Java中万物都是对象,如果使用过反射的同学,应该有所感受。Class是最基本的对象,而其中的函数(method)、属性(field)等,也可以看作是一个对象。不然,method不是个东西,我们怎么调用呢?

首先看下反射的使用

// 创建对象
Object innerClazz = InnerClazz.class.newInstance();
// 获取函数
Method doTaskMethod = InnerClazz.class.getDeclaredMethod("doTask");
// 调用函数
doTaskMethod.invoke(innerClazz);

获取所有函数信息

Class对象内部有一个函数:getDeclaredMethod,传入函数名和参数后,可以得到对应的Method对象。该函数的实现伪代码如下。

// *** 伪代码 ***
void getDeclaredMethod(String methodName , Class<?> params){
    // 从class中获取所有的函数信息
    Method[] declaredMethods = privateGetDeclaredMethods();
    // 根据methodName 和 参数,查找method
    Method method = searchMethod(declaredMethods, methodName, params);
}

首先,要获取Method对象,那就从该Class对象中寻找。privateGetDeclaredMethods()就是获取该Class对象的所有函数信息(返回的是Method数组)。接着,根据函数名(methodName)和函数参数(params)找到对应的Method对象。

// *** privateGetDeclaredMethods() 伪代码 ***
Method[] privateGetDeclaredMethods(){
    // 可以看成Class的缓存信息,里面有函数信息、属性信息等
    ReflectionData<T> rd = reflectionData();
    // 从缓存中获取 函数列表
    Method[] res = rd.declaredMethods;
    if (res == null){
        // res为空 向jvm请求获取res
        res = JVM.getMethods();
        // 缓存到rd中
        rd.declaredMethods = res;
    }
    return res;
}

为了提高性能,所以需要对所有 函数 进行一个缓存,缓存的对象就是ReflectionData。当发现缓存没有,就可以使用native函数向JVM请求获取函数信息,同时保存到缓存中。

查找对应的函数

经过上面的步骤后,缓存中保存了Class的函数信息,同时也返回了所有函数信息,现在就可以根据函数名 和 函数参数 对比查找需要反射的Method对象了。

Method searchMethods(Method[] methods, String methodName, Class<?> params){
    Method res = null;
    // for循环挨个比较
    for(int i = 0; i<methods.length; i++){
        if(满足条件){
            res = methods[i]
        }
    }
    return res == null ? res.copy();
}

搜索函数很简单,就是函数名和参数的比较,如果没找到,就会在外部收到null,抛出NoSuchMethodException异常。
如果找到了,就复制一个该函数对象。相当于从原始的函数信息中复制一份出来。

函数的调用(invoke)

在获取到了Method对象后,我们进行第二步:函数的调用。

// Method的invoke函数 伪代码
Object invoke(Object obj, Object... args){
    MethodAccessor ma = methodAccessor;
    if (ma == null){
        // 创建一个MethodAccessor
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj,args);
}

Method 的 invoke函数内部调用了MethodAccessor的invoke函数。

MethodAccessor是个接口,实现类有3个,其中1个是作为代理类,另外2个才是真正干活的。

代理类:DelegatingMethodAccessorImpl
干活类:NativeMethodAccessorImpl 和 MagicAccessorImpl

MethodAccessor调用invoke后,进入DelegatingMethodAccessorImpl的invoke函数,其中再调用实际的干活类。

// NativeMethodAccessorImpl的invoke
Object invoke(){
    if(调用次数 > 15){
        为DelegatingMethodAccessorImpl 设置 MagicAccessorImpl作为实际干 活类。
    }
    调用 native 的invoke
}

NativeMethodAccessorImpl的invoke函数中,会记录该函数的调用次数,如果大于15次,就设置代理类的干活类为MagicAccessorImpl
从类名中可以看出:NativeMethodAccessorImpl是native的调用者,MagicAccessorImpl相对的是Java层面的invoke调用者。

native函数底层是让JVM调用JVM_InvokeMethod()函数
Java层的调用可以从MethodAccessorGenerator源码中看出,实际是在内存中生成函数的字节码,让JVM直接调用。

其中,这里最绕的就是调用次数达到了15次后,使用Java生成字节码来实现函数的调用。

为什么大于15次需要更换delegate?
参考其他文章的结论:

动态实现(Java)和本地实现(native)相比,执行速度要快上20倍,这是因为动态实现直接执行字节码,不用从java到c++ 再到java 的转换,但是因为生成字节码的操作比较耗费时间,所以如果仅一次调用的话反而是本地时间快3到4倍。

结尾

简单从Java源码层面分析了反射的执行过程,其中还有许多细节需要研究。

包括两个实现类为何执行速度相差这么大?
native层面,是如何实现invoke的?
Android中的反射涉及到hidden API,是如何工作的?

参考

java 反射原理(jvm是如何实现反射的) - 简书
JAVA深入研究——Method的Invoke方法。 - 寂静沙滩 - 博客园
假笨说-从一起GC血案谈到反射原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章