震驚!他竟然把反射用得這麼優雅!

Geedio發表於2017-12-08

本文首發於掘金專欄,轉載需授權

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);
複製程式碼

缺點

上面這種方式是非常常見的反射使用方式。但它有幾個問題:

  1. 使用繁瑣:為了達成hook的目的(修改內容/呼叫方法),至少要三步。
  2. 存在冗餘程式碼:每hook一個變數/方法,都要把反射涉及的API寫一遍。
  3. 不夠直觀,理解程式碼所要做的事情的成本也隨之上升。

當然,以上提到的幾點,在平常輕度使用的時候並不會覺得有什麼大問題。但對於一些大型且重度依賴使用反射來實現核心功能的專案,那以上幾個問題,在多加重複幾次之後,就會變成噩夢一般的存在。

心目中的程式碼

作為開發者,我們肯定希望使用的工具,越簡單易用越好,複雜的東西一來不方便理解,二來用起來不方便,三呢還容易出問題;

然後呢,我們肯定希望寫出來的程式碼能儘可能的複用,Don't Repeat Yourself,膠水程式碼是能省則省;

再則呢,程式碼最好要直觀,一眼就能看懂幹了啥事,需要花時間才能理解的程式碼,一來影響閱讀程式碼的效率,二來也增大了維護的成本。

回到我們的主題,Java裡,要怎樣才能優雅地使用反射呢?要想優雅,那肯定是要符合上述提到的幾個點的。這個問題困擾了我挺長一段時間。直到我遇到了VirtualApp這個專案。

VirtualApp的方案

VirtualApp是一個Android平臺上的容器化/外掛化解決方案。在Android平臺上實現這樣的方案,hook是必不可少的,因此,VirtualApp就是這樣一個重度依賴反射來實現核心功能的專案。

VirtualApp裡,有關反射的部分,做了一個基本的反射框架。這個反射框架具備有這麼幾個特點:

  1. 宣告式。反射哪個類,哪個成員物件,哪個方法,都是用宣告的方式給出的。什麼是宣告?就是用類定義的方式,直截了當的定義出來。
  2. 使用簡單,沒有膠水程式碼。在宣告裡,完全看不到任何和反射API相關的程式碼,基本隱藏了Java的反射框架,對使用者來說,幾乎是無感的。
  3. 實現簡潔,原理簡單。這麼一個好用的框架,它的實現卻不復雜,原始碼不多,程式碼實現很簡單,卻很好地詮釋了什麼叫優雅。

宣告

說了這麼多,讓我們來看看它到底賣的什麼藥:

首先來看看什麼是宣告式:

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類內部的mBasePackageNamemPackageInfomPackageManagergetReceiverRestrictedContext成員和方法。

注意,這裡只有宣告的過程,沒有賦值的過程。這個過程,便完成了傳統的查詢目標類內的變數域、方法要乾的事情。從程式碼上看,相當的簡潔直觀。

下面這個表格,能更直觀形象的表現它的優雅:

反射結構型別 宣告 實際型別 實際宣告
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是最核心的類。

圍繞在它周邊的RefBooleanRefConstructorRefDoubleRefFloatRefIntRefLongRefMethodRefObjectRefStaticIntRefStaticMethodRefStaticObject則是用於宣告和使用的反射結構的定義。從名字也能直觀的看出該反射結構的型別資訊,如構造方法、資料型別、是否靜態等。

在右邊角落的兩個小傢伙MethodParamsMethodReflectParams是用於定義方法引數型別的註解,方法相關的反射結構的定義會需要用到它。它們兩個的差別在於,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)內部的邏輯。

先不放原始碼,簡單概括一下:

  1. mappingClass內部,查詢需要初始化的反射結構(如RefObject<String> mBasePackageName)
  2. 例項化查到到的反射結構變數(即做了RefObject<String> mmBasePackageName = new RefObject<String>(...))

查詢,就需要限定條件範圍。結合定義,可以知道,要查詢的反射結構,具有以下特點:

  1. 靜態成員
  2. 型別為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這種是最簡單的。依舊先不放原始碼。先思考下,對於一個這樣的放射結構,需要關心的東西有什麼?

  1. 首先是這個反射結構對映到原始類中是哪個Field
  2. 緊接著就是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,並設為可訪問的,然後作為反射結構的成員變數持有了。

為了方便使用,又新增了getset兩個方法,便於快捷的存取這個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

RefStaticIntRefInt的基礎上,加了一個限制條件:該變數是靜態變數,而非類的成員變數。熟悉反射的朋友們知道,通過反射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即可。這個差異,對應的getset方法也做了調整,不再需要傳入操作物件。原始碼如下:

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相關的反射結構,與之類似的有RefConstructorRefStaticeMethod,實現原理上也是大同小異。

和前面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());
    }
}
複製程式碼

看起來很長的實現,實際上是對三種可能的情況做了區分處理:

  1. @MethodParams註解宣告引數的情況
  2. @MethodReflectParams註解宣告引數的情況
  3. 沒有使用註解的情況,即無參的場景

然後照例,增加了一個便捷的呼叫方法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上自己看一遍原始碼感受下。

如果覺得筆者的文章對你有所幫助,還請給個喜歡/感謝/贊。如有紕漏,也請不吝賜教。歡迎大家留言一起討論。:-)

相關文章