JNI 使用 Native 库时 load 与 loadLibrary 方法的区别 如 libA.so 和 libB.so 有依赖关系,如果选择 System.load(“/sdcard/path/libA.so”),即使 libB.so 也放在 /sdcard/path/ 路径下,load 方法还是会因为找不到依赖的 libB.so 文件而失败,因为虚拟机在载入 libA.so 的时候发现它依赖于 libB.so,那么会先去 java.library.path 下载入 libB.so,而 libB.so 并不位于 java.library.path 下,所以会报错。 解决的方案就是先 System.load(“/sdcard/path/libB.so”) 再 System.load(“/sdcard/path/libA.so”),但是这种方式不太靠谱,因为必须明确知道依赖关系; 另一种解决方案就是使用 System.loadLibrary(“A”),然后把 libA.so 和 libB.so 都放在 java.library.path 下即可,也就是我们开发调用时尽量用loadLibrary方式。

Java调用Native方法

native-lib 采用Android Studio 自动生成的cpp目录下的文件,配置都没改

静态注册规则

非静态方法,不推荐

  1. //1.MainActivity
  2. public class MainActivity extends AppCompatActivity {
  3. static {
  4. System.loadLibrary("native-lib");
  5. }
  6. public native String stringFromJNI();
  7. }
  8. //2.native-lib.cpp
  9. extern "C" JNIEXPORT jstring JNICALL
  10. Java_com_example_testcp_MainActivity_stringFromJNI(
  11. JNIEnv* env,
  12. jobject /* this */) {
  13. std::string hello = "Hello from C++";
  14. return env->NewStringUTF(hello.c_str());
  15. }

静态方法写法如下

  1. //1.MainActivity
  2. public class MainActivity extends AppCompatActivity {
  3. static {
  4. System.loadLibrary("native-lib");
  5. }
  6. public static native String stringFromJNI();
  7. }
  8. //2.native-lib.cpp
  9. extern "C" JNIEXPORT jstring JNICALL
  10. Java_com_example_testcp_MainActivity_stringFromJNI(
  11. JNIEnv* env,
  12. jclass args) {
  13. std::string hello = "Hello from C++";
  14. return env->NewStringUTF(hello.c_str());
  15. }

至于native-lib.cpp 对应方法怎么生成,可以利用这个指令对class文件进行操作

  1. cmd 进入到 src/main/java 目录下 执行以下命令
  2. javah -d ../jni 包名(如com.example.test.JniTest 回车
  3. 会生成一个jni文件夹下的.h文件(com_example_test_JniTest.h

JNIEXPORT 和 JNICALL 都是关键字,夹在 JNIEXPORT 和 JNICALL 宏中间的 jstring,表示函数的返回值类型,对应 Java 的String 类型

动态注册规则

为什么要动态注册,因为静态注册很繁琐,并且还要通过javah生成头文件或者自己写,方法名太长了。
对维护修改并不是很友好

  1. //1.java文件
  2. public class MainActivity extends AppCompatActivity {
  3. // Used to load the 'native-lib' library on application startup.
  4. static {
  5. System.loadLibrary("native-lib");
  6. }
  7. public native static void sayHello();
  8. }
  1. 引入android log,主要是用来看下运行打印的

CMakeLists 添加配置

  1. add_library( # Sets the name of the library.
  2. native-lib
  3. # Sets the library as a shared library.
  4. SHARED
  5. # Provides a relative path to your source file(s).
  6. native-lib.cpp tuya_log.h)
  1. log库配置 ```cpp

    include

    ifndef TESTCP_TEST_LOG_H

    define TESTCP_TEST_LOG_H

define LOGI(…) android_log_print(ANDROID_LOG_INFO, “========= Info ========= “, VAARGS_)

define LOGE(…) android_log_print(ANDROID_LOG_ERROR, “========= Error ========= “, VAARGS_)

define LOGD(…) android_log_print(ANDROID_LOG_INFO, “========= Debug ========= “, VAARGS_)

define LOGW(…) android_log_print(ANDROID_LOG_WARN, “========= Warn ========= “, VAARGS_)

endif //TESTCP_TEST_LOG_H

  1. 4. Native动态注册之前Java定义的方法
  2. 主要是利用JNI_OnLoad方法实现预注册,即当执行System.LoadLibrary()方法时候就把需要调用的方法给注册了
  3. ```cpp
  4. //定义native对应的方法
  5. static void sayHello(JNIEnv *env, jobject, jlong handle) {
  6. LOGI( "native: say hello ###");
  7. }
  8. //java方法和native方法的映射
  9. static JNINativeMethod gJni_Methods_table[] = {
  10. {"sayHello", "(J)V", (void*)sayHello},
  11. };
  12. static const char *className = "com/example/testcp/MainActivity";
  13. static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
  14. const JNINativeMethod* gMethods, int numMethods)
  15. {
  16. jclass clazz;
  17. LOGI("Registering %s natives\n", className);
  18. clazz = (env)->FindClass( className);
  19. if (clazz == NULL) {
  20. LOGE("Native registration unable to find class '%s'\n", className);
  21. return -1;
  22. }
  23. int result = 0;
  24. if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
  25. LOGE("RegisterNatives failed for '%s'\n", className);
  26. result = -1;
  27. }
  28. (env)->DeleteLocalRef(clazz);
  29. return result;
  30. }
  31. //关键是这个方法
  32. jint JNI_OnLoad(JavaVM* vm, void* reserved){
  33. LOGI("enter jni_onload");
  34. JNIEnv* env = NULL;
  35. jint result = -1;
  36. if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
  37. return result;
  38. }
  39. jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
  40. return JNI_VERSION_1_4;
  41. }

Native调用Java方法

//java类MainActivity中定义这个方法
public static void getVoidFromC(int id, String name){
     Log.e("JNI","test " + id +" name -> " + name);
 }

//c调用java方法

static JavaVM *j_vm = NULL;
static jclass globalCallback = NULL;

static void callJavaString(){
    JNIEnv *env;
    j_vm->AttachCurrentThread(&env, nullptr);
    jmethodID  methodId = (*env).GetStaticMethodID(globalCallback,"getVoidFromC","(ILjava/lang/String;)V");
    env->CallStaticVoidMethod(globalCallback,methodId,10,env->NewStringUTF("Native Test"));
}
//只是定义了globalCallback
static int jniRegisterNativeMethods(JNIEnv *env, const char *className,
                                    const JNINativeMethod *gMethods, int numMethods) {
    jclass clazz;

    LOGI("Registering %s natives\n", className);
    clazz = (env)->FindClass(className);
    if (clazz == NULL) {
        LOGE( "Native registration unable to find class '%s'\n", className);
        return -1;
    }
    globalCallback =  (jclass )env->NewGlobalRef(clazz);
     ......

    (env)->DeleteLocalRef(clazz);
    return result;
}

//代码和之前一样,只是加了j_vm赋值
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("enter jni_onload");

     ....

    jniRegisterNativeMethods(env, className, gJni_Methods_table,
                             sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
    j_vm = vm;

    return JNI_VERSION_1_4;
}

测试正常

JNI数据类型

类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double

除了booleanlong,其他都是首字母大写

JNI方法签名

由于Java支持方法重载,在JNI访问Java层方法时仅靠函数名是无法唯一确定一个方法,因此JNI提供了一套签名规则(如:Z、B、[Z等),用一个字符串来唯一确定一个方法,其规则:(参数1类型签名参数2类型签名…)返回值类型签名,比如Java方法long getDeviceId(int n, String s, int[] arr)long getDeviceId(int n)的类型签名分别为(ILjava/lang/String;[I)J(I)J

  1. 基本类型:见上表
  2. 如果是类的类型:L+类全名,类名中的.用/代替,比如java.lang.String就是Ljava/lang/String;
  3. 如果是数组类型:则在前面加上 然后加类型签名,几位数组就加几个,比如int[]->[Iboolean[][]->[[Zjava.lang.Class[] -> [Ljava/lang/Class; 可以通过javap -s来打印该方法的签名

    签名都是 (方法)返回值

    JNIEnv和JavaVM的区别

    JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。
    5713484-4383ea9f680216b9.webp
    总结下来即:

  • JavaVM:JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个
  • JNIEnv:JavaVM 在线程中的代码,每个线程都有一个,JNI可能有非常多个JNIEnv;

所以JNIEnv的作用也比较清楚了

  • 调用Java 函数:JNIEnv代表了Java执行环境,能够使用JNIEnv调用Java中的代码
  • 操作Java代码:Java对象传入JNI层就是jobject对象,需要使用JNIEnv来操作这个Java对象

那么JNIEnv如何创建?

//c++写法 c也类似
JNIEnv *env;
j_vm->AttachCurrentThread(&env, nullptr);

如何销毁呢?

j_vm->DetachCurrentThread();

上述的j_vm就是 JavaVM,在加载库的时候会触发onLoad方法

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    ....
    j_vm = vm;
}

参考资料
https://juejin.cn/post/6844904002405007373
https://juejin.cn/post/7028214429216931847