本文是在针对 Android 平台中使用 JNI 的分析。
基础
JNI:Java Native Interface 是 Java 本地开发接口,JNI 是一个协议,这个协议用来沟通 Java 代码和外部的本地代码 C/C++ ,确保 Java 和 C/C++ 代码之间能相互调用。
类型与数据结构
基本数据类型
Java 基本数据类型在 JNI 中的对应关系:
| Java 类型 | Native 类型 | 描述 |
|---|---|---|
| boolean | jboolean | unsigned 8 bits |
| byte | jbyte | signed 8 bits |
| char | jchar | unsigned 16 bits |
| short | jshort | signed 16 bits |
| int | jint | signed 32 bits |
| long | jlong | signed 64 bits |
| float | jfloat | 32 bits |
| double | jdouble | 64 bits |
| void | void | not applicable |
jni.h 中基本类型对应源码:
1 | // jni.h |
引用类型
Java 中的引用类型 Object 对应 JNI 中的 jobject ,代表一切引用类型。在 C++ 中通过类继承的方式体现层级,在 C 中都是无类型指针 void* ;先看对应关系:
| Java 类型 | Native 类型 | 描述 |
|---|---|---|
| Object | jobject | 所有的 Java 对象 |
| Class | jclass | Class 对象 |
| Throwable | jthrowable | 可抛出类型对象 |
| String | jstring | 字符串 |
| Object[] | jarray | 数组 |
| boolean[] | jbooleanArray | 布尔数组 |
| byte[] | jbyteArray | 字节数组 |
| char[] | jcharArray | 字符数组 |
| short[] | jshortArray | 短整型数组 |
| int[] | jintArray | 整形数组 |
| long[] | jlongArray | 长整型数组 |
| float[] | jfloatArray | 浮点数组 |
| double[] | jdoubleArray | 双浮点数组 |
jni.h 中引用类型对应的源码:
1 |
|
属性和方法
Java 中的属性和方法,在反射时会用到,在 JNI 中由对应类型:
1 | struct _jfieldID; /* opaque structure */ |
这里并没有给出 _jfieldID, _jmethodID 两个结构体的详细定义,但是可以用 jfieldID, jmethodID 两个结构体指针来表示 Java 类的属性和方法。
可变参数列表
Java 方法中的可变参数列表比如 void set(String... params) ,不定长度的参数可以看做一个数组;在 JNI 中使用 jvalue 来表示,它是一个联合体,通常在 C/C++ 调用 Java 时会用到:
1 | typedef union jvalue { |
类型签名 Type Signatures
JNI 中使用 Java VM 虚拟机的类型签名格式来表示:
| 类型签名 | Java 类型 |
|---|---|
| Z | boolean |
| B | byte |
| C | char |
| S | short |
| I | int |
| J | long |
| F | float |
| D | double |
| L fully-qualified-class ; | fully-qualified-class |
| [ type | type[] |
| ( arg-types ) ret-type | method type |
这个表格包含如下几个部分:
- 基本类型
基本类型都使用单个字符来表示签名,如int a,签名为I。 - 类全限定名
类都使用全限定名,并以L开头,;分号结尾,比如String的全限定名为:Ljava/lang/String;。 - 数组
数组必须以[开头,比如int[]表示为:[I。 - 方法描述符
先写参数再写返回值,比如方法long f (int n, String s, int[] arr);表示为(ILjava/lang/String;[I)J。
字符串编码
Unicode 编码
字符串常见的编码有 ASCII 和 Unicode ;其中 ASCII 只能表示 128 种符号,对于英文完全够用了,但是对于中文、俄文、阿拉伯文等远远不够,于是新定义了通用型 Unicode 方案,它使用 21 位来编码,表示范围为 U+0000 ~ U+10FFFF 接近几百万个字符,其中 U+D800 ~ U+DFFF 之间为保留码位。ASCII 只需要 8 位就能全部表示完,但是 Unicode 编码方案,并没有规定如何存储,因此实现 Unicode 的存储出现了不同的 UTF: Unicode Transformation Format 编码方案:UTF-8, UTF-16, UTF-32 。其中 UTF-8, UTF-16 是变长编码,而 UTF-32 是 32 bit 定长编码。
UTF-8
UTF-8: 8-bit Unicode Transformation Format 变长编码,每个 Unicode 字符被编码为 1-4 个字节,对应关系如下:
| Unicode 编码 | UTF-8 编码 |
|---|---|
| U+0000 ~ U+007F | 0xxxxxxx |
| U+0080 ~ U+07FF | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
也就是最高字节有几个 1 即表示当前 Unicode 字符占几个字节;所以英文字符只需要 1 个字节,绝大部分中文需要 3 个字节,少数可能会使用 4 个字节。
| Unicode 码 | 对应字符 | UTF-8 编码 |
|---|---|---|
| U+0041 | A | 0x41 |
| U+7834 | 破 | 0xE7 0xA0 0xB4 |
| U+6653 | 晓 | 0xE6 0x99 0x93 |
| U+2A6A5 | 𪚥(四个龍) | 0xF0 0xAA 0x9A 0xA5 |
从实际的结果上看,UTF-8 编码只有在单字节内的 Unicode 码和对应的 UTF-8 编码相同;多字节的 UTF-8 编码已经看不出对应的 Unicode 码了。
UTF-16
UTF-16: 16-bit Unicode Transformation Format 变长编码,每个 Unicode 字符被编码为 2 个或者 4 个字节;即码位范围 U+0000 ~ U+FFFF 使用 2 字节,而 U+10000 ~U+10FFFF 使用 4 字节。
码位范围 U+0000 ~ U+FFFF ,以中文为例,相当于能表示 65536 个常用汉字,因为使用的是 2 字节,所以这个范围内的 UTF-16 编码和 Unicode 码完全一样:
| Unicode | 字符 | UTF-16 码元 | UTF-16 LE 小端 | UTF-16 BE 大端 |
|---|---|---|---|---|
| U+0041 | A | 0x0041 | 0x41 0x00 | 0x00 0x41 |
| U+7834 | 破 | 0x7834 | 0x34 0x78 | 0x78 0x34 |
| U+6653 | 晓 | 0x6653 | 0x53 0x66 | 0x66 0x53 |
码位范围 U+10000 ~U+10FFFF ,并不是直接使用 4 字节存储的,而是使用的代理码,即先将码表减去 0x10000 再针对高低 10 位分别与前导码做或运算后再存储的,这里不做详细分析。
修改后的 UTF-8 编码
JVM 中字符串使用的是修改后的 UTF-8 编码方案,即 Modified UTF-8 ,它实际是 UTF-8 和 UTF-16 的一个混合体:
| Unicode 编码 | Modified UTF-8 编码 |
|---|---|
| U+0000 ~ U+007F | 0xxxxxxx |
| U+0080 ~ U+07FF | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 11101101 1010vvvv 10wwwwww 11101101 1011yyyy 10zzzzzz |
修改后的 UTF-8 只使用 1 字节、 2 字节、 3 字节和 6 字节四种方式来存储,其中码位范围 U+10000 ~U+10FFFF 的第 1 和 4 字节都是固定的 11101101 ,另外 4 个字节使用的是 UTF-16 的代理码,高 10 位放入第 2 和第 3 字节,低 10 位放入第 5 和第 6 字节。JNI 中的字符串也采用的是这种修改后的 UTF-8 编码。
JNI 接口指针
基本概念
JNI 的设计架构如下图所示:

JNI 接口指针指向函数表,包含当前线程的所有信息,而这个接口指针在 JNI 中,使用 JNIEnv 来表示;每个线程对应一个 JNIEnv 结构,它们保存在线程本地存储 TLS 中;因此不同的线程的 JNIEnv 是不同,也不能相互共享使用。JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM ,所以一个 JavaVM 中可以有多个 JNIEnv 。
1 | // jni.h |
JavaVM 源码
JavaVM 代表的是虚拟机相关信息,源码中 JavaVM 实际就是 JNIInvokeInterface 指针,在 C++ 中,_JavaVM 是 JNIInvokeInterface 的包装类。
1 | // jni.h |
JNIEnv 源码
JNIEnv 代表了当前线程的函数表,几乎包含了所有的 JNI 函数,它是线程隔离 TLS 的,所以不能在线程间共享。JNIEnv 主要用于操作 Java 对象的类、方法、属性等,可以理解为反射的 C/C++ 实现。JNIEnv 实际是 JNINativeInterface 结构指针,在 C++ 中,_JNIEnv 是 JNINativeInterface 的包装类。所以它们的调用有两种风格:
C风格:(*env)->NewStringUTF(env, “Hellow World!”);C++风格:env->NewStringUTF(“Hellow World!”);
1 | struct JNINativeInterface { |
JNI 引用
JNI 支持将类实例和数组类型(如 jobject, jclass, jstring, jarray )作为不透明的引用,通过使用 JNI 方法来得到指向该数据结构的指针的引用,也就是说 JNI 操作的内容都是引用。JNI 规范中定义了三种引用:
Local Reference局部引用Global Reference全局引用Weak Global Reference弱全局引用
这三种引用的作用域和生命周期都不一样:
- 局部引用在方法返回时会被自动释放,当局部引用在使用过程中,
GC不会回收该对象 - 全局引用和弱全局引用,一直保存直到手动释放
- 全局引用在释放前不会被
GC回收;弱全局引用GC时会被回收
局部引用
- 通过
NewLocalRef;或者大部分创建接口FindClass, NewObject, GetObjectClass, NewCharArray等都会生成局部引用 - 虽然局部引用在方法返回时会自动释放,但是通常如果后续并不使用该引用了,应该主动释放
DeleteLocalRef - 局部引用是线程相关的,不在跨函数使用,不能跨线前使用
全局引用
- 全局引用必须主动创建
NewGlobalRef;通过DeleteGlobalRef主动释放 - 全局引用可以跨方法使用,可以在多线程中使用
- 全局引用在使用中,引用的对象不会被
GC回收,需要特别注意
弱全局引用
- 弱全局引用必须主动创建
NewWeakGlobalRef;通过DeleteGlobalWeakRef主动释放 - 弱全局引用可以跨方法使用,可以在多线程中使用
- 弱全局引用在使用中,引用的对象可能会被
GC回收,使用时需要做空判断
引用比较
使用 IsSameObject 比较两个引用是否指向同一个对象:jboolean (IsSameObject)(JNIEnv, jobject, jobject); ;返回 JNI_TRUE 表示指向的是同一个。
API 基本功能
JNIEnv 主要是操作 Java 中的代码,类似反射,所以主要根据 Java 特性来介绍例子。先说明 JNI 中几个重要的类型:
jclass:表示Java中的类jfieldID:表示Java中的属性字段IDjmethodID:表示Java中的方法IDjobject:表示Java中的一切对象
动态注册方法
Java 中 native 的方法可以通过静态注册和动态注册的方式和 C/C++ 中的代码绑定:
- 静态注册
必须满足命名格式:Java_packageName_className_nativeMethodName,都是以下划线_拼接的。如果满足这个格式,Java和C/C++会自动关联。 - 动态注册
不需要满足命名格式,在so文件加载时,通过代码动态注册来关联。
动态注册的入口函数为 JNI_OnLoad ,并通过 JNINativeMethod 结构体提供映射表,示例代码 Java 中的 dynamicRegistration 动态注册匹配 nativeDynamicRegistration 方法:
1 | void nativeDynamicRegistration(){ |
这段代码中:
JNI_OnLoad是入口函数,在so文件加载时调用,可以在该函数中使用全局变量保存vm变量的值JNINativeMethod定义映射数组时,每个映射方法必须按照:名称、方法签名、函数指针;格式来匹配env->RegisterNatives注册方法映射数组
字符串操作
从字符串编码那一节我们可以看到,Java, JNI, C/C++ 对字符串编码都不一样,所以 JNI 中的字符串操作需要先进行转码;可以简单的理解为:
jstring:Java和JNI中可以相互传递和使用char *:C/C++和JNI中可以相互传递和使用jchar*:JNI中特有的(本文暂时没找到怎么用)
因此我们重点需要关注的就是 jstring, char* 之间的转换了;相关 API :
1 | // jchar* 和 jstring 相互转换 |
示例代码,Java 传入的字符包含中英文,从 JNI 中返回的字符串也包含中英文:
1 | String input = "abc你好"; |
JNI 对字符串的处理:
1 | JNIEXPORT jstring JNICALL |
输出结果:
1 | 11:07:52.786 E/JniClient-jni: sampleString, inputStr = abc你好 |
数组
JNI 中数组操作重要 API :
GetIntArrayElements...获取数组的元素,返回一个指针类型,表示指向一个数组GetArrayLength获取数组长度- 使用完毕后,需要使用
ReleaseIntArrayElements释放内存 NewCharArray创建数组SetCharArrayRegion拷贝内容到数组
先看 Java 端调用的代码:
1 | public native char[] samplesArray(int[] array); |
JNI 中的实现,先获取数组指针,再获取数组长度,遍历打印数组,最后释放内存:
1 | JNIEXPORT jcharArray JNICALL |
创建 Java 对象
JNI 中创建 Java 对象步骤:
- 根据
package/Classname类名来查找对应类 - 查找构造方法
ID,构造方法默认名称为<init> - 根据构造方法
ID和类来创建对象 - 对象使用完毕后,需要删除释放
查找类时,传递的参数不是类的全限定名,只需要包名和类名即可!
1 | // Constructor |
获取 Java 对象的属性和方法
和反射一样,public, private 等访问控制符并不生效,JNI 中可以获取 Java 对象所有的属性和方法,包含 private 修饰的。重要 API :
GetFieldID:获取属性ID;参数需要使用类型签名GetMethodID:获取方法ID;参数需要使用方法的类型签名GetObjectField/GetIntField...:获取属性对应的值,有多个基本类型变种CallVoidMethod/CallBooleanMethod...:调用某个方法,根据返回值有多个基本类型变种
1 | // 获取和调用 public 的属性和方法 |
获取 Java 中 static 静态属性和方法
static 修饰的属性和方法是属于类的,所以在调用时,并不需要指定对象 jobject ;重要 API :
GetStaticFieldID:获取静态static属性ID;参数需要使用类型签名GetStaticMethodID:获取静态static方法ID;参数需要使用方法的类型签名GetStaticObjectField/GetStaticIntField...:获取静态属性的值,有多个基本类型的变种CallStaticVoidMethod/CallStaticIntMethod...:调用静态方法,根据返回值有多个基本类型的变种
1 | // static field and method |
注意事项:
- 这里在获取字符串时,
jobject可以直接强制转换为jstring jstring打印时,需要转换为char*指针
JNI 中对 Java 多态的处理
Java 多态中的重载,可以通过方法签名(参数不一样)来区分;重写,通常情况下并不需要通过子类调用父类的重写方法,但是 JNI 中提供了该功能;重要 API :
CallNonvirtualVoidMethod/CallNonvirtualIntMethod...:在重写时,直接调用父类的方法
Java 示例代码,father.show 因为重写,输出结果为 Son, show: :
1 | public class JniPolymorphic { |
JNI 中,可以通过 CallNonvirtualVoidMethod 来直接调用,输出 Father, show: :
1 | // 构造 JniPolymorphic 对象 |
注意事项:
- 内部类的类名拼接时,需要使用美元符
$
Android NDK
概念
Android NDK是一套允许使用C/C++等语言,以原生代码实现部分应用的工具集,Google NDK 官网CMake一款外部构建工具,可与Gradle搭配使用来构建原生库;Android Studio中已经使用Cmake替换原来的ndk-buildLLDB一种调试程序,Android Studio使用它来调试原生代码
CPU 架构及 ABI
不同的 CPU 架构:ARMv5, ARMv7, x86, MIPS, ARMv8, MIPS64, x86_64 都关联着一个对应的 ABI 。ABI(Application Binary Interface) 决定了二进制文件如何与系统进行交互。CPU 对应的 ABI 有 armeabi, armeabiv7a, arm64v8a, x86, x86_64, mips, mips64 。Android NDK 支持的 ABI,查看官网
armeabi
将创建以基于ARM v5的设备为目标的库。使用软件浮点运算,可以在所有ARM设备上运行;NDK中已经弃用。armeabi-v7a
创建支持基于ARM v7的设备的库,并将使用硬件FPU指令,支持硬件浮点运算及高级扩展功能。与ARMv5、v6设备不兼容。mips
是世界上很流行的一种RISC处理器,其机制是尽量利用软件办法避免流水线中的数据相关问题;NDK已弃用。x86
支持基于硬件的浮点运算的IA-32指令集。x86是可以兼容armeabi平台运行的,无论是armeabi-v7a还是armeabi,同时带来的也是性能上的损耗。
当前现状,几个大厂的 APK (微信,美团等) 都只使用了 armeabi-v7a,可能是 Android 手机碎片化严重,为了能支持更多的设备。Gradle 中如果不指定,默认会生成所有架构对应的 so ,导致 apk 很大。这里可以参考大厂,只指定 armeabi-v7a !在 defaultConfig 中增加如下代码:
1 | ndk{ |
Android Studio 设置快速生成 h 文件
这个方法是使用命令对 Java 中的 native 方法,手动生成对应头文件,非常方便。

对应填写的参数如下:
1 | $JDKPath$\bin\javah.exe |
搭建 JNI 编译环境
在 Android Studio 中搭建好 JNI 编译环境后,所有的 native 代码会自动生成对应的 JNI 代码。
- 新建
jni目录Folder -> JNI Folder,并创建JNI对应的cpp文件 - 新建
CMakeLists.txt文件,可以拷贝一份模板并将cpp文件和生成库的名称替换下 build.gradle中添加CMakeLists.txt路径,并重新sync工程确保能正确编译Java文件中添加native方法,测试是否能自动生成对应的JNI
如果是新建Native工程,则会自动添加;但是手动新建的,native方法总是生成对应的.c文件,需要拷贝到.cpp中;如果使用了动态注册,则Android Studio中对native方法不再出现小红灯提示,这个时候只能手动生成头文件了。

C/C++ 中的 LOG 打印
Cmake中增加log模块1
2target_link_libraries(hello-jni
log)C/C++包含头文件#include <android/log.h>自定义
LOG输出1
2
((void)__android_log_print(ANDROID_LOG_INFO, "MyTag::", __VA_ARGS__))C/C++代码中使用LOG打印LOGI("sum %d", sum);
调试
JNI 代码出错后,会导致 crash ,根据 logcat 中 backtrace 输出的出错地址,来找到原始代码出错行。工具主要是 ndk 提供的 addr2line 和 ndk-stack 。
示例
出错的源码:
1 | Java_com_***_jni_JniClient_sampleReflectJNI( |
出错原因是 env->FindClass 时,找不到指定类 Lcom/***/jni/JniObject; (FindClass 方法参数中类名不需要添加 L 和 ; ,直接使用 package/Classname ),在 logcat 中输出了如下错误信息:
1 | --------- beginning of crash |
也可以将 tombstone 取出来获取更详细的信息。
addr2line 工具
addr2line 工具在 ndk 的交叉编译工具链中,根据当前调试手机平台选择具体的交叉编译工具,示例代码使用的是 armeabi:ndk-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin ,找到 arm-linux-androideabi-addr2line.exe 文件,它就是 addr2line 工具。
命令格式: addr2line -e so文件 出错地址 ;上面示例中,so 文件为 libjni-sample.so ,这个库文件在 Android Studio 的 app\build\intermediates\cmake\debug\obj\armeabi-v7a\ 目录下;错误地址是 0000812d ,完整命令如下:
1 | C:\**\ndk-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin>arm-linux-androideabi-addr2line.exe -e E:\***\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libjni-sample.so 0x0000812d |
得到的解析结果为源码 JniClient.cpp 的第 26 行错误。
ndk-stack 工具
ndk-stack 工具在 ndk 的当前目录 $NDK_HOME 下,是一个脚本文件 ndk-stack.cmd ,不需要指定交叉编译版本,取出 tombstone 文件后,使用 ndk-stack 自动解析出错误行。
命令格式: ndk-stack -sym so所在文件目录 -dump tobstone > 1.txt ;上面示例中,不需要指定 so 文件名,只需要支持所在目录就行;完整命令如下:
1 | C:\***\ndk-bundle\ndk-stack.cmd -sym E:\***\app\build\intermediates\cmake\debug\obj\armeabi-v7a\ -dump tombstone_05 > 1.txt |
我们打开 1.txt 文件,可以查看到解析的结果:
1 | ********** Crash dump: ********** |
得到了同样的解析结果: JniClient.cpp 的第 26 行错误。
其他
JNI默认对应的是C代码,所以在C++代码中需要做如下声明:extern "C"Android中jni.h定义的函数,都是在art/runtime/jni_internal.cc中实现的
参考文档
- Oracle: Java 8 JNI 官网
- Goolge: Android JNI 官网
- Google: NDK 官网
- 隔壁老李头:Android JNI
- Unicode 与 utf8 utf16 utf32的关系
- 字符、编码和Java中的编码
- C–中文汉字占用字节长度
- JVM Utf8
- JNI中引用类型
- 在JNI中使用引用
- JavaVM和 JNIEnv 动态注册本地方法
- NewString 与 NewStringUtf 解析
- NI数据类型与方法属性访问
- Android Studio 中添加 C/C++ 代码
- Android Studio NDK快速生成.h头文件
- 小楠总: Android NDK开发之旅