反射分析一

獲取函數
使用僞代碼來簡化源碼的複雜邏輯,只關注主線邏輯。

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

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