本文首发于掘金专栏,转载需受权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);
复制代码
上面这种方式是很是常见的反射使用方式。但它有几个问题:框架
固然,以上提到的几点,在日常轻度使用的时候并不会以为有什么大问题。但对于一些大型且重度依赖使用反射来实现核心功能的项目,那以上几个问题,在多加剧复几回以后,就会变成噩梦通常的存在。函数
做为开发者,咱们确定但愿使用的工具,越简单易用越好,复杂的东西一来不方便理解,二来用起来不方便,三呢还容易出问题;工具
而后呢,咱们确定但愿写出来的代码能尽量的复用,Don't Repeat Yourself,胶水代码是能省则省;优化
再则呢,代码最好要直观,一眼就能看懂干了啥事,须要花时间才能理解的代码,一来影响阅读代码的效率,二来也增大了维护的成本。this
回到咱们的主题,Java里,要怎样才能优雅地使用反射呢?要想优雅,那确定是要符合上述提到的几个点的。这个问题困扰了我挺长一段时间。直到我遇到了VirtualApp
这个项目。spa
VirtualApp
是一个Android平台上的容器化/插件化解决方案。在Android平台上实现这样的方案,hook是必不可少的,所以,VirtualApp
就是这样一个重度依赖反射来实现核心功能的项目。
VirtualApp
里,有关反射的部分,作了一个基本的反射框架。这个反射框架具有有这么几个特色:
说了这么多,让咱们来看看它到底卖的什么药:
首先来看看什么是声明式:
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 |
除了形式上略有差别,两个类之间的结构上是保持一一对应的!
接着,查找到这些变量域和方法后,固然是要用它们来修改内容,调用方法啦,怎么用呢:
// 修改mBasePackageName内的值
ContextImpl.mBasePackageName.set(context, hostPkg);
// .....
// 调用getReceiverRestrictedContext方法
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) {
// 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*
中去。
挑几个有表明性的反射结构来分析。
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) {
//Ignore
}
}
复制代码
就这样,RefInt
就分析完了。这个类的实现依旧保持了一向的简洁优雅。
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) {
//Ignore
}
}
复制代码
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
}
}
复制代码
最后再分析下RefMethod
这个Method
相关的反射结构,与之相似的有RefConstructor
、RefStaticeMethod
,实现原理上也是大同小异。
和前面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());
}
}
复制代码
看起来很长的实现,其实是对三种可能的状况作了区分处理:
@MethodParams
注解声明参数的状况@MethodReflectParams
注解声明参数的状况而后照例,增长了一个便捷的调用方法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;
}
复制代码
至此,也就把几个有表明性的反射结构分析了一遍。能够看到,声明里重要的信息都是经过RefClass
内的反射结构的Field
定义提供的,反射结构在实例化的过程当中,从中取出信息,作处理。这种用法,实在高明。
笔者一开始看到这个框架,第一感受是牛逼,但又不知因此然。再进一步看的时候,又感觉到这短短的代码里的美。建议你们去Gayhub上本身看一遍源码感觉下。
若是以为笔者的文章对你有所帮助,还请给个喜欢/感谢/赞。若有纰漏,也请不吝赐教。欢迎你们留言一块儿讨论。:-)