Android JNI 实践基础(一)

Gradle 基本结构以及主要配置

简单来说 就是指定 一下cmake文件的位置,以及 你要编译的cpp版本,和abi版本

大多数情况 gradle配置就关注这2个即可

Cmake重要配置

介绍一些重要的,其余多数百度一下 即可

scss 复制代码
# 这里是打印出来的一些 cmake 路径信息,方便后面引入外部库使用 比如可以 /.. 这样指定外部路径
message(${CMAKE_CURRENT_LIST_FILE})
message(${CMAKE_CURRENT_LIST_DIR})

下面这个配置很有用,避免你写 "" 引入了,可以直接 尖括号引入 很方便

bash 复制代码
include_directories(${CMAKE_SOURCE_DIR}/base/)

1个module 可以配置多个so库的

根据你gradle版本的不同, 可以在 对应的build目录下找到对应的名字

动态库固定都是lib开头 后面的就是你配置的名称了

target_link_libraries 配置可以有多个

bash 复制代码
target_link_libraries(${CMAKE_PROJECT_NAME}
        people-lib
        # List libraries link to the target library
        android
        log)

target_link_libraries(dynamic-lib
        # List libraries link to the target library
        android
        log)

这里要注意的是很多新手会搞错,以为一个target_link_libraries 可以写多个 addLibrary中的库,这样会导致 错误 ,比如上面的dynamic-lib 你要是放到 people-lib下 就会导致dynamic-lib 拿不到 log 库的。这点一定要注意

静态注册JNI方法

可以看下kotlin的写法

对应的cpp写法

这种写法其实现在不用记了,因为 as 已经足够智能,让他自动生成就好

动态注册方法

相比于静态注册jni方法,动态注册jni方法 更加方便,下面完整的介绍一下 如何实现 动态注册jni方法

为了调试方便,我们可以首先 新建一个base.h 文件,封装一下 c++的log 方法

c 复制代码
#include <jni.h>
#include <android/log.h>
#ifndef NDKTEST_BASE_H
#define NDKTEST_BASE_H

#define LOG_TAG "kkk"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)


#endif //NDKTEST_BASE_H

然后新建一个cpp文件 首先实现一个方法

动态库在加载的时候 会回调到这个方法里 所以我们只要 在这个方法里 实现我们动态注册jni方法 就可以了

JNI_OnLoad 写法是固定的

c 复制代码
// 动态库被系统加载时 会自动回调这个方法
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGE("jni onload call");
 
    return JNI_VERSION_1_6;
}

java端上 ,我们实现一个类

c 复制代码
public class JNIDynamicLoad {
    static {
        System.loadLibrary("dynamic-lib");
    }

    public native int sum(int x,int y);
    public native String getNativeString();
}

在ide中会提示你红字,但其实不用管,我们既然是动态注册,就完全不用理会ide给你的提示

我们可以首先实现一下 这2个方法

注意这2个方法的返回值不要搞错了,必须得是j开头的,千万不要想当然惯性写成string和int了

c 复制代码
// jni方法的前2个参数 永远固定 就是这2个类型
jstring getMessage(JNIEnv *env, jobject job) {
    return env->NewStringUTF("this is msg");
}

jint plus(JNIEnv *env, jobject job, int x,int y) {
    return x + y;
}

另外我们还要定义一下 java中的2个jni方法

这里其实能看出来 就是在这个数组中 我们建立了 java方法和c++方法的关联关系

c 复制代码
static JNINativeMethod gMethods[] = {
        {"getNativeString", "()Ljava/lang/String;", (void *) getMessage},
        {"sum",             "(II)I",                (void *) plus},
};

这其中第二个参数可能会有人觉得奇怪,但是有过字节码经验的人一看就知道 这个就是方法的字节码描述而已

对于不熟悉字节码的同学来说 也没关系,我们下载一个插件 jclasslib即可

通过插件 一样可以拿到关键的方法签名信息

剩下的就是完整实现一下我们的方法

c 复制代码
int registerNativeMethods(JNIEnv *env, const char *name,
                          JNINativeMethod *methods, jint nMethods) {
    jclass jcls;
    // 查一看 能否找到这个类
    jcls = env->FindClass(name);
    if (jcls == nullptr) {
        return JNI_FALSE;
    }
    // 如果找到这个类了,就动态注册jni方法
    if (env->RegisterNatives(jcls, methods, nMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

// 动态库被系统加载时 会自动回调这个方法
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGE("jni onload call");
    JNIEnv *env;
    ```
    // 获取java虚拟机环境
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_FALSE;
    }
    registerNativeMethods(env, JAVA_CLASS, gMethods, 2);
    return JNI_VERSION_1_6;
}

这里其实关键的就是理解 RegisterNatives 这个方法,

第一个参数就是传入你java的类,就是哪个类 load了你的动态库 第二个参数 就是java类中的 native方法 ,是一个数组, 第三个参数 就是你要动态注册几个方法

JNI中的字符串操作

基本类型操作直接跳过 没啥可说的,字符串稍微复杂一些,

定义一个java方法

arduino 复制代码
public native String callNativeString(String str);

看下对应的cpp 实现

c 复制代码
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndktest_JNIBasicType_callNativeString(JNIEnv *env, jobject thiz, jstring str) {
    // java 一般都是utf的编码 非基本类型 要注意内存泄漏 一定要有release和get 配对
    const char *cstr = env->GetStringUTFChars(str, JNI_FALSE);
    LOGD("java string is %s", cstr);
    // release 千万不要忘记 使用完毕 以后一定记得释放内存
    env->ReleaseStringUTFChars(str, cstr);
    return env->NewStringUTF("this is C style ");

}
```c
当然也可以在jni中处理字符串

定义一个简单的jni方法

public native void stringMethod(String str);

scss 复制代码
实现对应的c++方法,其实就是 截取一下传来的字符串

注意内存回收即可

```c
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_stringMethod(JNIEnv *env, jobject thiz, jstring str) {
    const char *cstr = env->GetStringUTFChars(str, 0);
    LOGD("java string: %s", cstr);
    jsize utfLen = env->GetStringUTFLength(str);
    LOGD("utf-length : %d", utfLen);
    // 返回的是java字符串的长度
    int len = env->GetStringLength(str);
    LOGD("length %d", len);
    // 想截取的字符串
    int length = len - 3;
    jchar buf[length];
    env->GetStringRegion(str, 0, length, buf);
    jstring lstr = env->NewString(buf, length);
    const char *cstr2 = env->GetStringUTFChars(lstr, 0);
    LOGD("region str: %s", cstr2);
    env->ReleaseStringUTFChars(lstr, cstr2);
    env->ReleaseStringUTFChars(str, cstr);


}

可以看下运行结果

数组类型

c 复制代码
public native String callNativeStringArray(String[] strArray);
c 复制代码
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndktest_JNIBasicType_callNativeStringArray(JNIEnv *env, jobject thiz,
                                                            jobjectArray str_array) {
    int len = env->GetArrayLength(str_array);
    LOGD("len is %d", len);
//    env->GetIntArrayElements() 基础类型 选择对应的就好
//  对象类型
    jobject jo = env->GetObjectArrayElement(str_array, 0);
    // jstring 无法直接使用
    jstring firstStr = static_cast<jstring>(jo);
    const char *str = env->GetStringUTFChars(firstStr,0);
    LOGD("first str is %s", str);
    env->ReleaseStringUTFChars(firstStr,str);
}

jni 方法 java类字段

kotlin 复制代码
class Person(var name: String, var age: Int) {
    companion object {
        var score = 5
    }
}

看看如何修改Person对象的值,以及 Person的静态变量的值

c 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_changeObjectFieldValue(JNIEnv *env, jobject thiz,
                                                             jobject person) {
    jclass cls = env->GetObjectClass(person);

    jfieldID fid = env->GetFieldID(cls, "name", "Ljava/lang/String;");
    jstring str = env->NewStringUTF("lisa");
    env->SetObjectField(person, fid, str);

    jfieldID sfid = env->GetStaticFieldID(cls, "score", "I");
    // 这里的第一个参数 就是jclass 而不是上面field中的jboject了
    int num = env->GetStaticIntField(cls, sfid);
    env->SetStaticIntField(cls, sfid, ++num);


}

再看下 如何在cpp中调用java的代码

新增一个方法

kotlin 复制代码
fun callFromNative(num: Int) {
    age = num
}

在jni中如何调用

ini 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIBasicType_callJavaMethodFromNative(JNIEnv *env, jobject thiz,
                                                               jobject person) {
    jclass cls = env->GetObjectClass(person);
    jmethodID mid = env->GetMethodID(cls, "callFromNative", "(I)V");
    env->CallVoidMethod(person, mid, 2);
}

子线程 访问方法

在子县城调用方法很常用,毕竟不能阻塞ui主线程的工作

java 复制代码
public native void nativeThreadCallback(IThreadCallback callback);

interface IThreadCallback{
    void callBack();
}

然后我们看下 调用他的方式

bash 复制代码
jmethod.nativeThreadCallback {
    Log.v(
        "vivo",
        "Thread name is ${Thread.currentThread().name}",
    )
}

其实就是 通过jni方法 来执行这个callback回调,并行执行的时候 一定要在独立线程中执行,不能在默认的主线程中执行

考虑到我们是开线程工作 所以 我们为了方便 定义一组静态变量

c 复制代码
// 方便在子线程中使用
static jclass jc;
static jobject threadObject;
static jmethodID jm;

这里我们能看出来 所谓回调方法 其实也就是一个普通的类中的一个方法,仅此而已

c 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndktest_JNIInvokeMethod_nativeThreadCallback(JNIEnv *env, jobject thiz,
                                                              jobject callback) {
    // 这里是全局引用, 且要注意 env 是不可以跨线程使用的
    threadObject = env->NewGlobalRef(callback);
    jc = env->GetObjectClass(callback);
    jm = env->GetMethodID(jc, "callBack", "()V");
    pthread_t handle;
    pthread_create(&handle, nullptr, threadCallback, nullptr);
}

这里要注意了 JNIEnv是不可以跨线程传递的, 那咋办呢? 还记得之前的

JNI_OnLoad 方法吗吗,我们可以 通过这个方法的JavaVM 参数来在子线程中获取 JNIEnv

c 复制代码
static JavaVM *gvm = nullptr;

JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
    
gvm = vm;

}

最后我们来实现一下这个方法即可

c 复制代码
void *threadCallback(void *) {
    JNIEnv *env = nullptr;
    if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
        env->CallVoidMethod(threadObject,jm);
        gvm->DetachCurrentThread();
    }
    return 0;
}

最后看下日志

明显可以看出来,使用pthread_t 创建的线程执行的 已经不在主线程了, 而不在主县城执行的,依旧在main线程执行

Jni 方法java类构造方法

Java 复制代码
public native Person invokePersonConstructors();
public native Person allocObjectConstructors();

给出两种实现方式, 个人觉得第一种方式更好理解一些

c 复制代码
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndktest_JNIBasicType_invokePersonConstructors(JNIEnv *env, jobject thiz) {
    jclass cls = env->FindClass("com/example/ndktest/Person");
    jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
    jstring str = env->NewStringUTF("vivo");
    return env->NewObject(cls, mid, str, 100);

}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndktest_JNIBasicType_allocObjectConstructors(JNIEnv *env, jobject thiz) {

    jclass cls = env->FindClass("com/example/ndktest/Person");
    jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
    jstring str = env->NewStringUTF("xiaomi");

    jobject person = env->AllocObject(cls);
    env->CallNonvirtualVoidMethod(person, cls, mid, str, 10000);
    return person;
}
相关推荐
liang_jy2 小时前
Android 事件分发机制(二)—— 点击事件透传
android·面试·源码
圆号本昊5 小时前
Flutter Android Live2D 2026 实战:模型加载 + 集成渲染 + 显示全流程 + 10 个核心坑( OpenGL )
android·flutter·live2d
冬奇Lab6 小时前
ANR实战分析:一次audioserver死锁引发的系统级故障排查
android·性能优化·debug
冬奇Lab6 小时前
Android车机卡顿案例剖析:从Binder耗尽到单例缺失的深度排查
android·性能优化·debug
ZHANG13HAO6 小时前
调用脚本实现 App 自动升级(无需无感、允许进程中断)
android
圆号本昊7 小时前
【2025最新】Flutter 加载显示 Live2D 角色,实战与踩坑全链路分享
android·flutter
小曹要微笑8 小时前
MySQL的TRIM函数
android·数据库·mysql
mrsyf9 小时前
Android Studio Otter 2(2025.2.2版本)安装和Gradle配置
android·ide·android studio
DB虚空行者9 小时前
MySQL恢复之Binlog格式详解
android·数据库·mysql