0%

逆向学习 0x09NDK开发

SO入门

先自己测一下这个案例

抓个包

1
2
3
4
5
6
7
8
9
10
11
POST /user/api/v1/login/app/mobile HTTP/1.1
pa: MTczMjAxNzY1MzAzMyxhMTZhM2ZmOWVmYzU0M2IxYjlkMTBiMGI3NjY3NTk4MiwzZjg3MjNlMWNiMzBhYzYwM2RiYjY1ZGQ4N2QxMTA3Miw=
appInfo: {"IMEI":"bf1821d5634d0ec8","appBuild":"21070602","appVersion":"6.2.0","deviceId":"bf1821d5634d0ec8","deviceName":"NX627J","osType":"android","osVersion":"9","phoneName":"NX627J","phoneSystemVersion":"9","vendor":"nubia"}
User-Agent: PocketFans201807/6.2.0_21070602 (NX627J:Android 9;nubia PQ3B.190801.03191204 release-keys)
Content-Type: application/json; charset=UTF-8
Content-Length: 43
Host: pocketapi.48.cn
Connection: Keep-Alive
Accept-Encoding: gzip

{"mobile":"13112345678","pwd":"a123123123"}
image-20241119200211946
1
2
3
4
5
6
感觉只有 pa 字段值得注意, MT ey 开头的一般是base64
MTczMjAxNzY1MzAzMyxhMTZhM2ZmOWVmYzU0M2IxYjlkMTBiMGI3NjY3NTk4MiwzZjg3MjNlMWNiMzBhYzYwM2RiYjY1ZGQ4N2QxMTA3Miw=

base64解码
1732017653033,a16a3ff9efc543b1b9d10b0b76675982,3f8723e1cb30ac603dbb65dd87d11072,
看到有三节数据,第一节感觉像是时间戳

image-20241119201034054

那么直觉告诉我剩下两个就是账号和密码了

1
2
3
a16a3ff9efc543b1b9d10b0b76675982
3f8723e1cb30ac603dbb65dd87d11072
32位密文感觉和MD5有关,但是不完全是,可能加了什么东西

hook了一下加密方法,出现了SHA1,不理解,进去看看

image-20241119201744331

native表示调用SO层的方法

image-20241119202129315

这有个libcipher.so可能有加密的方法,逆一下

image-20241119202254016

打开IDA就麻爪了

视频是从base64开始入手,因为已经知道是Base64编码了,就hook base64的方法,尝试四个类

1
2
3
4
5
6
7
8
9
10
11
12
13
java.net.URLEncoder
java.util.Base64
okio.Base64
okio.ByteString

var base64 = Java.use("android.util.Base64") ;
base64.encodeToString.overload('[B', 'int').implementation = function (a, b){
showStacks();
console.log("base64.encodeToString:", JSON.stringify(a));
var result = this.encodeToString(a, b);
console.log("base64.encodeToString result: ", result)
return result;
}

两个数据,要找的在下面

image-20241119204315093

去反编译找 com.pocket.snh48.base.core.kotlin.net.KTokenHeaderInterceptor.intercept 方法

image-20241119204536583

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
str = String.valueOf(System.currentTimeMillis()) + ',' + StringsKt__StringsJVMKt.replace$default(UUID.randomUUID().toString(), "-", "", false, 4, (Object) null) + ',' + ((Object) EncryptlibUtils.MD5(AbstractC9602.m21775(), valueOf, replace$default, postkey)) + ',' + ((Object) postkeyVersion);


第一个是时间戳实锤了

第二部分
StringsKt__StringsJVMKt.replace$default(UUID.randomUUID().toString(), "-", "", false, 4, (Object) null)
这个东西是生成一个UUID,然后进行一些操作,这个东西看起来像MD5其实不是,只是一个随机数


最后一个是空值不管他


第三部分
((Object) EncryptlibUtils.MD5(AbstractC9602.m21775(), valueOf, replace$default, postkey))


进入MD5方法

看到没有代码,这个时候有两种情况,1、被加固了,或者被加密了(详见消失的钥匙)2、代码在SO层

这里有native关键字,那就是SO层

上面有提到加载了那个SO文件 encryptlib

image-20241119205122099

对比一下这两个流程,很显然下面这个更有逻辑,从base64入手。我自己就没什么逻辑了想到加密就hook一下。

IDA逆向,找到了MD5,这个 Java_+包名的 是jni的静态注册

空格切换视图,F5查看伪C代码

image-20241119205946687

hook一下这个方法就好了

image-20241119210326618

至于这么hook这个步子迈大了,先老老实实看一下SO的东西,再说hook

so中通常会解除到的东西

==jni调用==

jni相关的代码可以理解成so层的系统函数,下面这些就是jni的调用

这个调用名字固定,功能固定

GetStringUTFChars :将Java的字符串转换为C的字符串

NewStringUTF :将C语言字符串转换成Java字符串

image-20241119210640338

关于jni还要学习的东西还很多,比如为什么调用的就是这个函数呢,是这么实现转换的呢,C是这么调用Java函数的,Java是这么调用C函数的,为什么传进去四个参数,接收了六个参数

==系统库函数==

比如这些 strlen 、memcpy C语言函数,来自libc.so,由libc.so来实现

比如 std:: 这样的C++函数,来自libC++.so

这些系统函数也是可以hook的

image-20241119211616336

==加密算法==

==魔改算法==

这俩没啥好说的,学好基础就能看,熟能生巧

==系统调用==

所谓的Linux内核的东西了,app是有四层,最上方是第三方的app,然后是Java层,然后是so层,最后是Linux内核,在其他层的操作最终都是由Linux内核来实现的。那么有些app就直接对Linux内核进行调用,这个时候是hook不到的,除非魔改内核

==自定义算法==

自己写的算法,这个时候就要有汇编基础了

==SO加固、混淆==

SO有一个头表SHT,IDA就是从这个表开始,解析so文件,有的app运行起来会把这个表删掉,这个时候IDA就解析不了了,就需要去手机中进行 ==SO的dump== 就是将SO从内存中保存下来,保存的SO是损坏的,需要使用工具进行 ==SO的修复== ,想要理解就需要去了解 ==SO的文件结构== ,这个东西就很复杂了,如果工具解析不出来,说明进行了 ==自定义linker== ,否则工具一般没问题

自定义linker 是自己定义了SO的加载和解析

NDK介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1、app为什么会把代码放到SO中
a C语言历史悠久,有很多现成的代码可用
b C代码的执行效率比Java高
c java代码容易被反编译,而且反编译以后逻辑比较清晰,C语言反编译的伪代码扣出来是难运行的

2、为什么学NDK开发
在安卓的so开发中,其他基本跟C/C++开发一致,而与Java交互需要用到jni
学习NDK的jni相关的内容
so中会接触到的:系统库调用、jni调用、加密算法、魔改算法、系统调用、自定义算法

3、什么是jni
jni是Java Native Interface的缩写。从Java1.1开始,jni标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互

4、什么是NDK
交叉编译工具链,用来编译so代码的
NDK的配置
NDK、CMake、LLDB
NDK是原生开发套件,可以让app中使用C/C++代码
CMake是外部构建工具,与Gradle搭配构建原生库
LLDB可以调试原生代码(stuid默认存在)

image-20241120110704561

1
2
5、ABI与指令集
https://developer.android.com/ndk/guides/abis
ABI 支持的指令集 备注
armeabi-v7a
arm32
32位汇编
armeabi
Thumb-2
VFPv3-D16
arm64-v8a
64位
AArch64
x86
x86主要是来支持模拟器
x86(IA-32)
MMX
SSE/2/3
SSSE3
x86_64
如果没有模拟器的支持
就需要模拟器来模拟
arm的指令来运行app
x86_64
MMX
SSE/2/3
SSSE3
SSE4.1、4.2
POPCNT

NDK和Java工程的区别

C工程比Java工程多静态代码块用于加载SO

image-20241120120506984

需要有声明,声明完就可以使用了

1
public native String stringFromJNI();

C的代码是写在cpp文件夹下的,有个同名的cpp

build.gradle.kts 文件中指明了CMake的路径和C语言的版本,这个CMake是指导编译的文件,C语言版本下面可以选择支持的SOABI,默认是四种都支持

image-20241120120857752

这些是C工程比Java工程多出来的东西,在Java工程中加上这些就是一个C工程了

jni注册

这里是一种静态注册,jni还有动态注册,这里默认是给SO层的方法传了两个参数,只要是跟Java对接的函数,都有这两个参数 JNIEnv 和 jobject/jclass

JNIEnv 后面再说,还是比较重要的

jobject是指的调用方法的对象,用 jclass是当方法为静态方法时使用

NewStringUTF 是将C语言的字符串转化为Java的字符串

image-20241121132114713

extern “C” 是表示函数以C的形式解析,可能会给函数名加上一些东西

JNICALL 中没有什么东西

JNIEXPORT 这里定义了方法是否需要导出,默认是 default ,如果是hidden 之后这个函数名就不会出现在so的导出表内,如果是静态注册是必须要出现在导出表内的

image-20241121133627556

so中常见的log输出

1
2
3
4
#include <android/log.h>


__android_log_print(ANDROID_LOG_DEBUG, "demo", "xxxxxx jni native %d %d", 100, 200);

需要最少传入三个值,第一个是枚举类型,第二个是TAG,和安卓里面的tag一样,第三个是输出的结果,但是支持占位符,可以传超过三个的参数

image-20241121140105082

这么写有点麻烦一般是使用 define 进行一个预先的声明

这个定义之后,再调用就方便很多

编译之后这俩东西就是一样的,预编译会将这个参数替换上去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <jni.h>
#include <string>
#include <android/log.h>

#define TAG "demo hello"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_demo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
__android_log_print(ANDROID_LOG_DEBUG, "demo", "xxxxxx jni native %d %d", 100, 200);
LOGD("xxxxxx jni native %d %d", 100, 200);
return env->NewStringUTF(hello.c_str());
}

NDK多线程

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
#include <jni.h>
#include <string>
#include <android/log.h>

#define TAG "demo hello"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);

void myThread() {
LOGD("this is from myThread")
}


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_demo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
__android_log_print(ANDROID_LOG_DEBUG, "demo", "xxxxxx jni native %d %d", 100, 200);
//LOGD("xxxxxx jni native %d %d", 100, 200);

pthread_t thread;
// int pthread_create(pthread_t*--pthread_ptr,pthread_attr_t const*__attr,void*(*__start_routine)(void*),void*);
pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);
return env->NewStringUTF(hello.c_str());
}

==pthread_t thread==

线程ID,其实就是long

image-20241128193959939

pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);

// 线程ID(传刚刚定义的long类型的指针)、线程属性、函数(传递函数指针)、传给函数的参数

// 等待线程执行完毕

// 默认的线程属性是 joinable 随着主线程结束而结束

pthread_join(thread, nullpyr)

// 线程属性是 dettach 可以分离执行

pthread_exit(0)

// 子线程中使用它来退出线程

JNI_OnLoad

so中各种函数的执行时机

init、init_array、JNI_OnLoad

JNI_OnLoad 的执行是比定义的普通函数还要早,init 比 init_array 早, init_array 比 JNI_OnLoad 早

image-20241128200003818

1
2
3
4
5
6
7
8
9
10
11
// 这两个参数是固定的,第一个JavaVM是一个结构体,第二个暂时没啥用
// 返回值类型是JNI版本
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
// JavaVM中有一个方法GetEnv
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("getenv failed");
return -1;
}
return JNI_VERSION_1_6;
}

一个so中可以不定义JNI_OnLoad

一旦定义了JNI_OnLoad,在so被加载的时候会自动执行

必须返回JNI版本 JNI_VERSION_1_6,否则报错

JNIEnv

里面定义了很多的API,创建对象数组什么的,所以需要先获取这个JNIEnv对象,而这个对象的获取就需要用到 JavaVM 这个结构体中的 GetEnv 方法,所以,上面的代码还是很常用的

image-20241128204059608

状态信息,判断是否支持JNI版本

image-20241128204423521

JavaVM

结构体,注意这个是C++版本的

东西也不多,一个属性,五个函数

比较常用的只有两个方法,GetEnv 和 AttachCurrentThread 一个是在主线程获得 JNIEnv ,一个是在子线程获得 JNIEnv

image-20241128204627408

点 DestroyJavaVM 查看C语言版本,其实就在上面翻一下就到了

C++版本只是对C版本的封装,其实还是调用的C中的方法,真正反编译看到的是C语言版本的

注意一下,参数个数是不一样的

image-20241128205145656

JavaVM的获取

​ JNI_OnLoad 的第一个参数

​ JNI_OnUnload的第一个参数

​ env->GetJavaVM

​ 对比各种方式获取的JavaVM指针是否一致

​ %p 打印地址,地址是一样的

因为 ==JavaVM每个进程中只有一份==(一个地址值)

JNIEnv

也是结构体,但是这个很常用

里面定义了很多的API,创建对象数组什么的,所以需要先获取这个JNIEnv对象,而这个对象的获取就需要用到 JavaVM 这个结构体中的 GetEnv 方法,具体的API后面再学,先简单知道有这个东西就好了

JNIEnv的获取方式

​ 函数静态/动态注册,传的第一个参数(不光JNIEnv的函数)

image-20241128211740416

​ vm->GetEnv

​ globalIVM->AttachCurrentThread(在子线程中使用的,和上面的同理)

​ 主线程和子线程中取得的JNIEnv的地址是不一样的

与JavaVM不同的是,==JNIEnv是每个线程中都有一份==(每个线程一个地址值)

so的基本概念

导出表、导入表

反编译后这个Exports就是导出表,也可以在编写的时候使用hidden这样对应的函数就不会出现在导出表中

导入表在旁边的imports中,是程序依赖的函数

image-20241129112947458

出现在导出表,导入表中的函数,一般可以通过frida相关的API获取函数地址,也可以自己计算

没有出现在导出表、导入表、符号表中的函数,都需要自己计算函数地址

所谓的符号表就是能看见名字的,都在符号表内,有的名字是IDA自己生成的,这个就需要自己计算,为什么一定要获取函数地址呢,因为So函数的hook需要函数地址

SO函数注册

有静态注册和动态注册两种方式

JNI函数的静态注册

静态注册的命名必须遵循一定的命名规则,一般是 Java_包名_类名_方法名

系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用静态注册的jni函数

静态注册的函数必然在导出表内

JNI动态注册

通过 env -> RegisterNatives 注册函数,通常在 JNI_OnLoad 中注册

​ RegisterNatives:意思是注册本地

​ JNINativeMethod:指定C函数和Java调用的对应关系

​ 函数签名:指定调用哪一个C函数

可以给同一个Java函数注册多个native函数,以最后一次为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这个FindClass的作用类似于frida的Java.use,也是注册函数传入的第一个参数
jclass MainActivityClazz = env->FindClass("com/example/demo/test");

// JNINativeMethod是一个结构体,分别为 name 函数名,signature 存放函数签名,fnRtr 函数指针
// 这个结构体规定了Java和C函数的对应关系
// JNINativeMethod中是可以注册多个函数的
// 第二个参数 () 内是传入的参数,后面是返回值的类型,因为String是对象,不是基本数据类型,所以需要用 L; 来包围,中间填写对象的具体路径。基本数据类型 int 就用I表示了,byte 数组用 [B
// 第三个参数是指定函数在C层对应的方法
JNINativeMethod method[] = {
{"stringFromJNI3", "(Ljava/lang/String;I[B)Ljava/lang/String;", (void *)test}
};
// jint其实就是int,是指明注册函数的个数
// jint RegisterNatives(jclass clazz, const JNINativeMethod* method, jint nMethods)
// 最后这个参数可以之间给数字,但是如果在开发中最好计算一下 sizeof(method)/sizeof(JNINativeMethod) 。
env->RegisterNatives(MainActivityClazz, method, 1);
image-20241129164315798

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 这个时候就可以自定义方法名了,传参什么的不能少
jstring test(JNIEnv* env, jobject that, jstring a, jint b, jbyteArray c) {
return env->NewStringUTF("Hello");
}


// 在JNI_OnLoad中进行 env->RegisterNatives 的动态注册
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("getenv failed");
return -1;
}

jclass MainActivityClazz = env->FindClass("com/example/demo/test");
JNINativeMethod method[] = {
{"stringFromJNI3", "(Ljava/lang/String;I[B)Ljava/lang/String;", (void *)test}
};
// jint RegisterNatives(jclass clazz, const JNINativeMethod* method, jint nMethods)
env->RegisterNatives(MainActivityClazz, method, sizeof(method)/sizeof(JNINativeMethod));

return JNI_VERSION_1_6;
}
1
2
3
4
5
6
public class test {
static {
System.loadLibrary("native-lib");
}
public native String stringFromJNI3(String a, int b, byte[] c);
}

注意:如果有重名的调用的是后面的,以最后一个为准。如:

1
2
3
4
JNINativeMethod method[] = {
{"stringFromJNI3", "(Ljava/lang/String;IB)Ljava/lang/String;", (void *)test};
{"stringFromJNI3", "(Ljava/lang/String;IB)Ljava/lang/String;", (void *)test0x1}
};

这个时候在Java端调用 stringFromJNI3 时,执行的C函数是 test0x1

SO之间的相互调用

多个cpp编译成一个so文件

创建一个c的源文件,将要编译成一个so文件的cpp写在配置文件中

image-20241202202608155

调用另一个cpp中的方法时,不需要再使用下面的方法调用,省去这个步骤,在前面声明一下方法名,然后在需要调用的时候直接函数名调用即可

1
#include ""

image-20241202203340209

测试成功,只有一个so,且可以正常使用cpp内的方法

image-20241202202406481image-20241202202457604

编译多个SO

这个需要的操作用小脑想一下,创建cpp文件,修改CMakeLists文件。最重要的当然是CMakeLists文件。

新加上一个add_library和target_link_libraries

一个值得so的名称和so内的cpp文件,另一个进行链接

image-20241202204854659

SO路径的动态获取

so在手机中的data/app/包名+随机名/lib/ 目录下

包名这个目录后面的数是随机的,每次安装都不一样

image-20241202210143999

我们需要用代码来找到这个目录中的SO文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这个Context是安卓一个常用的对象,里面有很多API
public String getPath(Context cxt) {
// .getPackageManager 得到包管理器
PackageManager pm = cxt.getPackageManager();
// .getInstalledPackages 获取已经安装的app
List<PackageInfo> pkgList = pm.getInstalledPackages(0);
// 过滤
if (pkgList == null || pkgList.size() == 0) return null;

// 循环查找
for (PackageInfo pi : pkgList) {
// .applicationInfo.nativeLibraryDir 就是获取本地so保存的地方
// 然后判断是 /data/app 开头,内容包含包名的so文件
if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")
&& pi.packageName.startsWith("com.example.demo")) {
return pi.applicationInfo.nativeLibraryDir;
}
}
return null;
}

Context对象通过 getApplicationContext() 方法来获取,可以看到输出了so所在的路径,但是还差了一个so的名字,自行补充一下就好了

image-20241202212101582

SO的动态调用

既然已经能够获取到SO的路径了,那么就可以实现so之间的相互调用了

在cpp文件中使用 dlopen 方法来获得

需要给两个参数,一个是SO的文件名,这个地方要给出全路径,一个是指定对SO的操着是立即解析还是懒加载等,常用的是 now和 lazy,RTLD_NOW是立即解析

image-20241203132351488

注册的时候加上一个这玩意,调用的时候多加一个参数,jstring

image-20241203133033340

人要的是 char * ,你给的是Java的字符串jstring,需要一个转换

使用 GetStringUTFChars 将 jstring 转换成 cstring,两个参数第一个是Java字符串,第二个是选择是否拷贝,一般用nullptr

然后使用 void* 来接收

1
2
const char* cPath = env->GetStringUTFChars(SoPath, nullptr);
void* soInfo = dlopen(cPath, RTLD_NOW);

有了这个 void* 的指针就可以使用 dlsym 函数来获取so文件中的符号,不是cpp文件中的函数名,而是函数名经过符号修饰之后的结果,这个结果可以通过反编译查看,或者使用 extren “C” 修饰函数,如此函数名就不会被修饰。

返回值是一个函数指针

1
2
3
4
5
// 先进行一个函数声明,声明一个可以代表任意一个没有返回值和没有参数的函数
void (*ref)();
// 使用 dlsym 函数获取指定函数名的地址值,然后使用 reinterpret_cast 强转成函数指针,然后就可以直接调用方法了
ref = reinterpret_cast<void (*)()> (dlsym(soInfo ,"_Z4testv"));
ref();

image-20241203174733741

调用成功

image-20241203180532224

image-20241203182405188

这中方式调用so是不用在Java层加载so的,执行代码的时候就自己给加载上了,如果要卸载so使用dlclose 函数

1
dlclose(soInfo);

SO的动态调用了解一下,逆向的时候可能会碰到。

这个的hook也和普通的不一样,因为这个地址是在一定时间内存在的

还有一种方式

给需要调用的SO方法 extern ”C“,然后在调用cpp的文件中使用 extern "C" 进行声明

image-20241203191333495

通过JNI创建Java对象

在使用so层的方法时,难免需要Java对象,有两种创建Java对象的方式

NewObject

很像Java中的反射,找类,找方法,调方法

1
2
3
4
5
6
7
   // NewObject创建对象,通过FindClass获取相应的类
jclass clazz = env->FindClass("com/example/demo/test");
// init这个函数名代表构造函数,找到对象的构造方法,这个示例是无参构造
jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
// 获取object对象,如果需要传参数,就从第三个开始
jobject ReflectDemoObj = env->NewObject(clazz, methodId);
LOGD("ReflectDemoObj %p", ReflectDemoObj);

ReflectDemoObj 是Java中的一个对象,输出的是这个对象在虚拟机中的一个唯一标识

这个地址是在虚拟机内,和外部的内存地址不在一个位置,因为在JNI中需要区分Java的东西和C的东西,本质就是因为存放的位置不一样。所以需要来回的转换

image-20241203210850623

AllocObject

1
2
3
4
5
6
7
8
9
10
// AllocObject创建对象
jclass jclass1 = env->FindClass("com/example/demo/test");
jmethodID jmethodId = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");

// 前两步是一样的,这里 AllocObject 是给函数分配内存,但是不进行初始化,可以看到只传递了类
jobject ReflectDemoObj2 = env->AllocObject(jclass1);
// 定义一个字符串
jstring jstr = env->NewStringUTF("from jni str");
// 进行初始化,传入内存地址,类,构造方法,传入值
env->CallNonvirtualVoidMethod(ReflectDemoObj2, jclass1, jmethodId, jstr);

image-20241203212934398

通过JNI访问Java属性

Java属性分为静态和非静态的,两种属性获取的方法不同

获取静态字段

使用 GetStatic****Field 来设置非静态字段

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
   jclass clazz = env->FindClass("com/example/demo/test");
// 获取类之后,通过 GetStaticFieldID 函数来获取静态成员的ID, GetFieldID 是获取非静态成员的ID
// 给三个参数,第一个是 jclass ,第二个是字段名,第三个是字段签名(数据类型)
jfieldID privateStaticStringField =
env->GetStaticFieldID(clazz, "age", "Ljava/lang/String;");

// 然后使用 GetStatic+数据类型+Field 这个函数来获取字段的具体内容
// 一共是8+1,八个基本数据类型+object
// env->GetStaticBooleanField();
// env->GetStaticIntField();
// env->GetStaticShortField();
// env->GetStaticByteField();
// env->GetStaticCharField();
// env->GetStaticFloatField();
// env->GetStaticDoubleField();
// env->GetStaticLongField();
// env->GetStaticObjectField();
// 已知是字符串的情况下,可以直接强转从object转成字符串
// 根据类名和字段名以及数据类型可以找到这个静态字段,但是注意,这个时候的age还是在虚拟机中,是Java的字符串,需要进行一个转换
jstring age =
static_cast<jstring> (env->GetStaticObjectField(clazz, privateStaticStringField));
// 将Java字符串转换成C字符串
const char* privatecstr = env->GetStringUTFChars(age, nullptr);
LOGD("age: %s", age);
// 使用完毕后需要释放内存
env->ReleaseStringUTFChars(age, privatecstr);

获取对象字段

这个和获取静态字段大差不差,就几个函数不一样

使用 Get****Field 来获取非静态字段

1
2
3
4
5
6
7
8
9
10
11
   jclass clazz = env->FindClass("com/example/demo/test");
jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodId);

// 上面创建实例化对象,非静态的东西用之前new一下很合理吧
jfieldID nameID = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
// 除了API上没有static之外,只有这里,传入的是实例化之后的对象,其他的都和上面的差不多
jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID));
const char* nameContent = env->GetStringUTFChars(name, nullptr);
LOGD("name: %s", nameContent);
env->ReleaseStringUTFChars(name, nameContent);

image-20241204114753473

设置非静态字段

使用 Set****Field 来设置非静态字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   jclass clazz = env->FindClass("com/example/demo/test");
jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodId);

// 获取对象字段
jfieldID nameID = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID));
const char* nameContent = env->GetStringUTFChars(name, nullptr);
LOGD("name: %s", nameContent);
env->ReleaseStringUTFChars(name, nameContent);

// 设置非静态字段
// 直接使用 SetObjectField 来更改字段,传入的是实例化之后的对象和要更改的字段ID,以及要更改为的数据
env->SetObjectField(ReflectDemoObj, nameID, env->NewStringUTF("呓语"));
// 重新获取Java类型的数据
name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID));
// 将Java类型的数据转换成C类型输出
nameContent = env->GetStringUTFChars(name, nullptr);
LOGD("name new: %s", nameContent);

image-20241204172804519

通过JNI访问Java数组

获取数组

通过 Get****ArrayElements 获取数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   jclass clazz = env->FindClass("com/example/demo/test");
jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodId);

// 获取数组字段ID,同字段ID的获取方式一样
jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B");
// 这里也是获取数组内容,使用Object来接收,然后再转换成jbyteArray的类型
jbyteArray byteArray1 =
static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID));
// 使用 GetArrayLength 方法获取数组的长度
int _byteArrayLength = env->GetArrayLength(byteArray1);


// 这个时候还不能直接遍历数组,需要一个转换,将Java类型的数据转换成 jbyte*
// 不光是 byte数组,其他数组遍历的时候也需要一个转换,对应的api为 Get+类型+ArrayElements。一共九个
jbyte* cByte = env->GetByteArrayElements(byteArray1, nullptr);
for (int i = 0; i < _byteArrayLength; i++) {
LOGD("byteArray: %d", cByte[i])
}

// 释放
env->ReleaseByteArrayElements(byteArray1, cByte, 0);

image-20241204174959021

修改数组

通过 Get****ArrayRegion 修改数组内容,Region是一次性设置完成,即使是数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   // 修改数组
// 先创建一个C的数组,C的char相当于Java的byte
char javaByte[10];
for (int i = 0; i < 10; i++) {
javaByte[i] = static_cast<char>(100-i);
}
// 将这个char数组转换成jbyte的类型
const jbyte *java_array = reinterpret_cast<const jbyte*>(javaByte);
// 使用 Region 的这一套API进行数组的修改
env->SetByteArrayRegion(byteArray1, 0, _byteArrayLength, java_array);
// 修改完之后,可以重新获取数组来遍历
byteArray1 =
static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID));
cByte = env->GetByteArrayElements(byteArray1, nullptr);
for (int i = 0; i < _byteArrayLength; i++) {
LOGD("byteArray: %d", cByte[i])
}

image-20241204180039225

通过JNI来访问Java方法

调用静态函数

调用静态函数的API和使用构造方法的API相似,但是不一样

1
2
3
4
5
6
7
   // 调用静态函数
// 创建类
jclass clazz = env->FindClass("com/example/demo/test");
// 构造方法使用 NewObject 或者 AllocObject 两个API来找到方法,其他的函数就不一样了,是 GetStaticMethodID
jmethodID staticFunID = env->GetStaticMethodID(clazz, "StaticFunction", "()V");
// 找到这个方法ID之后,使用 Call 系列的API CallStatic+返回值类型+Method,这个组合API有十个
env->CallStaticVoidMethod(clazz, staticFunID);

调用对象函数

1
2
3
4
5
6
7
8
9
10
11
   // 调用对象函数
// 找到对象函数的方法ID
jmethodID FunID =
env->GetMethodID(clazz, "functional", "(Ljava/lang/String;)Ljava/lang/String;");
jstring str = env->NewStringUTF("this is from JNI");
// 因为是对象函数调用需要一个实例化对象,使用前面已经创建过的实例化对象 ReflectDemoObj,然后传入方法ID和方法参数,因为返回值是String,将Object转换成string
jstring str2 = reinterpret_cast<jstring>(static_cast<jstring> (env->CallObjectMethod(ReflectDemoObj, FunID, str)));
// 将返回值的Java类型的字符串转换成C中的char*字符串
const char* retval_cstr = env->GetStringUTFChars(str2, nullptr);
LOGD("retval_cstr: %s", retval_cstr);
env->ReleaseStringUTFChars(str2,retval_cstr);

image-20241204191017506

CallObjectMethodA

CallObjectMethod这个API是用来调用执行函数的,同系列的还有 CallObjectMethodA 和 CallObjectMethodV

CallObjectMethod 执行的时候调用了 CallObjectMethodV,也就是说 CallObjectMethod 可以完美替代 CallObjectMethodV 在调用的时候的使用了,因为 CallObjectMethod 支持接收多个参数,如果不使用 CallObjectMethod 还得自行处理参数,传入 CallObjectMethodV

image-20241204191718523

CallObjectMethodA 的使用需要传入一个 jvalue,这个jvalue是一个联合体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;

// 后面的字母代表数据类型的简写,定义参数的时候可以使用以下方式
jvalue args[3];
args[0].i = 100;
args[1].c = str2;
args[2].z = true;

传递数组作为参数

稍微升级一下,调用传入值为数组,返回值也是数组的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   // 传入数组参数
// 先创建一个字符串数组
// 下面的操作就相当于Java中
// String StringArr[] = new String[3]
// for (int i = 0; i < 3; i++) {
// StringArr[i] = "NB666";
// }
jclass StringClazz = env->FindClass("java/lang/String");
jobjectArray StringArr = env->NewObjectArray(3, StringClazz, nullptr);
for (int i = 0; i < 3; ++i) {
env->SetObjectArrayElement(StringArr, i, env->NewStringUTF("NB666"));
}

// 获取方法ID GetStaticMethodID
jmethodID StringArrayID =
env->GetStaticMethodID(clazz, "functionTest", "([Ljava/lang/String;)[I");
// 执行函数,使用 jintArray 接收返回值,对返回值进行强转
jintArray jint = static_cast<jintArray>
(env->CallStaticObjectMethod(clazz, StringArrayID, StringArr));
// 处理返回值,将Java的数组转换成C的数组
int *cinArr = env->GetIntArrayElements(jint, nullptr);
LOGD("cintArr[0]=%d", cinArr[0]);
env->ReleaseIntArrayElements(jint, cinArr, JNI_ABORT);

image-20241204201627997

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
86
87
88
89
90
91
92
93
94
95
96
jstring demoHelloWorld(JNIEnv* env, jobject that, jstring a, jint b, jbyteArray c) {
// NewObject创建对象
jclass clazz = env->FindClass("com/example/demo/test");
jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodId);
LOGD("ReflectDemoObj %p", ReflectDemoObj);

// AllocObject创建对象
jclass jclass1 = env->FindClass("com/example/demo/test");
jmethodID jmethodId = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");
jobject ReflectDemoObj2 = env->AllocObject(jclass1);
jstring jstr = env->NewStringUTF("from jni str");
env->CallNonvirtualVoidMethod(ReflectDemoObj2, jclass1, jmethodId, jstr);

// 获取静态字段
jfieldID privateStaticStringField = env->GetStaticFieldID(clazz, "age", "Ljava/lang/String;");
jstring age = static_cast<jstring> (env->GetStaticObjectField(clazz, privateStaticStringField));
const char* privatecstr = env->GetStringUTFChars(age, nullptr);
LOGD("age: %s", privatecstr);
env->ReleaseStringUTFChars(age, privatecstr);

// 获取对象字段
jfieldID nameID = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID));
const char* nameContent = env->GetStringUTFChars(name, nullptr);
LOGD("name: %s", nameContent);
env->ReleaseStringUTFChars(name, nameContent);

// 设置非静态字段
env->SetObjectField(ReflectDemoObj, nameID, env->NewStringUTF("呓语"));
name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID));
nameContent = env->GetStringUTFChars(name, nullptr);
LOGD("name new: %s", nameContent);
env->ReleaseStringUTFChars(name, nameContent);

// 获取数组字段ID
jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B");
jbyteArray byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID));
int _byteArrayLength = env->GetArrayLength(byteArray1);

jbyte* cByte = env->GetByteArrayElements(byteArray1, nullptr);
for (int i = 0; i < _byteArrayLength; i++) {
LOGD("byteArray: %d", cByte[i])
}
env->ReleaseByteArrayElements(byteArray1, cByte, 0);


// 修改数组
char javaByte[10];
for (int i = 0; i < 10; i++) {
javaByte[i] = static_cast<char>(100-i);
}
const jbyte *java_array = reinterpret_cast<const jbyte*>(javaByte);
env->SetByteArrayRegion(byteArray1, 0, _byteArrayLength, java_array);

byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID));
cByte = env->GetByteArrayElements(byteArray1, nullptr);
for (int i = 0; i < _byteArrayLength; i++) {
LOGD("byteArray: %d", cByte[i])
}
env->ReleaseByteArrayElements(byteArray1, cByte, 0);


// 调用静态函数
jmethodID staticFunID = env->GetStaticMethodID(clazz, "StaticFunction", "()V");
env->CallStaticVoidMethod(clazz, staticFunID);

// 调用对象函数
jmethodID FunID =
env->GetMethodID(clazz, "functional", "(Ljava/lang/String;)Ljava/lang/String;");
jstring str = env->NewStringUTF("this is from JNI");
jstring str2 = reinterpret_cast<jstring>(static_cast<jstring>
(env->CallObjectMethod(ReflectDemoObj, FunID, str)));
const char* retval_cstr = env->GetStringUTFChars(str2, nullptr);
LOGD("retval_cstr: %s", retval_cstr);
env->ReleaseStringUTFChars(str2,retval_cstr);


// 传入数组参数
jclass StringClazz = env->FindClass("java/lang/String");
jobjectArray StringArr = env->NewObjectArray(3,StringClazz, nullptr);

for (int i = 0; i < 3; ++i) {
env->SetObjectArrayElement(StringArr, i, env->NewStringUTF("NB666"));
}
jmethodID StringArrayID =
env->GetStaticMethodID(clazz, "functionTest", "([Ljava/lang/String;)[I");
jintArray jint = static_cast<jintArray>
(env->CallStaticObjectMethod(clazz, StringArrayID, StringArr));
int *cinArr = env->GetIntArrayElements(jint, nullptr);
LOGD("cintArr[0]=%d", cinArr[0]);
env->ReleaseIntArrayElements(jint, cinArr, JNI_ABORT);


return env->NewStringUTF("Hello");
}

通过JNI访问Java父类方法

在Java中使用 super 来访问父类中的方法,现在就需要在CPP文件中实现类似的操作,就需要使用CallNonvirtual 系列的API

image-20241205085226582

先获取到调用的onCreate方法的父类,jclass,然后找到需要调用的父类方法的ID,再传入参数,参数是从Java层传过来的,直接放进去即可,这样就实现了一个简单的调用父类方法。

也可以看出要将Java代码使用JNI转换成c代码还是很费事的

1
2
3
4
5
6
7
8
9
extern "C"
JNIEXPORT void JNICALL
Java_com_example_demo_MainActivity_onCreates(JNIEnv *env, jobject thiz,
jobject saved_instance_state) {
// TODO: implement onCreates()
jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity");
jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state);
}

内存管理

之前使用的 ReleaseIntArrayElements、ReleaseStringUTFChars 等方法也算是一种吧,这里主要介绍的是局部引用和全局引用的相关API

局部引用

之前创建的 jclass、jmethodID、jstring 什么的就是一个局部引用,在函数运行完毕后消失,看起来和局部变量是差不多的,但是不一样的。可以用全局变量来实验一下

IDE也没有报错,运行直接崩溃了

image-20241205103603926

在函数中使用JNI函数,在函数执行完毕之后全部进行回收,不管是不是全局变量还是局部变量,也就是因为JNI_OnLoad函数执行完毕之后,clazz被回收了,所有在 demoHelloWorld 函数中没有获得正确的 clazz。这也就是局部引用和局部变量、全局变量的区别

大多数的jni函数,调用以后返回的结果都是局部引用

因此,env->NewLocalRef(创建一个局部引用) 基本不用

一个函数内的局部引用数量是有限制的,在早期的安卓系统中,十分的明显,现在基本够用,因此可能不太会见到这几个函数,早期的时候的so可能会有

当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用

可以使用env->DeleteLocalRef 来删除局部引用

局部引用还有一些函数

1
2
3
4
5
6
env->EnsureLocalCapacity(num)
// 判断是否有足够的局部引用可以使用,足够则返回0

需要大量使用局部引用时,手动删除太过麻烦,可以使用下面两个函数来批量管理局部引用
env->PushLocalFrame(num)
env->PopLocalFrame(nullptr)

全局引用

在jni开发中,需要跨函数使用变量时,直接定义全局变量是没啥用的,需要两个方法,来创建、删除全局引用

1
2
env->NewGlobalRef
env->DeleteGlobalRef

env->NewGlobalRef 是创建全局引用,需要传入一个 jobject ,然后返回一个jobject

现在就可以正常运行了

image-20241205110707267

全局引用使用完毕之后使用 env->DeleteGlobalRef 删除

还有两个函数,是弱全局引用,什么叫弱全局引用呢,就是内存充足的时候就是全局引用,内存紧张的时候,可能会被回收。

1
2
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef

子线程中获取Java类

常规的手段在子线程中是不能获取到Java类的,获取值为nullptr。获取方法会导致系统崩溃

但是可以使用FindClass来获取系统类,可以看到自己的Java类是没有获取到地址的

image-20241205112914772

如果想在子线程中获取自己定义的类,也是可以的,可以用到全局引用,在主线程中获取类,使用全局引用来传递到子线程中去。

image-20241205135936157

还有一种方式就是,在主线程中获取正确的ClassLoader,在子线程中去加载类

之前在Java学习过程中,出现了一些方法已经加载,但是无法hook的情况,那是因为不在同一个 ClassLoader 中,C可以先获取到对应方法所在的 ClassLoader,将ClassLoader给到子线程

在Java中想要获取ClassLoader,可以先获取类字节码,然后时候 getClassLoader() 来获取

​ Demo.class.getClassLoader();

​ new Demo().getClassLoader();

​ Class.forName(……).getClassLoader();

这三种方法,多少跟反射沾点边,其实FindClass就相当于Demo.class

这个时候只需要在主线程中调用一下getClassLoader() 然后在子线程中调用 loadClass() 就ojbk了

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
在JNI_OnLoad中

// 这些操作就相当于 Demo.class.getClassLoader()
// 然后将返回的 jobject 设置为全局引用,在子线程中引用
jclass text = env->FindClass("com/example/demo/test");
clazz = static_cast<jclass> (env->NewGlobalRef(text));

jclass javaClass = env->FindClass("java/lang/Class");
jmethodID jmethodId = env->GetMethodID(javaClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
jobject obj = env->CallObjectMethod(text, jmethodId);
object1 = env->NewGlobalRef(obj);


在子线程中

jclass ClassLoaderClazz = env->FindClass("java/lang/ClassLoader");
jmethodID loadClassID = env->GetMethodID(ClassLoaderClazz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
// 主要传到Java的字符串包名分割不少/,而是.
jclass test = static_cast<jclass> (env->CallObjectMethod(object1, loadClassID, env->NewStringUTF("com.example.demo.test")));

// 上面的操作就相当于 .loadClass("com.example.demo.test")
// 上面那么多代码,就相当于在Java中写
// test.class.getClassLoader().loadClass("com.example.demo.test");
// 获取到这个类之后就可以进行一些其他的操作了
jmethodID jmethodId =
env->GetStaticMethodID(test, "functionTest", "([Ljava/lang/String;)[I");
LOGD("myThread jmthodID: %p", jmethodId);

image-20241205153112644

这种方法挺麻烦的,不如 在主线程中获取类,使用全局引用来传递到子线程中来的方便,如果系统类在子线程中 findClass 更好。所以只做了解

init与initarray

JNI_OnLoad 在其他函数执行之前执行,还有俩个执行的时机比他早,一个是init,一个是initarray

init和initarray的存在就是处理so的加固解密什么的

init定义

1
2
3
extern "C" void _init() {
// 函数名必须为 _init,在C++中需要extern "C" 来防止修饰
}

initarray定义

1
__attribute__ ((constructor)) void initArrayTest1() {}

init函数定义只能是这一个,名字为 _init ,而initarray可以有多个,如果 constructor 后面不指定数字的话,就按照在cpp文件中的前后顺序执行,指定数字之后按照数字从小到大执行,不写数字的在带有数字的执行完毕之后执行,不建议使用 0-100,这些系统可能会用。这些执行完毕之后再执行 JNI_OnLoad

image-20241206103125764

可以加上一个 hidden 属性,这样so反编译的时候,这个函数就不会出现

image-20241206104625809

image-20241206104913974

在 IDA View-A 中使用快捷键 ctrl +S,找到这个 .init_array

image-20241206105000980

可以看到有这三个函数,按照给定的顺序排列执行,2被隐藏了,函数名无法解析,IDA就给自动生成了一个名字,sub_+函数地址

image-20241206105208277

这个函数地址其实是这个函数在So中的偏移量,一般so中的hook都是hook地址,很少hook函数名,因为符号会被各种的修饰,去除,但是地址值是不变的

image-20241206105419586

_init 编译之后,符号会变为 .init_proc,但是定义的时候必须是 _init

image-20241206105651028

onCreate函数native化

一般情况下app实现先执行MainActivity中的onCreate方法

1
2
3
4
5
6
7
8
9
10
11
ActivityMainBinding.inflate()
这个是和页面绑定的,具体的类是编译之后自己生成的,将鼠标放上面找打具体的类

// ActivityMainBinding
com.example.demo.databinding
public final class com.example.demo.databinding.ActivityMainBinding
extends androidx.viewbinding.ViewBinding

// inflate
com.example.demo.databinding.ActivityMainBinding
public static @NonNull com.example.demo.databinding.ActivityMainBinding inflate( @NonNull android.view.LayoutInflater inflater )

image-20241206112301132

1
2
3
// getRoot
com.example.demo.databinding.ActivityMainBinding
public androidx.constraintlayout.widget.ConstraintLayout getRoot()

尝试实现native化

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
extern "C"
JNIEXPORT void JNICALL
Java_com_example_demo_MainActivity_onCreates(JNIEnv *env, jobject thiz,
jobject saved_instance_state) {
// TODO: implement onCreates()
// super.onCreate(savedInstanceState);
jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity");
jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state);


// binding = ActivityMainBinding.inflate(getLayoutInflater());
// getLayoutInflater()
jclass aClass = env->FindClass("android/app/Activity");
jmethodID getLayoutInflaterID =
env->GetMethodID(aClass, "getLayoutInflater", "()Landroid/view/LayoutInflater;");
// 这里需要的对象是那个对象调用这个方法,在Java中getLayoutInflater方法没有前缀,说明就是MainActivity调用了这个方法
jobject LayoutInflater = env->CallObjectMethod(thiz, getLayoutInflaterID);

// ActivityMainBinding.inflate()
jclass ActivityMainBindingClazz =
env->FindClass("com/example/demo/databinding/ActivityMainBinding");
jmethodID inflateID =
env->GetStaticMethodID(ActivityMainBindingClazz, "inflate",
"(Landroid/view/LayoutInflater;)Lcom/example/demo/databinding/ActivityMainBinding;");
jobject Binding = env->CallStaticObjectMethod(ActivityMainBindingClazz,
inflateID, LayoutInflater);


// setContentView(binding.getRoot());
jmethodID getRootID =
env->GetMethodID(ActivityMainBindingClazz, "getRoot",
"()Landroidx/constraintlayout/widget/ConstraintLayout;");
jobject ConstraintLayout = env->CallObjectMethod(ActivityMainBindingClazz, getRootID);

jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID setContentViewID =
env->GetMethodID(AppCompatActivityClazz, "setContentView", "(Landroid/view/View;)V");
env->CallVoidMethod(thiz, setContentViewID, ConstraintLayout);
}

调了那么一串,其实才执行了三句话,后面的实现其实也大差不差,都是这个流程。

1
2
3
4
5
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
}

补档:

将onCreate彻底native化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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;
String path = getPath(getApplicationContext()) + "/libdemo.so";
tv.setText(stringFromJNI(path));
int a = 2;
String c = stringFromJNIObject(new String("a"), a,new byte[]{'h','e','l','l'} );

Button button1 = binding.button1;
button1.setOnClickListener(MainActivity.this);

text = binding.textView;
editText = binding.editTextText;
}
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
extern "C"
JNIEXPORT void JNICALL
Java_com_example_demo_MainActivity_onCreate(JNIEnv *env, jobject thiz,
jobject saved_instance_state) {
// TODO: implement onCreates()
// super.onCreate(savedInstanceState);
jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity");
jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state);


// binding = ActivityMainBinding.inflate(getLayoutInflater());
// getLayoutInflater()
jclass aClass = env->FindClass("android/app/Activity");
jmethodID getLayoutInflaterID =
env->GetMethodID(aClass, "getLayoutInflater", "()Landroid/view/LayoutInflater;");
// 这里需要的对象是那个对象调用这个方法,在Java中getLayoutInflater方法没有前缀,说明就是MainActivity调用了这个方法
jobject LayoutInflater = env->CallObjectMethod(thiz, getLayoutInflaterID);


// ActivityMainBinding.inflate()
jclass ActivityMainBindingClazz =
env->FindClass("com/example/demo/databinding/ActivityMainBinding");
jmethodID inflateID =
env->GetStaticMethodID(ActivityMainBindingClazz, "inflate",
"(Landroid/view/LayoutInflater;)Lcom/example/demo/databinding/ActivityMainBinding;");
jobject Binding = env->CallStaticObjectMethod(ActivityMainBindingClazz,
inflateID, LayoutInflater);



// setContentView(binding.getRoot());
jmethodID getRootID =
env->GetMethodID(ActivityMainBindingClazz, "getRoot",
"()Landroidx/constraintlayout/widget/ConstraintLayout;");
jobject ConstraintLayout = env->CallObjectMethod(Binding, getRootID);

jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID setContentViewID =
env->GetMethodID(AppCompatActivityClazz, "setContentView", "(Landroid/view/View;)V");
env->CallVoidMethod(thiz, setContentViewID, ConstraintLayout);



// TextView tv = binding.sampleText
// 注意这里,这个 sampleText 不是方法,而是字段,所以要使用 GetFileID
jfieldID sampleTextID =
env->GetFieldID(ActivityMainBindingClazz,
"sampleText", "Landroid/widget/TextView;");
jobject tv = env->GetObjectField(Binding, sampleTextID);



// String path = getPath(getApplicationContext()) + "/libdemo.so";
jclass ContextWrapperClazz = env->FindClass("android/content/ContextWrapper");
jmethodID getApplicationContextID =
env->GetMethodID(ContextWrapperClazz,
"getApplicationContext", "()Landroid/content/Context;");
jobject Context = env->CallObjectMethod(thiz, getApplicationContextID);

jclass MainActivityClazz = env->FindClass("com/example/demo/MainActivity");
jmethodID getPathID = env->GetMethodID(MainActivityClazz ,
"getPath", "(Landroid/content/Context;)Ljava/lang/String;");
jstring path = static_cast<jstring> (env->CallObjectMethod(thiz, getPathID, Context));
const char *Path = env->GetStringUTFChars(path, nullptr);
const char *So = "/libdemo.so";
char *result = new char[256];
strcpy(result, Path);
strcat(result, So);
jstring PathSo = env->NewStringUTF(result);


// tv.setText(stringFromJNI(path));

// 下面是为了执行 stringFromJNI(path) ,但是 stringFromJNI 是C的方法,干嘛去用C调用Java来调用C的方法呢,直接函数名调用就好了
// jmethodID stringFromJNIID =
// env->GetMethodID(MainActivityClazz,
// "stringFromJNI", "(Ljava/lang/String;)Ljava/lang/String;");
// jstring jstring1 = static_cast<jstring>
// (env->CallObjectMethod(thiz, stringFromJNIID, PathSo));

// tv.setText()
jclass TextViewID = env->FindClass("android/widget/TextView");
jmethodID setTextID =
env->GetMethodID(TextViewID, "setText", "(Ljava/lang/CharSequence;)V");
env->CallVoidMethod(tv, setTextID, Java_com_example_demo_MainActivity_stringFromJNI(env, thiz, PathSo));
LOGD("tv.setText(stringFromJNI(path));")



// String c = stringFromJNIobject(new String( original: "a"), a,new byte[]{'h','e','l','l});
jmethodID stringFromJNIObjectID =
env->GetMethodID(MainActivityClazz,
"stringFromJNIObject", "(Ljava/lang/String;I[B)Ljava/lang/String;");
jbyteArray a = env->NewByteArray(4);
jbyte bytes[] = {'h', 'e', 'l', 'l'};
jint myIntValue = 42;
jstring c = demoHelloWorld(env, thiz, env->NewStringUTF("a"), myIntValue, a);
LOGD("String c = stringFromJNIobject(new String( original: \"a\"), a,new byte[]{'h','e','l','l});")



// Button button1 = binding.button1;
jfieldID button1ID =
env->GetFieldID(ActivityMainBindingClazz, "button1", "Landroid/widget/Button;");
jobject button1 = env->GetObjectField(Binding, button1ID);
LOGD("Button button1 = binding.button1;")



// button1.setOnClickListener(MainActivity.this);
jclass ViewID = env->FindClass("android/view/View");
jmethodID setOnClickListenerID =
env->GetMethodID(ViewID,
"setOnClickListener", "(Landroid/view/View$OnClickListener;)V");
env->CallVoidMethod(button1, setOnClickListenerID, thiz);
LOGD("button1.setOnClickListener(MainActivity.this);")


// text = binding.textView;
// editText = binding.editTextText;
jfieldID textViewID =
env->GetFieldID(ActivityMainBindingClazz,
"textView", "Landroid/widget/TextView;");
jobject text = env->GetObjectField(Binding, textViewID);

jfieldID editTextTextID =
env->GetFieldID(ActivityMainBindingClazz,
"editTextText", "Landroid/widget/EditText;");
jobject editText = env->GetObjectField(Binding, editTextTextID);
LOGD("text = binding.textView;\n"
"editText = binding.editTextText;")
}

image-20241208210424182