java 反射初步理解

前言

  之前整理了java同步的相关内容,现在开始整理java反射,都属于java相关内容。在查找资料的过程中,找到两篇个人认为很不错的介绍及应用:

  Java反射以及在Android中的特殊应用

  Java 技术之反射

什么是反射?

  反射,简单来讲,是一种与类动态交互的机制。为什么是动态交互的?一般来讲,在一个类已经写好且不能再修改的前提下,我们只能调用其暴露出来的方法或属性,而有时候我们又有扩展其功能或使用其隐藏功能的需求,这时候我们就可以在程序运行状态下,获取该类的相关信息并操作其信息以满足我们的需求。这种在写代码时仍旧保持该类不修改不改变而在程序运行时扩展其功能或使用其隐藏功能的机制,就是一种动态交互的机制。相对于编译阶段类不做改变(静态参照物),运行时的改变就是动态的了。

使用反射获取一个类所有的变量和方法

  反射可以获取字节码文件内的信息,下面的代码是获取信息的示例,主要作用是为了熟悉获取字段、构造方法、普通方法的API。

  获取字节码内所有已声明的字段:

StringBuilder fieldBuffer = new StringBuilder();
//getDeclaredFields即为获取字节码内所有已声明字段的API
Field[] fields = clazz.getDeclaredFields();
//遍历所有的字段,获取字段相关信息
for (Field field : fields) {

    //字段名称
    String name = field.getName();

    //这两句是获取字段的修饰符的,并通过Modifier此类转换成了字符串
    int modifiers = field.getModifiers();
    String modifiersStr = Modifier.toString(modifiers);

    //getType是获取字段的类型(Class),getSimpleName为其简化名称
    String typeName = field.getType().getSimpleName();
    
    //组装
    fieldBuffer.append(modifiersStr + " " + typeName + " " + name + ";" + "\n\n");
}
bundle.putSerializable("fileds", fieldBuffer.toString());

  获取字节码内所有的构造方法:

//getDeclaredConstructors:获取字节码内所有已声明的构造方法
Constructor[] constructors = clazz.getDeclaredConstructors();
StringBuilder constructorBuilder = new StringBuilder();
//逐一遍历每个构造方法,并获取其相关信息
for (Constructor constructor : constructors) {

    //获取构造方法的修饰符,并转换成字符串
    int modifiers = constructor.getModifiers();
    String modifiersStr = Modifier.toString(modifiers);
    constructorBuilder.append(modifiersStr);

    //这里如果使用constructor.getName获取构造方法名,获取的不是简化的形式
    //所以,这里直接使用类的简化名
    String name = clazz.getSimpleName();
    constructorBuilder.append(" " + name);

    //getParameterTypes:获取构造方法的所有参数的类型,以数组形式返回
    Class[] parameterTypes = constructor.getParameterTypes();
    constructorBuilder.append("(");
    if (parameterTypes != null) {
        StringBuilder constructBuilder = new StringBuilder();
        //逐一遍历所有参数的类型,获取其简化名
        for (int i = 0; i < parameterTypes.length; i++) {
            Class paramType = parameterTypes[i];
            String paramTypeSimpleName = paramType.getSimpleName();
            constructBuilder.append(paramTypeSimpleName + " param" + i + ",");
        }
        
        //多一个逗号的处理
        String constructStr = constructBuilder.toString();
        Log.i("test", "constructStr == " + constructStr);
        int length = constructStr.length();
        if (length >= 1) {
            constructStr = constructStr.substring(0, length - 1);
        }
        constructorBuilder.append(constructStr);
    }
    constructorBuilder.append(");\n\n");
}
bundle.putSerializable("constructors", constructorBuilder.toString());

  获取所有已声明方法:

//getDeclaredMethods:获取所有已声明的方法
Method[] methods = clazz.getDeclaredMethods();
StringBuilder methodBuilder = new StringBuilder();
for (Method method : methods) {

    //获取该方法的修饰符
    int modifiers = method.getModifiers();
    String modifiersStr = Modifier.toString(modifiers);
    methodBuilder.append(modifiersStr);

    //获取该方法的返回类型
    String typeName = method.getReturnType().getSimpleName();
    methodBuilder.append(" " + typeName);

    //获取方法名
    String name = method.getName();
    methodBuilder.append(" " + name);

    //获取该方法的所有参数类型
    Class<?>[] typeParameters = method.getParameterTypes();
    methodBuilder.append("(");
    if (typeParameters != null) {
        StringBuilder paramTypeBuilder = new StringBuilder();
        //逐一编译该方法的类型
        for (int i = 0; i < typeParameters.length; i++) {
            String param = typeParameters[i].getSimpleName();
            paramTypeBuilder.append(param + " param" + i + ",");
        }
        //多一个逗号的处理
        String paramTypeStr = paramTypeBuilder.toString();
        int length = paramTypeStr.length();
        if (length >= 1) {
            paramTypeStr = paramTypeStr.substring(0, length - 1);
        }
        methodBuilder.append(paramTypeStr);
    }
    methodBuilder.append(");\n\n");
}
bundle.putSerializable("methods", methodBuilder.toString());

  观察上述代码,可以发现,获取构造方法信息和获取普通方法信息的代码是很像的。因为构造方法也是方法嘛,只是在获取时不一致,一个是getDeclaredConstructors,另一个是getDeclaredMethods,其他诸如获取方法的修饰符、获取方法的参数类型等都是一样的。

使用反射获取一个类所有的变量和方法

  上述只是在使用反射的相关api获取类信息,并没有对这些信息进行使用。其实我们可以获取一个类中的字段值,比如获取ActivityThread中的字段mH的值:

//获取ActivityThread的mH字段的值
//获取ActivityThread的class对象
Class clazz = ActivityThread.class;
try {
    //每一个成员变量对应一个Field对象,获取mH的Field对象
    Field field = clazz.getDeclaredField("mH");
    //设置成员变量为可访问
    field.setAccessible(true);
    //获取字段mH的值
    Handler mH = (Handler) field.get(ActivityThread.currentActivityThread());
    
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

  需要说明的有几点:ActivityThread在android.jar中是隐藏api,但在代码中是可以直接用的;setAccessible设成true,表示该字段值就可以访问或者修改了;field的get方法的调用,需要传入此field所在类的对象,该field是在ActivityThread中,ActivityThread.currentActivityThread()就是获取ActivityThread对象的。

  越来越觉得,android.jar存在的意义就是在Android Studio上模拟手机的运行环境,以便应用在Android Studio中能编译通过在手机上就能够正常运行。ActivityThread类中的main方法是应用运行的入口,手机中肯定也会有ActivityThread的字节码或其它形式的文件存在的。另外,想通过上述方式获取mH的对象有一个前提,就是你知道ActivityThread中有mH这个字段且你知道怎么获取ActivityThread对象。还有一点要说明,在使用get获取字段值时,如果此字段为非静态字段,那get的参数需要传入此字段所在类的实例对象,如果此字段是静态字段,那get的参数传入null就可以了。下面在获取ActivityThread的内部类H的静态字段时就是传入了null的。

  开头说的两篇反射文章中第一篇hook了Handler的mCallback字段,然后在主线程处理消息时就可以进行一些我们自己的处理。那他们是怎么做到的呢?首先,我们要知道,主线程在处理消息(接收到消息,不再在nativePollOnce中阻塞,从消息队列中取出信息Message,然后执行此信息Message)时,会调用主线程Handler对象mH的dispatchMessage方法,其内容如下:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

  如果Message中有回调,就执行Message中的回调;如果Handler对象的成员变量mCallback非空,就执行mCallback的handleMessage方法;在上述两者均未执行或mCallback的handleMessage执行并未消耗掉此消息的前提下,才会执行Handler对象的handleMessage方法。而我们在此处可以获取到mCallback字段,然后给它附上我们自己的值,这样就能够处理我们自己的逻辑了。当然为了不影响android框架中mCallback原有的功能,我们还需要调用下mCallback的handleMessage方法。上面已经获得了ActivityThread的mH对象,下面我们要获取mH对象的mCallback字段并替换逻辑:

//获取Handler的mCallback对象
Class clazzHandler = Handler.class;
Field mCallbackField = clazzHandler.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
//这里是获取mCallback的值,有可能为空
Handler.Callback mCallback = (Handler.Callback) mCallbackField.get(mH);
mCallbackField.set(mH, new MyCallback(mCallback));

private static class MyCallback implements Handler.Callback {

    private Handler.Callback mCallback;

    public MyCallback(Handler.Callback mCallback) {
        this.mCallback = mCallback;
    }

    @Override
    public boolean handleMessage(Message msg) {
        
        //此处是获取ActivityThread的内部类H
        Class clazz = ActivityThread.class;
        Class[] declaredClasses = clazz.getDeclaredClasses();
        Class hClazz = null;
        for (Class cl : declaredClasses) {
            String simpleName = cl.getSimpleName();
            if (simpleName.equals("H")) {
                hClazz = cl;
                break;
            }
        }
        if (hClazz != null) {
            try {
                //获取该内部类H的一个静态成员变量
                Field launchActivityField = hClazz.getDeclaredField("LAUNCH_ACTIVITY");
                int launchActivityFieldValue = (int) launchActivityField.get(null);
//                    Log.i("test", "launchActivityFieldValue:" + launchActivityFieldValue);
                if (msg.what == launchActivityFieldValue) {
                    //处理自己的逻辑
                    Log.i("test", "这里是我们自己的逻辑:监听到了Acitivity启动");
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        if (mCallback != null) {
            return mCallback.handleMessage(msg);
        } else {//此处返回false,可以继续向下执行
            return false;
        }
    }
}

  上面的代码首先通过反射的方式获取Handler类中的字段mCallback,然后给它设置一个新值:MyCallback对象。在创建MyCallback对象时,传入了Handler中原有的mCallback值,这样是为了处理原有的mCallback中的逻辑。之后,创建了一个静态内部类MyCallback。之所以是静态的,是为了不让其创建的对象拥有其外部类的引用。该静态内部类中通过反射的方式获取到了ActivityThread的内部类H(mH的类型),然后也是通过反射进一步获取到H类中的静态字段LAUNCH_ACTIVITY。

  另外,还需注意的一点是,上述代码应该写在我们自定义的Application类的attachBaseContext中。这里有一个小插曲,之前在看android源码时,看到在performLaunchActivity方法中创建了Activity对象之后调用了makeApplication创建了Application对象,所以之前一直以为Application的创建时机就是在入口Activity对象创建之后。然后就猜想,上述的hook是不能监听到入口activity的启动的,但执行之后的结果却让我大跌眼镜,上述的hook是可以监听到入口activity的启动的!然后通过打其它log进行验证,发现Application的创建时机,其实是在入口Activity启动之前的。

总结

  反射,是一种很强大的功能。像Android框架这种我们不能改变的代码,我们可以通过反射在不改变原有框架功能逻辑的基础上,添加一些自己的逻辑。个人认为这种能力,相当cool。

  总的来说,反射能够获取字节码的信息(成员变量、构造方法、普通方法等),然后对这些信息可以进行操作(获取值,修改值,调用方法,修改方法)。

完整代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容