本文首發于掘金專欄,轉載需授權。歡迎關注。
引
Java的反射技術相信大家都有所了解。作為一種從更高維度操縱代碼的方式,通常被用于實現Java上的Hook技術。反射的使用方式也不難,網上查查資料,復制粘貼,基本就哦了。
舉個栗子
舉個簡單的例子,通過反射修改private
的成員變量值,調用private
方法。
public class Person {
private String mName = "Hello";
private void sayHi() {
// dont care
}
}
如上的類,有一個私有成員變量mName
,和一個私有方法sayHi()
。講道理,在代碼中是無法訪問到他們的。但反射能做到。
Person person = new Person();
// person.mName = "world!"; // impossible
// person.sayHi(); // no way
Field fieldName = Person.class.getDeclaredField("mName");
fieldName.setAccessible(true);
fieldName.set(person, "world!");
Method methodSayHi = Person.class.getDeclaredMethod("getDeclaredMethod");
methodSayHi.setAccessible(true);
methodSayHi.invoke(person);
缺點
上面這種方式是非常常見的反射使用方式。但它有幾個問題:
- 使用繁瑣:為了達成hook的目的(修改內容/調用方法),至少要三步。
- 存在冗余代碼:每hook一個變量/方法,都要把反射涉及的API寫一遍。
- 不夠直觀,理解代碼所要做的事情的成本也隨之上升。
當然,以上提到的幾點,在平常輕度使用的時候并不會覺得有什么大問題。但對于一些大型且重度依賴使用反射來實現核心功能的項目,那以上幾個問題,在多加重復幾次之后,就會變成噩夢一般的存在。
心目中的代碼
作為開發者,我們肯定希望使用的工具,越簡單易用越好,復雜的東西一來不方便理解,二來用起來不方便,三呢還容易出問題;
然后呢,我們肯定希望寫出來的代碼能盡可能的復用,Don't Repeat Yourself,膠水代碼是能省則省;
再則呢,代碼最好要直觀,一眼就能看懂干了啥事,需要花時間才能理解的代碼,一來影響閱讀代碼的效率,二來也增大了維護的成本。
回到我們的主題,Java里,要怎樣才能優雅地使用反射呢?要想優雅,那肯定是要符合上述提到的幾個點的。這個問題困擾了我挺長一段時間。直到我遇到了VirtualApp
這個項目。
VirtualApp的方案
VirtualApp
是一個Android平臺上的容器化/插件化解決方案。在Android平臺上實現這樣的方案,hook是必不可少的,因此,VirtualApp
就是這樣一個重度依賴反射來實現核心功能的項目。
VirtualApp
里,有關反射的部分,做了一個基本的反射框架。這個反射框架具備有這么幾個特點:
- 聲明式。反射哪個類,哪個成員對象,哪個方法,都是用聲明的方式給出的。什么是聲明?就是用類定義的方式,直截了當的定義出來。
- 使用簡單,沒有膠水代碼。在聲明里,完全看不到任何和反射API相關的代碼,基本隱藏了Java的反射框架,對使用者來說,幾乎是無感的。
- 實現簡潔,原理簡單。這么一個好用的框架,它的實現卻不復雜,源碼不多,代碼實現很簡單,卻很好地詮釋了什么叫優雅。
聲明
說了這么多,讓我們來看看它到底賣的什么藥:
首先來看看什么是聲明式:
package mirror.android.app;
public class ContextImpl {
public static Class<?> TYPE = RefClass.load(ContextImpl.class, "android.app.ContextImpl");
public static RefObject<String> mBasePackageName;
public static RefObject<Object> mPackageInfo;
public static RefObject<PackageManager> mPackageManager;
@MethodParams({Context.class})
public static RefMethod<Context> getReceiverRestrictedContext;
}
上述類是VirtualApp
里對ContextImpl
類的反射的定義。從包名上看,mirror之后的部分和android源碼的包名保持一致,類名也是一致的。從這能直觀的知道,這個類對應的便是android.app.ContextImpl
類。注意,這個不是這個框架的硬性規定,而是項目作者組織代碼的結果。從這也看出作者編程的功底深厚。
public static Class<?> TYPE = RefClass.load(ContextImpl.class, "android.app.ContextImpl");
這句才是實際的初始化入口。第二個參數指定反射的操作目標類為android.app.ContextImpl
。這個是框架的硬性要求。
接下來幾個都是對要反射的變量。分別對應實際的ContextImpl
類內部的mBasePackageName
、mPackageInfo
、mPackageManager
、getReceiverRestrictedContext
成員和方法。
注意,這里只有聲明的過程,沒有賦值的過程。這個過程,便完成了傳統的查找目標類內的變量域、方法要干的事情。從代碼上看,相當的簡潔直觀。
下面這個表格,能更直觀形象的表現它的優雅:
反射結構類型 | 聲明 | 實際類型 | 實際聲明 |
---|---|---|---|
RefClass | mirror.android.app.ContextImp | Class | android.app.ContextImp |
RefObject<String> | mBasePackageName | String | mBasePackageName |
RefObject<Object> | mPackageInfo | LoadedApk | mPackageInfo |
RefObject<PackageManager> | mPackageManager | PackageManager | mPackageManager |
@MethodParams | ({Context.class}) | Params | (Context.class) |
RefMethod<Context> | getReceiverRestrictedContext | Method | getReceiverRestrictedContext |
除了形式上略有差異,兩個類之間的結構上是保持一一對應的!
使用
接著,查找到這些變量域和方法后,當然是要用它們來修改內容,調用方法啦,怎么用呢:
// 修改mBasePackageName內的值
ContextImpl.mBasePackageName.set(context, hostPkg);
// .....
// 調用getReceiverRestrictedContext方法
Context receiverContext = ContextImpl.getReceiverRestrictedContext.call(context);
用起來是不是也相當直觀?一行代碼,就能看出要做什么事情。比起最開始提及的那種方式,這種方式簡直清晰簡潔得不要不要的,一鼓作氣讀下來不帶停頓的。這樣的代碼幾乎沒有廢話,每一行都有意義,信息密度杠杠的。
到這里就講完了聲明和使用這兩個步驟。確實很簡單吧?接下來再來看看實現。
實現分析
結構
首先看看這個框架的類圖:
擺在中間的RefClass
是最核心的類。
圍繞在它周邊的RefBoolean
、RefConstructor
、RefDouble
、RefFloat
、RefInt
、RefLong
、RefMethod
、RefObject
、RefStaticInt
、RefStaticMethod
、RefStaticObject
則是用于聲明和使用的反射結構的定義。從名字也能直觀的看出該反射結構的類型信息,如構造方法、數據類型、是否靜態等。
在右邊角落的兩個小家伙MethodParams
、MethodReflectParams
是用于定義方法參數類型的注解,方法相關的反射結構的定義會需要用到它。它們兩個的差別在于,MethodParams
接受的數據類型是Class<?>
,而MethodReflectParams
接受的數據類型是字符串,對應類型的全描述符,如android.app.Context
,這個主要是服務于那些Android SDK沒有暴露出來的,無法直接訪問到的類。
運作
初始化
從上面的表格可以知道,RefClass
是整個聲明中最外層的結構。這整個結構要能運作,也需要從這里開始,逐層向里地初始化。上文也提到了,RefClass.load(Class mappingClass, Class<?> realClass)
是初始化的入口。初始化的時機呢?我們知道,Java虛擬機在加載類的時候,會初始化靜態變量,定義里的TYPE = RefClass.laod(...)
就是在這個時候執行的。也就是說,當我們需要用到它的時候,它才會被加載,通過這種方式,框架具備了按需加載的特性,沒有多余的代碼。
入口知道了,我們來看看RefClass.load(Class<?> mappingClass, Class<?> realClass)
內部的邏輯。
先不放源碼,簡單概括一下:
- 在
mappingClass
內部,查找需要初始化的反射結構(如RefObject<String> mBasePackageName
) - 實例化查到到的反射結構變量(即做了
RefObject<String> mmBasePackageName = new RefObject<String>(...)
)
查找,就需要限定條件范圍。結合定義,可以知道,要查找的反射結構,具有以下特點:
- 靜態成員
- 類型為
Ref*
查找的代碼如下:
public static Class load(Class mappingClass, Class<?> realClass) {
// 遍歷一遍內部定義的成員
Field[] fields = mappingClass.getDeclaredFields();
for (Field field : fields) {
try {
// 如果是靜態類型
if (Modifier.isStatic(field.getModifiers())) {
// 且是反射結構
Constructor<?> constructor = REF_TYPES.get(field.getType());
if (constructor != null) {
// 實例化該成員
field.set(null, constructor.newInstance(realClass, field));
}
}
}
catch (Exception e) {
// Ignore
}
}
return realClass;
}
這其實就是整個RefClass.laod(...)
的實現了。可以看到,實例化的過程僅僅是簡單的調用構造函數實例化對象,然后用反射的方式賦值給該變量。
REF_TYPES
是一個Map
,里面注冊了所有的反射結構(Ref*
)。源碼如下:
private static HashMap<Class<?>,Constructor<?>> REF_TYPES = new HashMap<Class<?>, Constructor<?>>();
static {
try {
REF_TYPES.put(RefObject.class, RefObject.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefMethod.class, RefMethod.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefInt.class, RefInt.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefLong.class, RefLong.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefFloat.class, RefFloat.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefDouble.class, RefDouble.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefBoolean.class, RefBoolean.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefStaticObject.class, RefStaticObject.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefStaticInt.class, RefStaticInt.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefStaticMethod.class, RefStaticMethod.class.getConstructor(Class.class, Field.class));
REF_TYPES.put(RefConstructor.class, RefConstructor.class.getConstructor(Class.class, Field.class));
}
catch (Exception e) {
e.printStackTrace();
}
}
發現沒有?在RefClass.laod(...)
里,實例化的過程簡單到不可思議?因為每個反射結構代表的含義都不一樣,初始化時要做的操作也各有不同。與其將這些不同都防止load的函數里,還不如將對應的邏輯分解到構造函數里更合適。這樣既降低了RefClass.laod(...)
實現的復雜度,保持了簡潔,也將特異代碼內聚到了對應的反射結構Ref*
中去。
反射結構定義
挑幾個有代表性的反射結構來分析。
1. RefInt
RefInt
這種是最簡單的。依舊先不放源碼。先思考下,對于一個這樣的放射結構,需要關心的東西有什么?
- 首先是這個反射結構映射到原始類中是哪個
Field
- 緊接著就是
Field
的類型是什么。
上文表格里可以看到,反射結構的名稱和實際類中對應的Field
的名稱的一一對應的。我們只要拿到反射結構的名稱就可以了。第二點,Field
的類型,由于RefInt
直接對應到了int
類型,所以這個是直接可知的信息。
public RefInt(Class cls, Field field) throws NoSuchFieldException {
this.field = cls.getDeclaredField(field.getName());
this.field.setAccessible(true);
}
源碼里也是這么做的,從反射結構的Field
里,取得反射結構定義時的名字,用這個名字去真正的類里,查找到對應的Field
,并設為可訪問的,然后作為反射結構的成員變量持有了。
為了方便使用,又新增了get
、set
兩個方法,便于快捷的存取這個Field
內的值。如下:
public int get(Object object) {
try {
return this.field.getInt(object);
} catch (Exception e) {
return 0;
}
}
public void set(Object obj, int intValue) {
try {
this.field.setInt(obj, intValue);
} catch (Exception e) {
//Ignore
}
}
就這樣,RefInt
就分析完了。這個類的實現依舊保持了一貫的簡潔優雅。
2. RefStaticInt
RefStaticInt
在RefInt
的基礎上,加了一個限制條件:該變量是靜態變量,而非類的成員變量。熟悉反射的朋友們知道,通過反射Field
是沒有區分靜態還是非靜態的,都是調用Class.getDeclaredField(fieldName)
方法。所以這個類的構造函數跟RefInt
是一毛一樣毫無差別的。
public RefStaticInt(Class<?> cls, Field field) throws NoSuchFieldException {
this.field = cls.getDeclaredField(field.getName());
this.field.setAccessible(true);
}
當然,熟悉反射的朋友也知道,一個Field
是否靜態是能夠根據Modifier.isStatic(field.getModifiers())
來判定的。這里若是為了嚴格要求查找到的Feild
一定是static field
的話,可以加上這個限制優化下。
靜態變量和成員變量在通過反射進行數據存取則是有差異的。成員變量的Field
需要傳入目標對象,而靜態變量的Field
不需要,傳null
即可。這個差異,對應的get
、set
方法也做了調整,不再需要傳入操作對象。源碼如下:
public int get() {
try {
return this.field.getInt(null);
} catch (Exception e) {
return 0;
}
}
public void set(int value) {
try {
this.field.setInt(null, value);
} catch (Exception e) {
//Ignore
}
}
3.RefObject<T>
RefObject<T>
跟RefInt
相比,理解起來復雜了一點:Field
的數據類型由泛型的<T>
提供。但實際上,和RefStaticInt
一樣,構造函數類并沒有做嚴格的校驗,即運行時不會在構造函數檢查實際的類型和泛型里的期望類型是否一致。所以,構造函數依舊沒什么變化。
public RefObject(Class<?> cls, Field field) throws NoSuchFieldException {
this.field = cls.getDeclaredField(field.getName());
this.field.setAccessible(true);
}
實際上,要做嚴格檢查也依舊是可以的。我猜想,我猜想作者之所以沒有加嚴格的檢查,一是為了保持實現的簡單,二是這種錯誤,屬于定義的時候的錯誤,即寫出了bug,那么在接下來的使用中一樣會報錯,屬于開發過程中必然會發現的bug,因此實現上做嚴格的校驗意義不大。
泛型<T>
的作用在于數據存取的時候,做相應的類型規范和轉換。源碼如下:
public T get(Object object) {
try {
return (T) this.field.get(object);
} catch (Exception e) {
return null;
}
}
public void set(Object obj, T value) {
try {
this.field.set(obj, value);
} catch (Exception e) {
//Ignore
}
}
4. RefMethod<ReturnType>和@MethodParams
最后再分析下RefMethod
這個Method
相關的反射結構,與之類似的有RefConstructor
、RefStaticeMethod
,實現原理上也是大同小異。
和前面Field
相關的反射結構不同,Method
的反射結構確實稍微復雜了一丟丟。RefMethod
對應的是方法,對方法來說,它有方法名、返回值、參數這三個信息要關心。
前面分析可知,變量名信息是通過反射結構定義的名字來確定的,方法名也一樣,通過反射結構的Field
就能獲取到。
返回值呢?所有的Method.invoke(...)
都有一個返回值,和RefObject<T>
一樣,類型信息通過泛型提供,在使用的時候,僅僅做了轉義。
參數這個信息,則是Method.invoke(...)
調用里必不可少的參數。VirtualApp
通過給RefMethod
定義加注解創造性地解決了這個問題,即實現了聲明式,也保證了實現的簡單優雅。理解這段代碼不難,但這個用法確實很新穎。
看下構造方法的源碼:
public RefMethod(Class<?> cls, Field field) throws NoSuchMethodException {
if (field.isAnnotationPresent(MethodParams.class)) {
Class<?>[] types = field.getAnnotation(MethodParams.class).value();
for (int i = 0; i < types.length; i++) {
Class<?> clazz = types[i];
if (clazz.getClassLoader() == getClass().getClassLoader()) {
try {
Class.forName(clazz.getName());
Class<?> realClass = (Class<?>) clazz.getField("TYPE").get(null);
types[i] = realClass;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
this.method = cls.getDeclaredMethod(field.getName(), types);
this.method.setAccessible(true);
} else if (field.isAnnotationPresent(MethodReflectParams.class)) {
String[] typeNames = field.getAnnotation(MethodReflectParams.class).value();
Class<?>[] types = new Class<?>[typeNames.length];
for (int i = 0; i < typeNames.length; i++) {
Class<?> type = getProtoType(typeNames[i]);
if (type == null) {
try {
type = Class.forName(typeNames[i]);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
types[i] = type;
}
this.method = cls.getDeclaredMethod(field.getName(), types);
this.method.setAccessible(true);
}
else {
for (Method method : cls.getDeclaredMethods()) {
if (method.getName().equals(field.getName())) {
this.method = method;
this.method.setAccessible(true);
break;
}
}
}
if (this.method == null) {
throw new NoSuchMethodException(field.getName());
}
}
看起來很長的實現,實際上是對三種可能的情況做了區分處理:
-
@MethodParams
注解聲明參數的情況 -
@MethodReflectParams
注解聲明參數的情況 - 沒有使用注解的情況,即無參的場景
然后照例,增加了一個便捷的調用方法call(Object receiver, Object... args)
。同樣的,這里也沒過多的校驗,直接透傳給實際的Method
實例。看下代碼:
public T call(Object receiver, Object... args) {
try {
return (T) this.method.invoke(receiver, args);
} catch (InvocationTargetException e) {
if (e.getCause() != null) {
e.getCause().printStackTrace();
} else {
e.printStackTrace();
}
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
5. 小結
至此,也就把幾個有代表性的反射結構分析了一遍。可以看到,聲明里重要的信息都是通過RefClass
內的反射結構的Field
定義提供的,反射結構在實例化的過程中,從中取出信息,做處理。這種用法,實在高明。
結
筆者一開始看到這個框架,第一感覺是牛逼,但又不知所以然。再進一步看的時候,又感受到這短短的代碼里的美。建議大家去Gayhub上自己看一遍源碼感受下。
如果覺得筆者的文章對你有所幫助,還請給個喜歡/感謝/贊。如有紕漏,也請不吝賜教。歡迎大家留言一起討論。:-)