震惊!他竟然把反射用得这么优雅!

6,644 阅读13分钟

本文首发于掘金专栏,转载需授权

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

如果觉得笔者的文章对你有所帮助,还请给个喜欢/感谢/赞。如有纰漏,也请不吝赐教。欢迎大家留言一起讨论。:-)