相信每一位接触过Java的同学对于反射都不陌生。作为一种从更高维度操纵代码的方式,反射通常被用于实现Java上的Hook技术。
然而反射的使用方式也不难,随便一Google 就有一堆文章。所以本文将不再累述反射的Api与原理等概念,而是从使用者的角度出发,站在Coder的角度上去观察如何优雅的写出反射代码。
常见的反射写法
public class ReflexDemo {
private int mId = 0xff;
private static final String TAG = "Reflex";
private void doSomething(){
printLog(TAG,"I will do something!");
}
private static void printLog(String tag,String message){
Log.i(tag,message);
}
}
如上面代码中的ReflexDemo,包含了私有变量、私有静态变量、私有函数、私有静态函数。如果我们想访问他们就必须通过反射调用,所以有了如下代码。
try {
ReflexDemo reflexDemo = new ReflexDemo();
Field fieldName = ReflexDemo.class.getDeclaredField("mId");
fieldName.setAccessible(true);
fieldName.set(reflexDemo, 0xff >> 2);
Method doSomething = ReflexDemo.class.getDeclaredMethod("doSomething");
doSomething.setAccessible(true);
doSomething.invoke(reflexDemo);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
简单观测我们可以发现普通的反射写法存在以下缺点:
- 上面的代码就是我们经常使用反射时的写法。很显然为了达到Hook的目的 我们需要先获取对应的变量/函数,再调用setAccessible修改其是否禁用安全检查,最后才能达到我们的目的修改内容/调用函数。这样使用起来会比较繁琐。
- 每调用一个变量或函数都需要将涉及到的反射接口写一遍。
虽然以上两点在少量使用时并不会让人觉得有什么问题,但对于一些大型项目且比较依赖使用反射来实现核心功能的项目,以上两点就会成为噩梦般的存在。
目标
想要优化使用反射的过程,需要遵循以下几个原则:
- 越简单越好,在实现功能的基础上代码应该更容易让人阅读,复杂的东西不容易理解且不利于使用。
- 尽可能的复用,不要重复性的写无意义的代码。
- 轻量快捷,使用起来最好让人一目了然。
声明式反射
什么是声明式反射?相信这是大家的第一反应,那么我们先来看一下声明式反射的使用方法。
class ReflexDemoMapping {
public static ReflexInt mId;
public static ReflexStaticObject<String> TAG;
public static ReflexMethod<Void> doSomething;
@MethodParams(value = {String.class, String.class})
public static ReflexStaticMethod<Void> printLog;
static {
ReflexClass.load(ReflexDemoMapping.class, ReflexDemo.class);
}
}
ReflexDemoMapping就是ReflexDemo的反射声明类。为了更直观的观察两者间的映射关系我们看下表:
声明 | 声明类型 | 映射类型 | 映射对象 |
---|---|---|---|
mId | ReflexInt | int | ReflexDemo.mId |
TAG | ReflexStaticObject | String | ReflexDemo.TAG |
doSomething | ReflexMethod | Method | ReflexDemo.doSomething() |
printLog | ReflexStaticMethod | Method | ReflexDemo.printLog() |
声明式:用类定义的方式,直截了当的定义出来反射哪个类、哪个成员对象、哪个函数。只关注定义,不关注实现细节与赋值过程。
对比一下我们可以发现 ReflexDemoMapping的每一个成员变量与要反射调用的变量或函数名一一对应,这样做的好处就是再后期维护时可以更加简单快捷的还原意图,降低维护成本。
接下来请看静态块中的内容,我们知道JVM在类的初始化阶段会完成对静态变量的初始化,静态块执行等工作,所以保证了反射声明是按需加载的。而静态块中执行的ReflexClass.load(Class mapping,Class real)函数就是Reflex框架的真正初始化入口。
使用起来也是非常的简洁明了:
ReflexDemo reflexDemo = new ReflexDemo();
ReflexDemoMapping.mId.set(reflexDemo, 99999);
ReflexDemoMapping.doSomething.call(reflexDemo);
ReflexDemoMapping.printLog.call(ReflexDemoMapping.TAG.get(), "Id is :" + ReflexDemoMapping.mId.get(reflexDemo));
}
至此相信大家已经对声明式反射有了一些了解,相比传统的反射调用,声明式的使用更加简单,在明确了意图的同时也减少了胶水代码。在声明里,完全看不到任何和反射API相关的代码,基本隐藏了Java的反射框架,对使用者来说,几乎是无感的。
Reflex
Reflex 提供了Java的八大基础类型用于反射声明,分别为ReflexByte
、ReflexShort
、ReflexInt
、ReflexLong
、ReflexFloat
、ReflexDouble
、ReflexBoolean
、ReflexChar
,并提供了ReflexObject
用于对其它类型的支持。
同时还提供了用于反射静态变量的声明类型ReflexStaticByte
、ReflexStaticShort
、ReflexStaticInt
、ReflexStaticLong
、ReflexStaticFloat
、ReflexStaticDouble
、ReflexStaticBoolean
、ReflexStaticChar
、ReflexStaticObject
。
用于声明反射函数的 ReflexMethod
、 ReflexStaticMethod
、ReflexConstructor
。
用于标注函数参数的 MethodParams
、MethodReflexParams
。
ReflexClass
作为整个框架的入口,其承担了构造反射类与初始化映射表的重担。并对外提供了load(Class<?> mappingClass, String realClassName) 与 load(Class<?> mappingClass, Class<?> realClass) 两个函数。mappingClass
为反射映射表,realClass
为反射操作的目标类,可以传入Class
或PackageName
+ClassName
。
框架要求MappingClass
反射映射表中的变量必须为静态变量且变量名要与要反射的变量或函数一致。
引入方式
Gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
并在需要引入的Module 中添加:
dependencies {
implementation 'com.github.CoderAlee:Reflex:1.0.0'
}
Maven
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
添加依赖:
<dependency>
<groupId>com.github.CoderAlee</groupId>
<artifactId>Reflex</artifactId>
<version>1.0.0</version>
</dependency>
框架结构及原理
传送门:如何优雅的使用反射(二)