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

本文首發于掘金專欄,轉載需授權。歡迎關注。

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上自己看一遍源碼感受下。

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,643評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,782評論 18 139
  • 整體Retrofit內容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李頭閱讀 4,608評論 2 12
  • 在看書 《牧羊少年 的嗯 奇幻 是 ...
    馨香1閱讀 255評論 0 1
  • 我和愛人相伴十余載,竟如雙生般有了心靈感應。好多次,我正給他撥打電話,電話鈴突然響了,嚇得我差點把電話摞了,還好我...
    清風徐徐霞笑江湖閱讀 408評論 4 6