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;
}
相关推荐
mmsx31 分钟前
android 登录界面编写
android·登录界面
姜毛毛-JYM31 分钟前
【JetPack】Navigation知识点总结
android
花生糖@2 小时前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
是程序喵呀2 小时前
MySQL备份
android·mysql·adb
casual_clover2 小时前
Android 之 List 简述
android·list
锋风Fengfeng3 小时前
安卓15预置第三方apk时签名报错问题解决
android
User_undefined4 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
程序员厉飞雨5 小时前
Android R8 耗时优化
android·java·前端
丘狸尾6 小时前
[cisco 模拟器] ftp服务器配置
android·运维·服务器
van叶~8 小时前
探索未来编程:仓颉语言的优雅设计与无限可能
android·java·数据库·仓颉