反射的原理到使用

2,870 阅读4分钟

引子

众所周知,java有三大特性: 封装继承和多态,封装是为了细化权限,继承是为了多态,多态是为了灵活复用。我们又知道,继承破坏了封装,但是,它破坏的不彻底,有没有破坏的更彻底的呢?有,反射! 反射效率很低,因为它工作在运行时,为什么工作在运行时呢?我们先来看个例子:

public class User {
    // 创建一个私有字段name
    private String name;
    public User(String name) {
        this.name = name;
    }
}

public void test(){
    // 创建一个User对象,name为:java
    User user = new User("java");
    // 反射的工作目标是.class对象
    Class aclass = User.class;
    // 获取声明的字段:name
    Field nameField = aclass.getDeclaredField("name");
    // 允许访问非public的字段
    nameField.setAccessible(true);
    // 获取此字段在user对象上的值
    Object nameValue = nameField.get(user);
    // 打印
    System.out.println(nameValue);
}

打印结果:

java
Process finished with exit code 0

这个例子简单的演示了反射的使用,其中只有一点需要我们记得: 反射的工作目标是.class对象,也就是说,如果把反射比作一个函数,那么它的入参是.class对象,那么这个.class对象从哪里来呢?答曰: 类加载!

类加载机制

我们之前在JVM类加载机制里面提到过类加载的一些知识,这里再来看一下。JVM类加载流程:

  • 1 通过全限定类名来获取定义此类的二进制字节流。
  • 2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

简而言之,就是: JVM类加载机制 接收的是: .class文件的二进制字节流,产出的是: java.lang.Class对象。

等等!我们上面反射的例子,不就是针对Class对象工作的吗,所以可以说: 类加载机制产出了反射可以进行工作的目标,也就是Class对象,经过了类加载机制,反射才有了入参(Class对象),才可以工作,而类加载工作在运行时,反射工作在类加载之后,所以: 反射工作在运行时!,这就回答了刚刚的问题。 那么,类加载为什么工作在运行时,java不是一门强类型静态语言吗? 语言划分 是,java是静态语言,但是它有动态特性,就是为了支持比如多态、JSP等技术引入的,比如:

A a = new B();

其中B是A的子类,这就是多态,只有在运行时,才知道它是什么类型,这就是java的动态特性,所以JVM的类加载是在运行时,这虽然一定程度上降低了效率,但是带来的作用是巨大的,这也是反射为什么工作在运行时,因为它需要的Class对象要在运行时才有。

那么既然JVM类加载机制 接收.class文件,产出java.lang.Class对象,那么它接收的.class文件从哪来呢?有人已经想到了,就是javac!也就是编译!

编译过程

我们知道,写一个Hello.java文件,然后运行指令:javac Hello.java,就会生成一个Hello.class文件,这个文件是个二进制流,正是JVM类加载需要的东西! javac代表了编译过程,运行了这个指令,就会找到你设置的环境变量里面的相关程序,去进行编译,大家也可以直接在:com.sun.tools.javac.main.JavaCompiler找相关代码,看一下这个.class文件是怎么生成的,大概三个步骤:

  • 1 解析与填充符号表,此阶段执行词法分析,语法分析,生成抽象语法树等。
  • 2 注解处理,此阶段着重处理注解,也是apt的工作阶段,比如arouter。
  • 3 分析与字节码生成,此阶段执行常量折叠,解语法糖等操作。 编译过程 可以看到,如果在注解处理过程中,有新文件生成,则会跳转到第一步继续"解析与填充符号表",直到不再生成java文件为止。这也是arouter的工作原理: 通过注解处理器和代码模板,在编译时动态生成文件,我们使用arouter时候,发现的一些ARouter$$Group$$app、ARouter$$Providers$$app等文件就是这么来的。

在编译阶段之后,我们就得到了.class文件,也就是jvm加载需要的东西!大家可以通过下面几个步骤查看.class文件的内容:

  • 1 在终端执行命令:vi Hello.class,打开.class文件,看到的是乱码
  • 2 执行命令: %!xxd,转换成二进制,即可查看相关信息
  • 3 执行命令: %!xxd -r,还原,然后输入:q!退出即可 当然也可以直接执行: javap -verbose Hello.class查看。

编译阶段接收.java文件,产出.class文件。

好,我们来小结一下:

  • 1 我们编写.java文件,输入相关代码
  • 2 经过javac编译,通过.java文件生成了.class文件
  • 3 经过JVM加载,通过.class文件生成了java.lang.Class对象
  • 4 我们使用这些Class对象执行各种操作,比如反射 知道了反射需要的java.lang.Class文件的来源,我们来看下它的API

反射常用API

Class对象的获取:

  • 有对象,使用object.getClass(); eg:
Class<? extends User> aClass = user.getClass();
  • 有类,使用Class.class; eg:
Class aclass = User.class;
Class<Void> voidClass = void.class; //void 也有class对象

值得一提的是,void也有class对象,基本类型(int,short)等没有getClass()方法,但有.class对象,它们对应的包装类有getClass()方法。

  • 知道类名,使用Class.forName(name)或ClassLoader.loadClass(name); eg:
try {
    // 加载类并初始化
    Class<?> aClass = Class.forName("com.company.bean.User");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
try {
    // 只加载但是不进行初始化
    ClassLoader.getSystemClassLoader().loadClass("com.company.bean.User");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

记得处理ClassNotFoundException异常,如果找不到对应的类就会报这个异常。Class.forName(name)除了加载对应的类,还会对类进行初始化,也就是会执行<clinit>()函数,表现在java层就是static块以及静态赋值语句会执行,而ClassLoader.loadClass(name)则只对类进行加载,不做其他任何多余的动作。 获得了Class对象后,我们就可以执行反射的各种API。Class.forName()方法还有两个参数的变体,大家自行查看文档即可。

  • 1 名称信息
public static void getName() throws ClassNotFoundException {
    Class<?> aClass = Class.forName("java.lang.String");
    // 获取包名
    println(aClass.getPackage().getName());
    // 获取java内部使用的真实名
    println(aClass.getName());
    // 获取简单名字,不带包名
    println(aClass.getSimpleName());
    // 获取全名
    println(aClass.getCanonicalName());
}

结果如下:

java.lang
java.lang.String
String
java.lang.String

这里有一点需要注意: getName()返回的是java内部的真实类型,一般都跟getCanonicalName()一样,但是数组就例外了,大家可以自己试试。

  • 2 字段信息
public static void getFields() throws Exception {
    Class<?> aClass = Class.forName("java.util.ArrayList");
    // 获取本类或父类的指定的public字段
    Field field = aClass.getField("size");
    // 获取本类或父类的所有public字段
    Field[] fields = aClass.getFields();
    // 获取本类的指定字段,包括非public的,但是不包括父类的
    aClass.getDeclaredField("DEFAULT_CAPACITY");
    // 获取本类的所有字段,包括非public的,但是不包括父类的
    aClass.getDeclaredFields();

    // 获取字段的名字
    field.getName();
    // 返回该字段是否可读写
    field.isAccessible();
    // 使得非public的字段也可以读写
    field.setAccessible(true);
    ArrayList<String> list = new ArrayList<>();
    // 返回该字段在list对象上的值,如果是static字段,则传null即可
    field.get(list);
    // 将该字段在list上的值设置为100
    field.set(list, 100);

    // 获取该属性上的所有注解
    Annotation[] annotations = field.getAnnotations();
    // 返回该字段的修饰符,这是一个复合值,类似于Android的MeasureSpec
    int modifiers = field.getModifiers();
    // 是否是public的
    Modifier.isPublic(modifiers);
    // 是否是final的
    Modifier.isFinal(modifiers);
    // ... 其他api
    // 返回该字段的类型,比如String name;返回就是java.lang.String
    Class<?> type = field.getType();
    // ... 其他api
}

这里的api比较多,就不一一列举了,自行查找官方API即可

  • 3 方法信息
public static void getMethod() throws Exception {
    Class<?> aClass = Class.forName("java.util.ArrayList");
    // 获取指定的public方法,包括父类的,名字为size,没有参数,
    Method method = aClass.getMethod("size", null);
    ArrayList<String> list = new ArrayList<>();
    // 在list上执行该方法,没有参数
    method.invoke(list,null);

    // 获取指定的方法,包括非public的,但是不包括父类的
    aClass.getDeclaredMethod("size",null);
    // 获取所有的public方法,包括父类的
    aClass.getMethods();
    // 获取所有的方法,包括非public的,但是不包括父类的
    aClass.getDeclaredMethods();
}

method和field的属性差不多,只不过有一个invoke(Object obj, Object... args)方法,表示执行该方法,第一个参数是方法的调用对象,第二个参数是方法接收的参数。

这里有个技巧,getXXX()返回的就是本类和父类的所有public的,我们简称为"父子共有";getDeclaredXXX()返回的就是仅限本类的,但是包括非public的,我们简称为"自己所有"。

  • 4 构造器信息
public static void getConstructor() throws Exception {
    Class<?> aClass = Class.forName("java.util.ArrayList");
    // Class对象的创建实例的方法
    aClass.newInstance();
    // 所有构造器,父子共有
    aClass.getConstructors();
    // 所有构造器,自己所有
    aClass.getDeclaredMethods();
    // 指定参数的构造器,父子共有
    aClass.getConstructor(int.class);
    // 指定参数的构造器,自己所有
    Constructor<?> constructor = aClass.getDeclaredConstructor(int.class);
    // 构造器创建实例的方法,指定参数为100
    constructor.newInstance(100);
}
  • 5 类型信息
public static void getClassInfo() throws ClassNotFoundException {
    Class<?> aClass = Class.forName("java.util.ArrayList");
    // 获取所有的接口,父子共有
    aClass.getInterfaces();
    // 获取所有的注解,父子共有
    aClass.getAnnotations();
    // 获取父类
    aClass.getSuperclass();
    // 是否是数组
    aClass.isArray();
    // 是否是接口
    aClass.isInterface();
    // 是否是枚举
    aClass.isEnum();
    // 是否是枚举
    aClass.isAnnotation();
    // 是否是成员内部类
    aClass.isMemberClass();
    // 是否是匿名内部类
    aClass.isAnonymousClass();
    // 是否是局部内部类
    aClass.isLocalClass();
}
  • 6 数组和枚举
public static void getArrayAndEnum() {
    int[] arr = new int[10];
    Class<? extends int[]> aClass = arr.getClass();
    // 获取数组的元素类型
    Class<?> componentType = aClass.getComponentType();
    // 创建数组对象,第一个参数为元素类型,第二个参数为数组长度
    // 这里的Array对象是:java.lang.reflect.Array
    int[] a = (int[]) java.lang.reflect.Array.newInstance(int.class, 10);
    // 获取数组a中下标为5的元素
    Object o = Array.get(a, 5);
    // 设置数组a中下标为5的元素为100
    Array.set(a, 5, 100);
    // 获取数组a的长度
    Array.getLength(a);


    Class<Enum> enumClass = Enum.class;
    // 获取所有的枚举常量
    enumClass.getEnumConstants();
}
  • 7 泛型
public static void getGeneral() {
    Class aClass = ArrayList.class;
    // Class的范型参数
    TypeVariable[] typeParameters = aClass.getTypeParameters();
    Field field = null;
    // field的范型类型
    Type genericType = field.getGenericType();
    Method method = null;
    // method的范型参数
    method.getGenericParameterTypes();
    // method的范型返回值
    method.getGenericReturnType();
    // method的范型异常
    method.getExceptionTypes();
    Constructor constructor = null;
    // 构造器的范型参数
    constructor.getGenericParameterTypes();
    // 构造器的范型异常
    constructor.getGenericExceptionTypes();
}

这里有人会问,java不是有泛型擦除吗,既然都擦除了,怎么还有泛型?没错,java的泛型在编译期就擦除了,但是擦除的同时,会在.class文件的属性表(attribute_info)里面写入一个Signature信息,这里面存放了泛型的相关信息,我们获取的泛型信息就是从这个属性里面获取的,所以java的泛型称为"伪泛型"。我们来验证一下,随便写一个泛型类:

public class Hello<T> {
}

然后javac Hello.java得到Hello.class对象,再执行javap -verbose Hello.class查看: 添加泛型

可以看到,常量池(Constant Pool)里面的第11行有个Signature属性,第12行就是它的展开信息,这里不过多描述,我们再去掉范型信息看下:

public class Hello {
}

移除泛型

可以看到,里面的Signature属性没有了,也就是说,javac在编译的时候,擦除了泛型,然后把相关信息保存在常量池的Signature里面去了。

反射的使用-插件化

我们知道,Activity的启动是通过ActivityThread里面的一个叫做mH的Handler驱动的,接下来我们就来替换一下这个玩意,达到Hook的目的:

  • 1 先来定义一个Handler的Callback:
private static class EvilCallback implements Handler.Callback {
    Handler mBase;

    public EvilCallback(Handler mBase) {
        this.mBase = mBase;
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        // 这里打印一句话
        Log.e("HOOK", "拦截消息: " + msg);
        mBase.handleMessage(msg);
        return true;
    }
}

这是个包装类,接收一个Handler,在handleMessage里main打印一句话。

  • 2 然后我们来偷梁换柱,把ActivityThread里面的mH里面的mCallback替换为我们第一步创建的那个:
/**
* Hook ActivityThread的mH的mCallback
*/
protected void hookHCallback() {
    //获取ActivityThread类里面的sCurrentActivityThread对象,也就是当前使用的ActivityThread对象
    Object sCurrentActivityThread = LReflect.getField("android.app.ActivityThread", null, "sCurrentActivityThread");
    //获取ActivityThread类里面的mH成员变量
    Handler mH = (Handler) LReflect.getField("android.app.ActivityThread", sCurrentActivityThread, "mH");
    //替换sCurrentActivityThread里连的mH的成员变量mCallback为我们第一步声明的那个
    LReflect.setField(Handler.class, mH, "mCallback", new EvilCallback(mH));
}

//工具类LReflect
class LReflect {
    // 获取属性值
    public static Object getField(String className, Object obj, String fieldName) {
        try {
            Class claz = Class.forName(className);
            Field field = claz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    // 设置属性值
    public static void setField(Class claz, Object obj, String fieldName, Object filedValue) {
        try {
            Field field = claz.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, filedValue);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 3 在Activity的onCreate()里面进行hook:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    hookHCallback();
}

然后运行Activity查看结果:

E/BaseActivity: 拦截消息: { id=46 when=-1ms what=159 obj=android.app.servertransaction.ClientTransaction@4962 target=android.app.ActivityThread$H }
W/ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@7daf006
E/BaseActivity: 拦截消息: { id=47 when=0 what=159 obj=android.app.servertransaction.ClientTransaction@fb13e219 target=android.app.ActivityThread$H }
    
    --------- beginning of system
I/PhoneWindow: initSystemUIColor
E/BaseActivity: 拦截消息: { id=48 when=-87ms what=159 obj=android.app.servertransaction.ClientTransaction@8382 target=android.app.ActivityThread$H }
E/BaseActivity: 拦截消息: { id=78 when=0 what=149 obj=android.os.BinderProxy@7daf006 target=android.app.ActivityThread$H }
E/BaseActivity: 拦截消息: { id=82 when=-5ms what=159 obj=android.app.servertransaction.ClientTransaction@8363 target=android.app.ActivityThread$H }
E/BaseActivity: 拦截消息: { id=83 when=-6ms what=159 obj=android.app.servertransaction.ClientTransaction@f043e0 target=android.app.ActivityThread$H }
E/BaseActivity: 拦截消息: { id=88 when=-23ms what=159 obj=android.app.servertransaction.ClientTransaction@7fe0 target=android.app.ActivityThread$H }
E/BaseActivity: 拦截消息: { id=90 when=-1ms what=119 obj=android.app.ActivityThread$ContextCleanupInfo@9540597 target=android.app.ActivityThread$H }

日志是 启动Activity,然后按下回退键 的结果。可以看到,我们的hook已经成功了,你可以在这里作各种骚操作,有兴趣的可以根据消息里面的what搜一下源码,看看它是干什么的,这里不再赘述。

反射很强大,不依赖于具体的类,能极大的减轻耦合,但是因为是在运行时生效,所以影响效率,所以我们一般能使用接口就使用接口,不能使用接口可以考虑使用apt技术(编译期),总之,不到万不得已,不轻易使用反射。