NDK开发基础
目录
什么是NDK
NDK(Native Development Kit):原生开发套件,是一套工具,作用是能够在安卓应用中使用C和C++代码,并提供众多的平台库,可使用这些平台库管理原生activity和访问实体设备组件。
如需为您的应用编译和调试原生代码,您需要以下组件:
- Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。
- CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。
- LLDB:Android Studio 用于调试原生代码的调试程序。
为什么需要NDK
- 进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。
- 重复使用您自己或其他开发者的 C 或 C++ 库。
什么是JNI
Java 原生接口 (JNI):JNI 是 Java 和 C++ 组件用于相互通信的接口。
第一个NDK工程
SDK Manager中选在NDK和Cmake进行安装
file —> New Project —> Natie C++ —> Next
根据实际需求填写对应的信息:工程名字、包名、保存路径、开发语言、兼容的最小sdk
选在C++版本后点击Finish
一个默认的NDK工程创建好后,主要包括以下几个部分:
build.gradle中和NDK相关:
build.gradle
文件用于配置Android Studio项目的构建过程
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
| / /这里指定了用于构建Android应用程序的Gradle插件。 plugins { id 'com.android.application' }
android { namespace 'com.example.projectname' compileSdk 33
defaultConfig { applicationId "com.example.projectname" minSdk 16 targetSdk 33 versionCode 1 versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags '-std=c++11' } ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' }
} }
buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.22.1' } } buildFeatures { viewBinding true } }
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' }
|
CMakeLists:可以在这个文件中指定编译后的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
| # 设置该CMake文件所需的最低版本,这里是3.22.1 cmake_minimum_required(VERSION 3.22.1)
# 定义一个CMake项目,并给它起一个名字"projectname" project("projectname")
# 创建一个名为"projectname"的共享库(SHARED),源代码是native-lib.cpp # SHARED表示这是一个动态链接库,也就是.so文件(Shared Object) add_library( projectname # 库的名称 SHARED # 库的类型:SHARED(动态库)、STATIC(静态库)或者MODULE(模块库) native-lib.cpp # 源文件 )
# find_library用于在系统中查找一个库,并将其路径存储在变量中 # 这里查找的是Android系统的日志库(liblog.so),并将其路径存储在log-lib变量中 find_library( log-lib # 变量名称 log # 要查找的库名称 )
# target_link_libraries用于链接库到目标,这里把上面找到的日志库链接到我们自己的"projectname"库中 # 这样在native-lib.cpp中就可以使用Android的日志功能 target_link_libraries( projectname # 我们自己的库 ${log-lib} # Android系统的日志库 )
|
native-lib.cpp
1 2 3 4 5 6 7 8 9 10 11 12
| #include <jni.h> #include <string>
extern "C" JNIEXPORT jstring JNICALL Java_com_example_projectname_MainActivity_stringFromJNI( JNIEnv* env, jobject ) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
|
要在java中调用上面的c++方法,需要在对应的java代码中加载对应的so,以及声明native方法
1 2 3 4 5 6
| public native String stringFromJNI();
static { System.loadLibrary("projectname"); }
|
so中的log输出
1 2 3 4 5 6 7
| #include <android/log.h>
#define TAG "sectest" #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__);
|
#define中的…表示接受可变参数,传入的可变参数替换到对应的 __VA_ARGS__处,用法如上。
NDK多线程pthread
头文件引入
线程函数
定义一个线程将要执行的函数。
1 2 3 4 5
| void* myThread(void* arg) { return nullptr; }
|
创建和启动线程
1 2 3
| pthread_t thread; int result = pthread_create(&thread, NULL, myThread, NULL);
|
不需要手动给 thread
赋值。它的值会在 pthread_create
成功创建新线程后被自动设置。这个值通常会被用于其他 pthread 函数,如 pthread_join(thread, NULL);
,以便进行后续的线程管理。
所以,虽然在声明 pthread_t thread;
时没有显式地进行初始化或赋值,但它会在 pthread_create
调用成功后包含新线程的标识符。这个标识符是由系统自动生成和管理的。
等待线程结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| pthread_join(thread, NULL); #include <pthread.h> #include <android/log.h>
void* myThread(void* arg) { __android_log_print(ANDROID_LOG_INFO, "MyApp", "Hello from pthread"); return nullptr; }
extern "C" JNIEXPORT void JNICALL Java_com_example_myapp_MainActivity_startThread(JNIEnv*, jobject) { pthread_t thread; pthread_create(&thread, NULL, myThread, NULL); pthread_join(thread, NULL); }
|
JNI_OnLoad
JNI_OnLoad
是 JNI (Java Native Interface) 中的一个特殊函数,它会在 native 库被加载时执行。如果你在 native 库中定义了 JNI_OnLoad
,当 Java 代码使用 System.loadLibrary
或 Runtime.loadLibrary
加载这个库时,JNI_OnLoad
会被调用。
此函数为开发者提供了一个机会在库被加载时执行一些初始化操作,比如注册 native 方法、初始化 native 数据结构等。
1 2 3 4 5 6 7 8 9
| 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; } return JNI_VERSION_1_6; }
|
一个so中可以不定义JNI_OnLoad
一旦定义了JNI_OnLoad,在so被加载的时候会自动执行
必须返回JNI版本JNI_VERSION_1_6
JavaVM* vm
:是一个指向 Java 虚拟机的指针,可以使用它来获取 JNIEnv
,这是与 JVM 进行交互的主要工具。
void* reserved
:是一个预留的参数。目前,通常不需要担心这个参数。
- 函数的返回值是native 代码所支持的 JNI 版本。在上面的例子中,我们返回了
JNI_VERSION_1_6
,表示我们的 native 代码是基于 JNI 1.6 的。
JNIEnv
JNIEnv:java native interface 。这是一个指向结构体的指针,这个结构体包含了大量的函数指针,这些函数提供了jni的大部分功能。
JNIEnv为native层提供了与java虚拟机交互的能力,使得本地代码能够操作java对象、调用java方法、捕获java异常等。
JNIEnv指针的获取或使用场景
1)静态注册调用
当java调用静态注册的native方法时,JVM会自动将JNIEnv指针作为第一个参数传递给本地方法
native方法:
1 2 3 4 5 6
| JNIEXPORT void JNICALL Java_com_example_javaandso_MainActivity_stringFromJNI(JNIEnv *env, jobject obj) { jclass clazz = env->FindClass("java/lang/String"); }
|
java层调用:
2)在JNI_OnLoad中获取
当so库被加载时,JNI_OnLoad
函数会被自动调用,其中也可以获取JNIEnv
指针:
1 2 3 4 5 6 7 8 9 10
| JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } return JNI_VERSION_1_6; }
|
3) 在子线程中获取
先在JNI_OnLoad中获取JavaVM指针保存在一个全局变量中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| JavaVM* globalVM;
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { LOGD("this is form JNI_OnLoad");
JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { LOGD("GetEnv failed"); return -1; }
LOGD("JavaVM %p", vm); env->GetJavaVM(&globalVM); LOGD("JavaVM %p", globalVM); LOGD("JNI_OnLoad JNIEnv %p", env);
return JNI_VERSION_1_6; }
|
再在子线程中通过JavaVM指针获取JNIEnv指针
1 2 3 4 5 6 7 8 9 10 11
| void myThread(){
JNIEnv *env = nullptr; if (globalVM->AttachCurrentThread((JNIEnv **) &env, nullptr) != JNI_OK) { LOGD("GetEnv failed"); } LOGD("myThread JNIEnv %p", env);
LOGD("this is from myThread"); }
|
JNI函数静态注册
在静态注册中,C/C++函数的名称需要遵循一定的命名规则,格式为Java_包名_类名_方法名
。
系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用 静态注册的jni函数,必然在导出表里
1 2 3 4 5 6 7 8
| public class NativeClass { static { System.loadLibrary("native-lib"); } public native String nativeMethod(); }
|
C/C++代码示例:
1 2 3 4 5 6 7 8
| #include <jni.h>
extern "C" JNIEXPORT jstring JNICALL Java_com_example_NativeClass_nativeMethod(JNIEnv *env, jobject obj) { return env->NewStringUTF("Hello from JNI!"); }
|
JNI函数动态注册
通过env→RegisterNatives注册函数,通常在JNI_OnLoad中注册JNINativeMethod
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
| #include <jni.h>
static jstring nativeMethod(JNIEnv *env, jobject obj) { return (*env)->NewStringUTF(env, "Hello from JNI!"); }
static JNINativeMethod methods[] = { {"nativeMethod", "()Ljava/lang/String;", (void *)nativeMethod}, };
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass clazz = (*env)->FindClass(env, "com/example/NativeClass"); if (clazz == NULL) { return JNI_ERR; } if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) { return JNI_ERR; } return JNI_VERSION_1_6; }
|
下面是对这段代码中使用的各种方法及参数的详细解释:
1. JNI_OnLoad
1 2 3 4 5 6
| JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_VERSION_1_6; }
|
JNI_OnLoad
是JNI库加载时被调用的函数,通常在这个函数中进行动态注册。
JavaVM *vm
:这是Java虚拟机的指针,可以用于获取JNIEnv
指针或者在其他线程中使用。
- 返回值:该函数返回你的JNI库所需的JNI版本号,通常返回
JNI_VERSION_1_6
。
2. GetEnv
1 2 3 4 5
| JNIEnv *env; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }
|
GetEnv
函数用于获取与当前线程关联的JNIEnv
指针。
JNIEnv **env
:用于存储获取到的JNIEnv
指针的地址。
JNI_VERSION_1_6
:表示所需的JNI版本号。
- 如果函数返回
JNI_OK
,则获取JNIEnv
指针成功,否则返回JNI_ERR
。
3. FindClass
1 2 3 4 5
| jclass clazz = (*env)->FindClass(env, "com/example/NativeClass"); if (clazz == NULL) { return JNI_ERR; }
|
FindClass
函数用于查找Java类。
"com/example/NativeClass"
:要查找的Java类的完全限定名。
- 如果找到了类,则返回类的引用;如果没找到,则返回NULL。
4. RegisterNatives
1 2 3 4 5 6 7 8 9 10 11 12
| static jstring nativeMethod(JNIEnv *env, jobject obj) { return (*env)->NewStringUTF(env, "Hello from JNI!"); }
static JNINativeMethod methods[] = { {"nativeMethod", "()Ljava/lang/String;", (void *)nativeMethod}, };
if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) { return JNI_ERR; }
|
RegisterNatives
函数用于注册本地方法。
jclass clazz
:要注册本地方法的Java类的引用。
JNINativeMethod methods[]
:一个数组,包含要注册的本地方法的信息。每个JNINativeMethod
结构体包含三个字段:Java方法名、方法签名和C/C++函数指针。
sizeof(methods) / sizeof(methods[0])
:计算数组中的元素个数,即要注册的本地方法的个数。
- 如果注册成功,则函数返回0;如果注册失败,则返回一个负数。
5. NewStringUTF
1 2
| return (*env)->NewStringUTF(env, "Hello from JNI!");
|
NewStringUTF
函数用于创建一个新的Java字符串对象。
"Hello from JNI!"
:C字符串,表示要创建的Java字符串的内容。
这段代码的整体流程是:在JNI_OnLoad
函数中获取JNIEnv
指针,查找Java类,然后使用RegisterNatives
函数注册本地方法。如果在任何步骤中出现错误,函数会返回JNI_ERR
。如果一切正常,则返回所需的JNI版本号。
so路径的动态获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public String getPath(Context cxt){ PackageManager pm = cxt.getPackageManager(); List<PackageInfo> pkgList = pm.getInstalledPackages(0); if (pkgList == null || pkgList.size() == 0) return null; for (PackageInfo pi : pkgList) { if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/") && pi.packageName.startsWith("com.xiaojianbang.demo")) { return pi.applicationInfo.nativeLibraryDir; } } return null; }
|
多个cpp文件编译成一个so
CMakeLists.txt配置文件中的add_library添加多个cpp文件名
代码运行结果:
so编译结果
so反编译结果
编译多个so
编写多个cpp文件
修改CMakeLists.txt
Java静态代码块加载多个so
编译结果
so之间的相互调用
使用dlopen、dlsym、dlclose获取函数地址,然后调用。需要导入dlfcn.h
extern 函数声明要调用的方法,避免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 25
| #include <jni.h> #include <string> #include <dlfcn.h>
extern "C" JNIEXPORT jstring JNICALL Java_com_example_sotest_MainActivity_stringFromJNI( JNIEnv* env, jobject ) { char* hello = "Hello from C++";
void* handle = dlopen("libtestcppso.so", RTLD_LAZY);
if (handle) { void (*runtestcpp)(char*) = reinterpret_cast<void (*)(char *)> (dlsym(handle, "runtestcpp")); if (runtestcpp) { runtestcpp(hello); } dlclose(handle); }
return env->NewStringUTF(hello); }
|
运行结果:
JNI创建java对象
使用NewObject创建对象
➡️ 功能:NewObject不仅分配对象内存,还调用对象的构造函数。
➡️ 用法:
1 2 3 4
| jobject NewObject( jclass clazz, jmethodID methodID, ...);
|
➡️** 参数:**
clazz:要创建的Java类的引用。
methodID:构造函数的ID。
…:构造函数的参数。
➡️ 返回值:
返回一个新创建的Java对象。
➡️ C/C++代码示例:
1 2 3 4 5 6
| jclass clazz = env ->FindClass("com/example/sotest/NdkDemo"); jmethodID methdID = env ->GetMethodID(clazz,"<init>", "()V");
jobject javademoObj = env ->NewObject(clazz,methdID); LOGD("通过jni创建java对象success");
|
用于创建对象的java类
1 2 3 4 5 6 7 8 9
| package com.example.sotest;
import android.util.Log;
public class NdkDemo { public NdkDemo(){ Log.d("wnxwzr", "NdkDemo() running !Call by native code"); } }
|
➡️ 代码运行结果:
使用AllocObject创建对象
👉🏿 功能:AllocObject只分配对象内存,但不调用构造函数。
👉🏿 用法:
1 2 3 4
| jobject AllocObject(jclass clazz);
|
👉🏿 参数:
clazz:要创建的Java类的引用。
👉🏿 返回值:
返回一个未初始化的Java对象。
需要手动初始化对象或避免调用构造函数的情况
1 2 3 4 5 6 7 8 9 10 11
| jclass clazz = env ->FindClass("com/example/sotest/NdkDemo");
jmethodID methdID2 = env ->GetMethodID(clazz,"<init>", "(Ljava/lang/String;)V"); jobject javademoObj2 = env->AllocObject(clazz);
jstring jstr = env->NewStringUTF("from jni str CallNonvirtualVoidMethod");
env->CallNonvirtualVoidMethod(javademoObj2, clazz, methdID2, jstr);
|
1 2 3 4 5 6 7 8 9 10
| public class NdkDemo { public NdkDemo(){ Log.d("wnxwzr", "NdkDemo() running !Call by native code"); }
public NdkDemo(String who){ Log.d("wnxwzr", "NdkDemo(String) running !Call by native code"+who); } }
|
JNI访问java属性
获取静态字段
1 2 3 4 5 6 7
| jclass clazz = env ->FindClass("com/wnxwzr/sotest1/MainActivity"); jfieldID privateStaticStringField = env->GetStaticFieldID(clazz, "privateStaticStringField", "Ljava/lang/String;"); jstring privateStaticString = static_cast<jstring>(env->GetStaticObjectField(clazz, privateStaticStringField)); const char* privatecstr = env->GetStringUTFChars(privateStaticString, nullptr); LOGD("privateStaticString: %s", privatecstr); env->ReleaseStringUTFChars(privateStaticString, privatecstr);
|
获取对象字段
ReflectDemoObj对象可以在native中本地创建,也可以通过从java层获取对象传递过来。
1 2 3 4 5 6 7
| jfieldID publicStringField = env->GetFieldID(clazz, "publicStringField", "Ljava/lang/String;"); jstring publicString = static_cast<jstring>(env->GetObjectField(ReflectDemoObj, publicStringField)); const char* publiccstr = env->GetStringUTFChars(publicString, nullptr); LOGD("publicStringField: %s", publiccstr); env->ReleaseStringUTFChars(publicString, publiccstr);
|
设置字段
1 2
| env->SetObjectField(ndkobj, privateStringFieldID, env->NewStringUTF("xxxxx"));
|
通过JNI访问java数组
获取数组字段id
1 2 3 4 5
| jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B"); jbyteArray byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID)); int _byteArrayLength = env->GetArrayLength(byteArray);
|
修改数组字段
1 2 3 4 5 6 7
| 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(byteArray, 0, _byteArrayLength, java_array);
|
获取数组字段
1 2 3 4 5 6 7 8
| byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID)); _byteArrayLength = env->GetArrayLength(byteArray); char* str = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr)); for(int i = 0; i< _byteArrayLength; i++){ LOGD("str[%d]=%d", i, str[i]); } env->ReleaseByteArrayElements(jbyteArray, reinterpret_cast<jbyte *>(cbyteArray), 0);
|
通过JNI访问java方法
调用静态函数
1 2 3 4
| jclass ReflectDemoClazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo"); jmethodID publicStaticFuncID = env->GetStaticMethodID(ReflectDemoClazz, "publicStaticFunc", "()V"); env->CallStaticVoidMethod(ReflectDemoClazz, publicStaticFuncID);
|
调用对象函数
1 2 3 4 5 6 7
| jmethodID privateFuncID = env->GetMethodID(ReflectDemoClazz, "privateFunc", "(Ljava/lang/String;I)Ljava/lang/String;"); jmethodID ReflectDemoInit = env->GetMethodID(ReflectDemoClazz, "<init>", "(Ljava/lang/String;)V"); jstring str1 = env->NewStringUTF("this is from NDK"); jobject ReflectDemoObj = env->NewObject(ReflectDemoClazz, ReflectDemoInit, str1); jstring str2 = env->NewStringUTF("this is from JNI"); jstring retval = static_cast<jstring>(env->CallObjectMethod(ReflectDemoObj, privateFuncID, str2, 1000));
|
CallObjectMethodA的使用
1 2 3 4 5 6 7 8
| jvalue args[2]; args[0].l = str2; args[1].i = 1000; jstring retval = static_cast<jstring>(env->CallObjectMethodA(ReflectDemoObj, privateFuncID, args)); const char* cpp_retval = env->GetStringUTFChars(retval, nullptr); LOGD("cpp_retval: %s", cpp_retval); env->ReleaseStringUTFChars(retval, cpp_retval);
|
参数说明:
env
: 指向JNI环境的指针,用于访问JNI接口。
obj
: 要调用方法的Java对象。
methodID
: 要调用的方法的ID,可以通过GetMethodID
或GetStaticMethodID
获取。
args
: 指向一个jvalue
数组的指针,该数组包含要传递给方法的参数。
返回值:
- 返回调用的Java方法的结果,如果方法的返回类型是对象,则返回一个
jobject
;如果方法是void类型,则返回NULL。
CallObjectMethodA
与CallObjectMethod
的区别在于,CallObjectMethodA
允许您通过jvalue
数组传递参数,这在处理可变参数列表时特别有用。
参数是数组,返回值是数组的函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| jclass StringClazz = env->FindClass("java/lang/String"); jobjectArray StringArr = env->NewObjectArray(3, StringClazz, nullptr); for(int i = 0; i < 3; i++){ jstring str3 = env->NewStringUTF("NDK"); env->SetObjectArrayElement(StringArr, i, str3); } jmethodID privateStaticFuncID = env->GetStaticMethodID(ReflectDemoClazz, "privateStaticFunc", "([Ljava/lang/String;)[I"); jintArray intArr = static_cast<jintArray>(env->CallStaticObjectMethod(ReflectDemoClazz, privateStaticFuncID, StringArr)); int *cintArr = env->GetIntArrayElements(intArr, nullptr); LOGD("cintArr[0]=%d", cintArr[0]); env->ReleaseIntArrayElements(intArr, cintArr, JNI_ABORT);
|
通过JNI访问java父类
1 2 3 4 5
| jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity"); jmethodID onCreateID = env->GetMethodID(AppCompatActivityClazz, "onCreate", "(Landroid/os/Bundle;)V"); env->CallNonvirtualVoidMethod(thiz, AppCompatActivityClazz, onCreateID, saved_instance_state);
|