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

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

Java的反射技術相信你們都有所瞭解。做爲一種從更高維度操縱代碼的方式,一般被用於實現Java上的Hook技術。反射的使用方式也不難,網上查查資料,複製粘貼,基本就哦了。android

舉個栗子

舉個簡單的例子,經過反射修改private的成員變量值,調用private方法。編程

public class Person {
    private String mName = "Hello";
    
    private void sayHi() {
        // dont care
    }
}
複製代碼

如上的類,有一個私有成員變量mName,和一個私有方法sayHi()。講道理,在代碼中是沒法訪問到他們的。但反射能作到。app

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,膠水代碼是能省則省;優化

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

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

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上本身看一遍源碼感覺下。

若是以爲筆者的文章對你有所幫助,還請給個喜歡/感謝/贊。若有紕漏,也請不吝賜教。歡迎你們留言一塊兒討論。:-)

相關文章
相關標籤/搜索