預備知識
瞭解 Java 反射基本用法
看完本文可以達到什麼程度
瞭解 Java 反射原理及 Java 反射效率低的原因
文章概覽
我們在 Java 開發中,難免會接觸到反射,而在一些框架中,反射的運用更是常見。我相信,每次提到反射,大家的第一反應一定是反射效率低,儘量少使用。
但是反射的效率到底低多少?反射效率低的原因在哪裏?
這篇文章就來探索一下這些問題。
由於本機上安裝的是 openjdk 12,所以這裏就使用 openjdk 12 源碼進行分析。
更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集
免費提供簡歷修改建議,獲取大廠面試PDF
先放結論
Java 反射效率低主要原因是:
- Method#invoke 方法會對參數做封裝和解封操作
- 需要檢查方法可見性
- 需要校驗參數
- 反射方法難以內聯
- JIT 無法優化
一、Java 反射原理--獲取要反射的方法
1.1 反射的使用
我們先來看看 Java 反射使用的一段代碼:
public class RefTest {
public static void main(String[] args) {
try {
Class clazz = Class.forName("com.zy.java.RefTest");
Object refTest = clazz.newInstance();
Method method = clazz.getDeclaredMethod("refMethod");
method.invoke(refTest);
} catch (Exception e) {
e.printStackTrace();
}
}
public void refMethod() {
}
}
我們在調用反射時,首先會創建 Class 對象,然後獲取其 Method 對象,調用 invoke 方法。 獲取反射方法時,有兩個方法,getMethod
和 getDeclaredMethod
,我們就從這兩個方法開始,一步步看下反射的原理。
接下來就進入代碼分析,大家做好準備
1.2 getMethod / getDeclaredMethod
這裏我們先整體看一下 getMethod
和 getDeclaredMethod
的實現。
class Class {
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. 檢查方法權限
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
// 2. 獲取方法
Method method = getMethod0(name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. 返回方法的拷貝
return getReflectionFactory().copyMethod(method);
}
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. 檢查方法是權限
checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
}
// 2. 獲取方法
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. 返回方法的拷貝
return getReflectionFactory().copyMethod(method);
}
}
從上面的代碼,我們可以看到,獲取方法的流程分三步走:
檢查方法權限
獲取方法 Method 對象
返回方法的拷貝
這裏主要有兩個區別:
1.getMethod
中 checkMemberAccess
傳入的是 Member.PUBLIC
,而 getDeclaredMethod
傳入的是 Member.DECLARED
這兩個值有什麼區別呢?我們看下代碼中的註釋:
interface Member {
/**
* Identifies the set of all public members of a class or interface,
* including inherited members.
*/
public static final int PUBLIC = 0;
/**
* Identifies the set of declared members of a class or interface.
* Inherited members are not included.
*/
public static final int DECLARED = 1;
}
註釋裏清楚的解釋了 PUBLIC 和 DECLARED 的不同,PUBLIC 會包括所有的 public 方法,包括父類的方法,而 DECLARED 會包括所有自己定義的方法,public,protected,private 都在此,但是不包括父類的方法。
這也正是 getMethod
和 getDeclaredMethod
的區別
getMethod
中獲取方法調用的是getMethod0
,而getDeclaredMethod
獲取方法調用的是privateGetDeclaredMethods
關於這個區別,這裏簡單提及一下,後面具體分析代碼。privateGetDeclaredMethods
是獲取類自身定義的方法,參數是boolean publicOnly
,表示是否只獲取公共方法。
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
//...
}
而 getMethod0
會遞歸查找父類的方法,其中會調用到 privateGetDeclaredMethods
方法。
既然我們上面看了 getMethod
和 getDeclaredMethod
的區別,我們自然選擇 getMethod
方法進行分析,這樣可以走到整個流程
1.3 getMethod 方法
getMethod
方法流程如下圖:
class Class {
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. 檢查方法權限
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
// 2. 獲取方法 Method 對象
Method method = getMethod0(name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. 返回方法拷貝
return getReflectionFactory().copyMethod(method);
}
}
我們上面說到獲取方法分三步走:
檢查方法權限
獲取方法 Method 對象
返回方法的拷貝
我們先看看檢查方法權限做了些什麼事情。
1.3.1 checkMemberAccess
class Class {
private void checkMemberAccess(SecurityManager sm, int which,
Class<?> caller, boolean checkProxyInterfaces) {
/* Default policy allows access to all {@link Member#PUBLIC} members,
* as well as access to classes that have the same class loader as the caller.
* In all other cases, it requires RuntimePermission("accessDeclaredMembers")
* permission.
*/
final ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (which != Member.PUBLIC) {
final ClassLoader cl = getClassLoader0();
if (ccl != cl) {
sm.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
}
}
this.checkPackageAccess(sm, ccl, checkProxyInterfaces);
}
}
在這裏可以看到,對於非 Member.PUBLIC 的訪問,會增加一項檢測,SecurityManager.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION)
; 這項檢測需要運行時申請 RuntimePermission("accessDeclaredMembers")
。
這裏就不繼續往下看了,方法整體是在檢查是否可以訪問對象成員。
接着看下是如何獲取方法的 Method 對象。
1.3.2 getMethod0
class Class {
private Method getMethod0(String name, Class<?>[] parameterTypes) {
PublicMethods.MethodList res = getMethodsRecursive(
name,
parameterTypes == null ? EMPTY_CLASS_ARRAY : parameterTypes,
/* includeStatic */ true);
return res == null ? null : res.getMostSpecific();
}
}
這裏是通過 getMethodsRecursive
獲取到 MethodList
對象,然後通過 MethodList#getMostSpecific
方法篩選出對應的方法。MethodList#getMOstSpecific
會篩選返回值類型最爲具體的方法,至於爲什麼會有返回值的區別,後面會講到。
(這裏的具體,指的是有兩個方法,返回值分別是 Child 和 Parent,Child 繼承自 Parent,這裏會篩選出返回值爲 Child 的方法)。
接着看 getMethodsRecursive
方法,是如何獲取方法的
1.3.3 getMethodsRecursive
class Class {
private PublicMethods.MethodList getMethodsRecursive(String name,
Class<?>[] parameterTypes,
boolean includeStatic) {
// 1. 獲取自己的 public 方法
Method[] methods = privateGetDeclaredMethods(/* publicOnly */ true);
// 2. 篩選符合條件的方法,構造 MethodList 對象
PublicMethods.MethodList res = PublicMethods.MethodList
.filter(methods, name, parameterTypes, includeStatic);
// 找到方法,直接返回
if (res != null) {
return res;
}
// 3. 沒有找到方法,就獲取其父類,遞歸調用 getMethodsRecursive 方法
Class<?> sc = getSuperclass();
if (sc != null) {
res = sc.getMethodsRecursive(name, parameterTypes, includeStatic);
}
// 4. 獲取接口中對應的方法
for (Class<?> intf : getInterfaces(/* cloneArray */ false)) {
res = PublicMethods.MethodList.merge(
res, intf.getMethodsRecursive(name, parameterTypes,
/* includeStatic */ false));
}
return res;
}
}
這裏獲取方法有四個步驟:
- 通過
privateGetDeclaredMethods
獲取自己所有的 public 方法- 通過
MethodList#filter
查找 方法名,參數相同的方法,如果找到,直接返回- 如果自己沒有實現對應的方法,就去父類中查找對應的方法
- 查找接口中對應的方法
通過上面四個步驟,最終獲取到的是一個 MethodList
對象,是一個鏈表結點,其 next
指向下一個結點。也就是說,這裏獲取到的 Method
會有多個。
這裏稍微解釋一下,在我們平時編寫 Java 代碼時,同一個類是不能有方法名和方法參數都相同的方法的,而實際上,在 JVM 中,一個方法簽名是和 返回值,方法名,方法參數 三者相關的。
也就是說,在 JVM 中,可以存在 方法名和方法參數都相同,但是返回值不同的方法。
所以這裏返回的是一個方法鏈表。
所以上面最終返回方法時會通過 MethodList#getMostSpecific 進行返回值的篩選,篩選出返回值類型最具體的方法。
這裏我們先暫停回顧一下整體的調用鏈路
getMethod -> getMethod0 -> getMethodsRecursive -> privateGetDeclaredMethods
通過函數調用,最終會調用到 privateGetDeclaredMethods
方法,也就是真正獲取方法的地方。
1.3.4 privateGetDeclaredMethods
class Class {
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
Method[] res;
// 1. 通過緩存獲取 Method[]
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// 2. 沒有緩存,通過 JVM 獲取
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicMethods = res;
} else {
rd.declaredMethods = res;
}
}
return res;
}
}
在 privateGetDeclaredMethods
獲取方法時,有兩個步驟:
relectionData
通過緩存獲取
如果緩存沒有命中的話,通過getDeclaredMethods0
獲取方法
先看看 relectionData
方法:
class Class {
private ReflectionData<T> reflectionData() {
SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
int classRedefinedCount = this.classRedefinedCount;
ReflectionData<T> rd;
if (reflectionData != null &&
(rd = reflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
// else no SoftReference or cleared SoftReference or stale ReflectionData
// -> create and replace new instance
return newReflectionData(reflectionData, classRedefinedCount);
}
}
在 Class 中會維護一個 ReflectionData
的軟引用,作爲反射數據的緩存。ReflectionData
結構如下:
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Cached names
String simpleName;
String canonicalName;
static final String NULL_SENTINEL = new String();
// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;
}
可以看到,保存了 Class 中的屬性和方法。
如果緩存爲空,就會通過 getDeclaredMethods0
從 JVM
中查找方法。getDeclaredMethods0
是一個 native 方法,這裏暫時先不看
通過上面幾個步驟,就獲取到 Method 數組。
這就是 getMethod
方法的整個實現了。
我們再回過頭看一下 getDeclaredMethod
方法的實現,通過 privateGetDeclaredMethods
獲取方法以後,會通過 searchMethods
對方法進行篩選。
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// ...
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
// ...
}
searchMethods
方法實現比較簡單,就是對比方法名,參數,方法返回值。
class Class {
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
ReflectionFactory fact = getReflectionFactory();
Method res = null;
for (Method m : methods) {
// 比較方法名
if (m.getName().equals(name)
// 比較方法參數
&& arrayContentsEq(parameterTypes,
fact.getExecutableSharedParameterTypes(m))
// 比較返回值
&& (res == null
|| (res.getReturnType() != m.getReturnType()
&& res.getReturnType().isAssignableFrom(m.getReturnType()))))
res = m;
}
return res;
}
}
1.3.5 Method#copy
在獲取到對應方法以後,並不會直接返回,而是會通過 getReflectionFactory().copyMethod(method);
返回方法的一個拷貝。
最終調用的是 Method#copy
,我們來看看其實現。
class Method {
Method copy() {
// This routine enables sharing of MethodAccessor objects
// among Method objects which refer to the same underlying
// method in the VM. (All of this contortion is only necessary
// because of the "accessibility" bit in AccessibleObject,
// which implicitly requires that new java.lang.reflect
// objects be fabricated for each reflective call on Class
// objects.)
if (this.root != null)
throw new IllegalArgumentException("Can not copy a non-root Method");
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
// Might as well eagerly propagate this if already present
res.methodAccessor = methodAccessor;
return res;
}
}
會 new 一個 Method 實例並返回。
這裏有兩點要注意:
設置 root = this
會給 Method 設置MethodAccessor
,用於後面方法調用。也就是所有的 Method 的拷貝都會使用同一份methodAccessor
通過上面的步驟,就獲取到了需要反射的方法。
我們再回顧一下之前的流程
二、Java 反射原理--調用反射方法
獲取到方法以後,通過 Method#invoke
調用方法
class Method {
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
// 1. 檢查權限
checkAccess(caller, clazz,
Modifier.isStatic(modifiers) ? null : obj.getClass(),
modifiers);
}
// 2. 獲取 MethodAccessor
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 創建 MethodAccessor
ma = acquireMethodAccessor();
}
// 3. 調用 MethodAccessor.invoke
return ma.invoke(obj, args);
}
}
invoke 方法的實現,分爲三步:
2.1 檢查是否有權限調用方法
這裏對 override
變量進行判斷,如果 override == true
,就跳過檢查
我們通常在 Method#invoke
之前,會調用 Method#setAccessible(true)
,就是設置 override 值爲 true
2.2 獲取 MethodAccessor
在上面獲取 Method 的時候我們講到過,Method#copy 會給 Method 的 methodAccessor
賦值。所以這裏的 methodAccessor
就是拷貝時使用的 MethodAccessor
。
如果 ma 爲空,就去創建 MethodAccessor
。
class Method {
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
}
這裏會先查找 root 的 MethodAccessor
,這裏的 root 在上面 Method#copy 中設置過。
如果還是沒有找到,就去創建 MethodAccessor
。
class ReflectionFactory {
public MethodAccessor newMethodAccessor(Method method) {
// 其中會對 noInflation 進行賦值
checkInitted();
// ...
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// 生成的是 MethodAccessorImpl
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
}
這裏可以看到,一共有三種 MethodAccessor
。MethodAccessorImpl
,NativeMethodAccessorImpl
,DelegatingMethodAccessorImpl
。
採用哪種 MethodAccessor
根據 noInflation
進行判斷,noInflation
默認值爲 false,只有指定了 sun.reflect.noInflation
屬性爲 true,纔會 採用 MethodAccessorImpl
。
所以默認會調用 NativeMethodAccessorImpl
。
MethodAccessorImpl 是通過動態生成字節碼來進行方法調用的,是 Java 版本的 MethodAccessor
,字節碼生成比較複雜,這裏不放代碼了。
DelegatingMethodAccessorImpl 就是單純的代理,真正的實現還是 NativeMethodAccessorImpl
。
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
void setDelegate(MethodAccessorImpl delegate) {
this.delegate = delegate;
}
}
NativeMethodAccessorImpl
是 Native 版本的 MethodAccessor
實現。
class NativeMethodAccessorImpl extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// Java 版本的 MethodAccessor
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
// Native 版本調用
return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
在 NativeMethodAccessorImpl
的實現中,我們可以看到,有一個 numInvocations
閥值控制,numInvocations
表示調用次數。如果 numInvocations
大於 15(默認閥值是 15),那麼就使用 Java 版本的 MethodAccessorImpl
。
爲什麼採用這個策略呢,可以 JDK 中的註釋:
// "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.
//
// Package-private to be accessible to NativeMethodAccessorImpl
// and NativeConstructorAccessorImpl
private static boolean noInflation = false;
Java 版本的 MethodAccessorImpl
調用效率比 Native 版本要快 20 倍以上,但是 Java 版本加載時要比 Native 多消耗 3-4 倍資源,所以默認會調用 Native 版本,如果調用次數超過 15 次以後,就會選擇運行效率更高的 Java 版本。
那爲什麼 Native 版本運行效率會沒有 Java 版本高呢?從 R 大博客來看,是因爲 這是HotSpot
的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其內聯,於是運行時間長了之後反而是託管版本的代碼更快些。
2.3 調用 MethodAccessor#invoke 實現方法的調用
在生成 MethodAccessor
以後,就調用其 invoke 方法進行最終的反射調用。
這裏我們對 Java 版本的 MethodAccessorImpl
做個簡單的分析,Native 版本暫時不做分析。
在前面我們提到過 MethodAccessorImpl
是通過 MethodAccessorGenerator#generate
生成動態字節碼然後動態加載到 JVM 中的。
其中生成 invoke 方法字節碼的是 MethodAccessorGenerator#emitInvoke
。
我們看其中校驗參數的一小段代碼
// Iterate through incoming actual parameters, ensuring that each
// is compatible with the formal parameter type, and pushing the
// actual on the operand stack (unboxing and widening if necessary).
// num args of other invoke bytecodes
for (int i = 0; i < parameterTypes.length; i++) {
// ...
if (isPrimitive(paramType)) {
// Unboxing code.
// Put parameter into temporary local variable
// astore_3 | astore_2
// ...
// repeat for all possible widening conversions:
// aload_3 | aload_2
// instanceof <primitive boxing type>
// ifeq <next unboxing label>
// aload_3 | aload_2
// checkcast <primitive boxing type> // Note: this is "redundant",
// // but necessary for the verifier
// invokevirtual <unboxing method>
// <widening conversion bytecode, if necessary>
// goto <next parameter label>
// <next unboxing label:> ...
// last unboxing label:
// new <IllegalArgumentException>
// dup
// invokespecial <IllegalArgumentException ctor>
// athrow
}
}
通過上面的註釋以及字節碼,我們可以看到,生成的 invoke 方法,會對傳入的參數做校驗,其中會涉及到 unboxing 操作。
到此,基本上 Java 方法反射的原理就介紹完了
三、Java 反射效率低的原因
瞭解了反射的原理以後,我們來分析一下反射效率低的原因。
1. Method#invoke
方法會對參數做封裝和解封操作
我們可以看到,invoke 方法的參數是 Object[] 類型,也就是說,如果方法參數是簡單類型的話,需要在此轉化成 Object 類型,例如 long ,在 javac compile 的時候 用了Long.valueOf() 轉型,也就大量了生成了Long 的 Object, 同時 傳入的參數是Object[]數值,那還需要額外封裝object數組。
而在上面 MethodAccessorGenerator#emitInvoke
方法裏我們看到,生成的字節碼時,會把參數數組拆解開來,把參數恢復到沒有被 Object[] 包裝前的樣子,同時還要對參數做校驗,這裏就涉及到了解封操作。
因此,在反射調用的時候,因爲封裝和解封,產生了額外的不必要的內存浪費,當調用次數達到一定量的時候,還會導致 GC。
2. 需要檢查方法可見性
通過上面的源碼分析,我們會發現,反射時每次調用都必須檢查方法的可見性(在 Method.invoke 裏)
3. 需要校驗參數
反射時也必須檢查每個實際參數與形式參數的類型匹配性(在NativeMethodAccessorImpl.invoke0
裏或者生成的 Java 版 MethodAccessor.invoke
裏);
4. 反射方法難以內聯
Method#invoke
就像是個獨木橋一樣,各處的反射調用都要擠過去,在調用點上收集到的類型信息就會很亂,影響內聯程序的判斷,使得 Method.invoke()
自身難以被內聯到調用方法。
5. JIT 無法優化
在 JavaDoc 中提到:
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
因爲反射涉及到動態加載的類型,所以無法進行優化。
總結
上面就是對反射原理和反射效率低的一些分析。
關於我:
更多面試內容,面試專題,flutter視頻 全套,音視頻從0到高手開發。
關注GitHub:https://github.com/xiangjiana/Android-MS
免費獲取面試PDF合集
免費提供簡歷修改建議,獲取大廠面試PDF