JNI 和 NDK 编程

JNI 全称 Java Native Interface,它是为了方便 Java 调用 C、C++ 等本地代码所封装的一层接口。

NDK 是 Android 所提供的一个工具集合,通过 NDK 可以在 Android 中更加方便的用过 JNI 来访问本地代码。此外 NDK 还提供了交叉编译器,只需简单的配置即可生成特定 CPU 平台的动态库。

使用 NDK 的好处:

  1. 提高代码的安全性
  2. 可以很方便地使用目前已有的 C/C++ 开源库
  3. 便于平台间移植
  4. 提高程序在某些特定情形下的执行效率

JNI 的开发流程

  1. 在 Java 中声明 native 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class JniTest {

    static {
    System.loadLibrary("jni-test"); // 加载动态库,库文件完整名为 libjni-test.so
    }
    ...
    public native String get();

    public native void set(String str);
    }
  2. 编译 Java 源文件得到 class 文件,然后通过 javah 命令导出 JNI 头文件。

    1
    2
    javac .\com\liyu\JniTest.java
    javah com.liyu.JniTest

    在当前目录自动产生一个 com_liyu_JniTest.h 的头文件。

  3. 实现 JNI 方法
  4. 编译 so 库并在 Java 中调用

NDK 的开发流程

本流程结合书籍和网络资料,当然也可以直接通过 Android Studio 新建 Project 时勾选 Include C++ support,这时 Android Studio 会自动生成一个 JNI 示例项目。

  1. 下载并配置 NDK
  2. 创建一个 Android 项目,并声明所需的 native 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.example.liyu.studytest;

    public class JniTest {

    static {
    System.loadLibrary("jni-test");
    }

    public native String get();

    public native void set(String str);

    }
  3. 右键 app -> New -> Folder -> JNI Folder 创建 jni 目录

  4. 通过 javah 命令生成头文件至 jni 目录

    1
    javah -d src/main/jni/ -classpath build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/ com.example.liyu.studytest.JniTest
  5. 实现所声明的 native 方法

    • 创建 test.cpp 实现头文件中的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      #include <jni.h>
      #include <stdio.h>

      #ifdef __cplusplus
      extern "C" {
      #endif

      jstring Java_com_example_liyu_studytest_JniTest_get(JNIEnv *env, jobject thiz) {
      printf("invoke get in c++\n");
      callJavaMethod(env, thiz);
      return env->NewStringUTF("Hello from JNI in libjni-test.so !");
      }

      void Java_com_example_liyu_studytest_JniTest_set(JNIEnv *env, jobject thiz, jstring string) {
      printf("invoke set from C++\n");
      char* str = (char*)env->GetStringUTFChars(string,NULL);
      printf("%s\n", str);
      env->ReleaseStringUTFChars(string, str);
      }

      #ifdef __cplusplus
      }
      #endif
    • 创建 Application.mk 文件

      1
      2
      APP_MODULES := jni-test
      APP_ABI :=all
    • 创建 Android.mk 文件

      1
      2
      3
      4
      5
      6
      7
      8
      LOCAL_PATH := $(call my-dir)

      include $(CLEAR_VARS)

      LOCAL_MODULE := jni-test
      LOCAL_SRC_FILES := test.cpp

      include $(BUILD_SHARED_LIBRARY)
  6. 切换到 jni 目录下,执行 ndk-build 命令,即可生成对应平台的 so 库。

  7. 最后在 app/src/main 中新建一个名为 jniLibs 的目录,将生成的 so 库复制进去即可。(jniLibs 的目录是 Android Studio 所能识别的默认目录,也可修改)

以上为手动创建的步骤,其实还可以使用 Android Studio 来自动编译产生 so 库,需要修改 build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android {
...
defaultConfig {
...
ndk {
moduleName "jni-test"
}
}

sourceSets.main {
jni.srcDirs 'src/main/jni'
}

}

如果只打包特定平台的 so 库,也需要修改 build.gradle

1
2
3
4
5
6
7
8
9
android {
...
defaultConfig {
...
ndk {
abiFilters "armeabi", "armeabi-v7a"
}
}
}

JNI 的数据类型和类型签名

基本类型
JNI 类型 Java 类型 描述
jbyte byte 有符号,8 位,整型
jshort short 有符号,16 位,整型
jint int 有符号,32 位,整型
jlong long 有符号,64 位,整型
jfloat float 32 位,浮点型
jdouble double 64 位,浮点型
jboolean boolean 无符号,8 位,整型
jchar char 无符号,16 位,整型
void void 无类型
引用类型
JNI 类型 Java 类型 描述
jclass Class
jobject Object Java 对象
jstring String 字符串
jobjectArray Object[] 对象数组
jbyteArray byte[] byte 数组
jshortArray short[] short 数组
jintArray int[] int 数组
jlongArray long[] long 数组
jfloatArray float[] float 数组
jdoubleArray double[] double 数组
jbooleanArray boolean[] boolean 数组
jcharArray char[] char 数组
jthrowable Throwable Throwable
类型签名

JNI 的类型签名标识了一个特定的 Java 类型,这个类型可以是类、方法或数据类型。

类和对象的签名

采用 L+包名+类名+; 的形式,且将包名中的 . 替换为 /

如:java.lang.String 的签名为Ljava/lang/String;

基本数据类型的签名
Java 类型 签名
byte B
short S
int I
long J
float F
double D
boolean Z
char C
void V
数组的签名
  1. 一维数组的签名就是 [+类型签名

    如: String[] 的签名为 [Ljava/lang/String;byte[] 的签名为 [B

  2. 多维数组的签名就是 n个[+类型签名

    如:int[][] 的签名为[[I

方法的签名

方法的签名为:(各参数类型签名) + 返回值类型签名

int func1() 的签名为 ()I
void func2(int i) 的签名为 (I)V
boolean func3(int a, double b, String[] c) 的签名为 (ID[Ljava/lang/String;)Z

JNI 调用 Java 方法的流程

JNI 调用 Java 静态方法的流程是先找到类,然后根据方法名找到方法的 id,最后就可以调用这个方法了。调用非静态方法时需要先构造出类的对象后才能调用它。

下面是一个 JNI 调用 Java 静态方法的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void callJavaMethod(JNIEnv *env, jobject thiz) {
// 获取 jclass
jclass clazz = env->GetObjectClass(thiz);
// 根据类、方法名以及方法签名获取静态方法的 ID
// 非静态方法的 ID 通过 GetMethodID 获取,参数一致
jmethodID id = env->GetStaticMethodID(clazz, "methodCalledByJni", "(Ljava/lang/String;)V");
if (id == NULL) {
printf("find method methodCalledByJni error!");
}
// String 参数需要通过 NewStringUTF 生成 jstring 使用,否则会报错
jstring msg = env->NewStringUTF("msg send by callJavaMethod in test.cpp.");
// 非静态方法通过 CallVoidMethod 调用,参数一致
env->CallStaticVoidMethod(clazz, id, msg);
}