正文
本文首发于掘金专栏,转载需授权
引
Java的反射技术相信大家都有所了解。作为一种从更高维度操纵代码的方式,通常被用于实现Java上的Hook技术。反射的使用方式也不难,网上查查资料,复制粘贴,基本就哦了。
举个栗子
举个简单的例子,通过反射修改
private
的成员变量值,调用
private
方法。
public class Person {
private String mName = "Hello" ;
private void sayHi () {
}
}
如上的类,有一个私有成员变量
mName
,和一个私有方法
sayHi()
。讲道理,在代码中是无法访问到他们的。但反射能做到。
Person person = new Person();
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
除了形式上略有差异,两个类之间的结构上是保持一一对应的!
使用
接着,查找到这些变量域和方法后,当然是要用它们来修改内容,调用方法啦,怎么用呢:
ContextImpl.mBasePackageName.set(context, hostPkg);
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) {
}
}
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) {
}
}
就这样,
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) {
}
}
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