反射及Android实战

反射在 Java 和 Android 中有着广泛应用,本文通过具体实例来学习反射的概念和思维方式。

反射

定义

Java 的反射机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

用途

在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的或者只对系统应用开放,这时候就可以利用 Java 的反射机制,通过反射来获取所需的私有成员或方法。

反射机制的相关类

类名 用途
Class 类 代表类的实体,在运行的 Java 应用程序中表示类和接口
Field 类 代表类的成员变量(成员变量也称为类的属性)
Method 类 代表类的方法
Constructor 类 代表类的构造方法

Class 类

获得类相关的方法
方法 用途
asSubclass(Class clazz) 把传递的类的对象转换成代表其子类的对象
Cast 把对象转换成代表类或是接口的对象
getClassLoader() 获得类的加载器
getClasses() 返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses() 返回一个数组,数组中包含该类中所有类和接口类的对象
forName(String className) 根据类名返回类的对象
getName() 获得类的完整路径名字
newInstance() 创建类的实例
getPackage() 获得类的包
getSimpleName() 获得类的名字
getSuperclass() 获得当前类继承的父类的名字
getInterfaces() 获得当前类实现的类或是接口
获得类中属性相关的方法
方法 用途
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对象
getDeclaredFields() 获得所有属性对象
获得类中注解相关的方法
方法 用途
getAnnotation(Class annotationClass) 返回该类中与参数类型匹配的公有注解对象
getAnnotations() 返回该类所有的公有注解对象
getDeclaredAnnotation(Class annotationClass) 返回该类中与参数类型匹配的所有注解对象
getDeclaredAnnotations() 返回该类所有的注解对象
获得类中构造器相关的方法
方法 用途
getConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法
获得类中方法相关的方法
方法 用途
getMethod(String name, Class...<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法
类中其他重要的方法
方法 用途
isAnnotation() 如果是注解类型则返回true
isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果是指定类型注解类型则返回true
isAnonymousClass() 如果是匿名类则返回true
isArray() 如果是一个数组类则返回true
isEnum() 如果是枚举类则返回true
isInstance(Object obj) 如果obj是该类的实例则返回true
isInterface() 如果是接口类则返回true
isLocalClass() 如果是局部类则返回true
isMemberClass() 如果是内部类则返回true

Field 类

方法 用途
equals(Object obj) 属性与obj相等则返回true
get(Object obj) 获得obj中对应的属性值
set(Object obj, Object value) 设置obj中对应属性值

Method 类

方法 用途
invoke(Object obj, Object... args) 传递object对象及参数调用该对象对应的方法

Constructor 类

方法 用途
newInstance(Object... initargs) 根据传递的参数创建类的对象

带有 Declared 修饰的方法可以反射到私有的方法,没有 Declared 修饰的只能用来反射公有的方法。

示例

  • 简单示例

    首先创建一个 Npc 类(被反射的类)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    package dev.svip.reflect;
    
    public class Npc {
        //静态field
        public static int NPC_TYPE_1 = 1;
        //私有成员变量
        private int npcType;
        //公有成员变量
        public String name;
        //无参构造函数
        public Npc(){
        }
        //有参构造函数
        public Npc(int npcType, String name){
            this.npcType = npcType;
            this.name = name;
        }
    
        public int getNpcType(){
            return npcType;
        }
    
        public void setNpcType(int npcType){
            this.npcType = npcType;
        }
    
        public String getName(){
            return name;
        }
    
        public void setName(String name){
            this.name = name;
        }
    
        //静态方法
        public static void sayHello(String word){
            System.out.println("hello " + word);
        }
    }
    

    反射代码封装在 ClazzTest.java 中。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    
    package dev.svip.reflect;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class ClazzTest {
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
            /*
             获取Class对象
             */
            //第一种方式获取Class对象
            Npc npc1 = new Npc();
            Class<? extends Npc> npcClazz1 = npc1.getClass();
            System.out.println(npcClazz1.getName());
    
            //第二种方式获取Class对象
            Class<Npc> npcClazz2 = Npc.class;
            //判断第一种和第二种方式获得的Class对象是否为同一个
            System.out.println(npcClazz2 == npcClazz1);
    
            //第三种方式获取Class对象
            try {
                Class<?> npcClazz3 = Class.forName("dev.svip.reflect.Npc");
                //判断第一种和第三种方式获得的Class对象是否为同一个
                System.out.println(npcClazz3 == npcClazz1);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
    
            /*
             *访问方法
             */
            Npc npc = new Npc(1, "妲己");
            Class<Npc> npcClazz = Npc.class;
            //第一个参数是方法名,第二个参数是函数的参数class对象,因为存在重载的可能性,用参数类型区分
            Method sayHello = npcClazz.getMethod("sayHello", String.class);
            sayHello.invoke(npc, "world");
    
            /*
             * 访问类方法
             */
            System.out.println(npc.getName());
            Method setName = npcClazz.getMethod("setName", String.class);
            setName.invoke(npc, "鲁班");
            System.out.println(npc.getName());
    
            /*
             * 获取字段,读取字段的值
             */
            //获取公有成员
            Field field = npcClazz.getField("name");
            field.setAccessible(true);
            System.out.println(field.get(npc));
            //获取私有成员
            field = npcClazz.getDeclaredField("npcType");
            field.setAccessible(true);
            System.out.println(field.get(npc));
    
            /*
             * 获取构造函数,创建实例
             */
            Constructor<Npc> declaredConstructor = npcClazz.getDeclaredConstructor(int.class, String.class);
            npc = declaredConstructor.newInstance(1, "后羿");
            System.out.println(npc.getName());
    
            /*
             * 获取继承的父类
             */
            Class<? super Npc> superclass = npcClazz.getSuperclass();
            System.out.println(superclass.getName());
    
            /*
             * 获取注解
             */
            Annotation[] annotations = npcClazz.getAnnotations();
            for (Annotation annotation : annotations) {
                System.out.println(annotation.getClass().getName());
            }
    
        }
    }
    
  • 成熟项目

    GravityBox 项目中使用了大量的反射,可以在代码中通过搜索反射相关的方法进行查看。

Android 实战

JNI 调用 Java

在之前文章的easymd5项目中,我们通过 Java 调用 Native 层代码,现在,我们将在 Native 层通过反射调用 Java 代码。

首先在 Android Studio 中新建一个 Native C++ 项目。Java 层代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package dev.svip.reflectionmd5;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.TextView;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import dev.svip.reflectionmd5.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'reflectionmd5' library on application startup.
    static {
        System.loadLibrary("reflectionmd5");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.append(System.lineSeparator());
        tv.append("java md5 => " + Javamd5("blog.svip.dev") + System.lineSeparator());
        tv.append("refe md5 => " + Refmd5("blog.svip.dev") + System.lineSeparator());
        tv.append("refe java md5 => " + Refmd5Second("blog.svip.dev") + System.lineSeparator());
    }

    /**
     * A native method that is implemented by the 'reflectionmd5' native library,
     * which is packaged with this application.
     */
    public native static String Refmd5(String string);

    public native String Refmd5Second(String string);

    public String Javamd5(String string) {
        if (TextUtils.isEmpty(string)) {
            return "";
        }
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            byte[] bytes = md5.digest(string.getBytes());
            StringBuilder result = new StringBuilder();
            for (byte b : bytes) {
                String temp = Integer.toHexString(b & 0xff);
                if (temp.length() == 1) {
                    temp = "0" + temp;
                }
                result.append(temp);
            }
            return result.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

我们定义了两个 native 层方法:Refmd5 和 Refmd5Second,并通过 Java 实现了一个计算 md5 的方法 Javamd5。

Refmd5 的 C++ 实现如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extern "C"
JNIEXPORT jstring JNICALL
Java_dev_svip_reflectionmd5_MainActivity_Refmd5(JNIEnv *env, jclass clazz, jstring string) {
//    MessageDigest md5 = MessageDigest.getInstance("MD5");
    jclass MessageDigest = env->FindClass("java/security/MessageDigest");
    jmethodID getInstance = env->GetStaticMethodID(MessageDigest, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jstring MD5 = env->NewStringUTF("MD5");
    jobject md5 = env->CallStaticObjectMethod(MessageDigest, getInstance, MD5);

//    byte[] bytes = md5.digest(string.getBytes());
    jmethodID digest = env->GetMethodID(MessageDigest, "digest", "([B)[B");
    jclass stringClass = env->FindClass("java/lang/String");
    jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "()[B");
    auto jba = (env -> CallObjectMethod(string, getBytes));
    jbyteArray md5result = static_cast<jbyteArray>(env -> CallObjectMethod(md5, digest, jba));

    char *cmd5 = reinterpret_cast<char *>(env -> GetByteArrayElements(md5result, 0));

    char dest[32] = {0};
    for (int i = 0; i < 16; ++i) {
        sprintf(dest+i*2, "%02x", cmd5[i]);
    }
    return env->NewStringUTF(dest);
}

注释为等效的 Java 代码。从中我们可以看出,在 JNI 中,首先通过反射获取到类,然后获取方法,最后通过 CallStaticObjectMethod 调用方法,从而实现对应的 Java 代码的功能。

Refmd5Second 实现如下。

1
2
3
4
5
6
7
8
extern "C"
JNIEXPORT jstring JNICALL
Java_dev_svip_reflectionmd5_MainActivity_Refmd5Second(JNIEnv *env, jobject obj, jstring string) {
    jclass MainActivity = env->FindClass("dev/svip/reflectionmd5/MainActivity");
    jmethodID Javamd5 = env->GetMethodID(MainActivity, "Javamd5", "(Ljava/lang/String;)Ljava/lang/String;");
    jstring result = static_cast<jstring>(env -> CallObjectMethod(obj, Javamd5, string));
    return result;
}

两个方法实现的功能都是计算字符串的 md5,区别在于:Refmd5 的实现调用了 JDK 自带的类提供的方法,而 Refmd5Second 的实现调用了我们自己定义的类方法,即 Javamd5。

Unidbg 调用 so 文件

最终生成的 apk 可点击此处下载,解压后将 lib 目录下的 so 文件复制到 unidbg 目录中即可。

此处我们使用 64 位,将 so 文件放置在 unidbg-android/src/test/resources/example_binaries/arm64-v8a/libreflectionmd5.so 中。

在 unidbg 中新建一个类来主动调用 so 文件中的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package dev.svip.easymd5;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class RefMd5 extends AbstractJni {
    public static void main(String[] args) {
        RefMd5 refMd5 = new RefMd5();
        refMd5.run();
        refMd5.runsecond();
    }

    private final AndroidEmulator androidEmulator;
    private final DvmClass dvmClass;

    private RefMd5() {
        androidEmulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new DynarmicFactory(true)).build();
        Memory memory = androidEmulator.getMemory();
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        VM vm = androidEmulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(false);
        File libfile = new File("unidbg-android/src/test/resources/example_binaries/arm64-v8a/libreflectionmd5.so");

        DalvikModule dalvikModule = vm.loadLibrary(libfile, false);

        dalvikModule.callJNI_OnLoad(androidEmulator);
        dvmClass = vm.resolveClass("dev/svip/reflectionmd5/MainActivity");
    }

    private void run() {
        DvmObject<?> result = dvmClass.callStaticJniMethodObject(androidEmulator, "Refmd5(Ljava/lang/String;)Ljava/lang/String;", "blog.svip.dev");
        System.out.println("result is => " + result.getValue());
    }

    private void runsecond() {
        DvmObject<?> result = dvmClass.newObject(null).callJniMethodObject(androidEmulator, "Refmd5Second(Ljava/lang/String;)Ljava/lang/String;", "blog.svip.dev");
        System.out.println("jni call java method result => " + result.getValue());
    }
}

image-20220114143719056

可以看出,对 Refmd5 的调用运行成功,而对 Refmd5Second 的调用失败,提示不支持的操作,抛出的异常中正是我们自己定义的 Javamd5 方法。同样都是通过反射调用 Java 方法,为什么第一个能够成功呢?这是因为 Unidbg 帮我们实现了 Java 官方库中的方法,我们可以在 AbstractJni.java 中找到相关的实现。

image-20220114145128880◎ callObjectMethodV 实现

所以要解决上述报错,我们只需要将缺失的 Javamd5 补全,然后重写 callObjectMethodV 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
	if (signature.equals("dev/svip/reflectionmd5/MainActivity->Javamd5(Ljava/lang/String;)Ljava/lang/String;")) {
		//获取传入的参数
		String str = vaList.getObjectArg(0).getValue().toString();
		//调用我们自己实现的md5方法并返回
		return new StringObject(vm, md5(str));
	}

	return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

//我们自己实现的md5方法
private String md5(String string) {
	byte[] digest = null;
	try {
		MessageDigest md5 = MessageDigest.getInstance("md5");
		digest = md5.digest(string.getBytes(StandardCharsets.UTF_8));
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	}
	//16是表示转换为16进制数
	assert digest != null;
	return new BigInteger(1, digest).toString(16);
}

image-20220114150042009◎ 调用成功

onCreate Native 化

同理,我们也可以将 onCreate 进行 Native 化,jni 代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
extern "C"
JNIEXPORT void JNICALL
Java_dev_svip_reflectionmd5_MainActivity_onCreate(JNIEnv *env, jobject thiz,
                                                  jobject saved_instance_state) {
    // super.onCreate(savedInstanceState);
    jclass FragmentActivity = env -> FindClass("androidx/fragment/app/FragmentActivity");
    jmethodID onCreate = env -> GetMethodID(FragmentActivity, "onCreate", "(Landroid/os/Bundle;)V");
    env -> CallNonvirtualVoidMethod(thiz, FragmentActivity, onCreate, saved_instance_state);


    // binding = ActivityMainBinding.inflate(getLayoutInflater());
    jclass LayoutInflater = env -> FindClass("dev/svip/reflectionmd5/MainActivity");
    jmethodID getLayoutInflater = env -> GetMethodID(LayoutInflater, "getLayoutInflater",
                                                     "()Landroid/view/LayoutInflater;");
    jclass ActivityMainBinding = env -> FindClass(
            "dev/svip/reflectionmd5/databinding/ActivityMainBinding");
    jmethodID inflate = env -> GetStaticMethodID(ActivityMainBinding, "inflate",
                                                 "(Landroid/view/LayoutInflater;)Ldev/svip/reflectionmd5/databinding/ActivityMainBinding;");

    jobject ret_getLayoutInflater = env -> CallNonvirtualObjectMethod(thiz, LayoutInflater,
                                                                      getLayoutInflater);
    jobject binding = env -> CallStaticObjectMethod(ActivityMainBinding, inflate,
                                                    ret_getLayoutInflater);

    // setContentView(binding.getRoot());
    jclass MainActivity = env -> FindClass("dev/svip/reflectionmd5/MainActivity");
    jmethodID setContentView = env -> GetMethodID(MainActivity, "setContentView",
                                                  "(Landroid/view/View;)V");

    jmethodID getRoot = env -> GetMethodID(ActivityMainBinding, "getRoot",
                                           "()Landroidx/constraintlayout/widget/ConstraintLayout;");

    jobject ret_getRoot = env -> CallObjectMethod(binding, getRoot);

    env -> CallVoidMethod(thiz, setContentView, ret_getRoot);

    // TextView tv = binding.sampleText;
    jfieldID sampleText = env -> GetFieldID(ActivityMainBinding, "sampleText",
                                            "Landroid/widget/TextView;");
    jobject tv = env -> GetObjectField(binding, sampleText);

    // tv.append("\nblog.svip.dev");
    jclass TextView = env -> FindClass("android/widget/TextView");
    jmethodID append = env -> GetMethodID(TextView, "append", "(Ljava/lang/CharSequence;)V");
    env -> CallVoidMethod(tv, append, env -> NewStringUTF("\nblog.svip.dev"));
}

其中,super.onCreate(savedInstanceState); 还有以下两种实现方式,主要区别在于对父类的寻找上。

1
2
3
4
jclass MainActivity = env -> GetObjectClass(thiz);
jclass superActivity = env -> GetSuperclass(MainActivity);
jmethodID onCreaate = env -> GetMethodID(superActivity, "onCreate", "(Landroid/os/Bundle;)V");
env -> CallNonvirtualVoidMethod(thiz, superActivity, onCreaate, saved_instance_state);
1
2
3
4
jclass MainActivity = env -> FindClass("dev/svip/reflectionmd5/MainActivity");
jclass superActivity = env -> GetSuperclass(MainActivity);
jmethodID onCreaate = env -> GetMethodID(superActivity, "onCreate", "(Landroid/os/Bundle;)V");
env -> CallNonvirtualVoidMethod(thiz, superActivity, onCreaate, saved_instance_state);

然后将 MainActivity 中的 onCreate 方法修改为以下即可

1
2
3
@SuppressLint("MissingSuperCall")
@Override
protected native void onCreate(Bundle savedInstanceState);

image-20220115165744831◎ jadx

此时我们已无法通过 jadx 看到 onCreate 的实现。

另外,我们还可以将方法都进行动态注册,从而增大分析难度,具体可参考动态注册部分

本文中 Android 项目代码请访问:https://github.com/svipblog/ReflectionMd5

参考资料

1、https://www.jianshu.com/p/9be58ee20dee

2、https://www.52pojie.cn/thread-1367840-1-1.html

3、https://github.com/svipblog/ReflectionMd5

updatedupdated2022-01-172022-01-17