反射分析一

作者: space0o0 | 来源:发表于2021-01-20 14:18 被阅读0次

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

    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血案谈到反射原理

    相关文章

      网友评论

        本文标题:反射分析一

        本文链接:https://www.haomeiwen.com/subject/kgdkzktx.html