NDK:JNI 的数据结构
Posted Elson_6
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NDK:JNI 的数据结构相关的知识,希望对你有一定的参考价值。
文章目录
- 一、概述
- 二、Native 的数据类型
- 三、属性ID和方法ID
- 四、方法签名(Signatures)
- 五、JNINativeMethod
- 六、全局JNINativeMethod的注册流程
- 六、JavaVM指针
- 七、JNIEnv指针
- 八、小结
一、概述
JNI(全名Java Native Interface)Java native接口,其可以让一个运行在Java虚拟机中的Java代码被调用或者调用native层的用C/C++编写的基于本机硬件和操作系统的程序。简单理解为就是一个连接Java层和Native层的桥梁。
开发者可以在native层通过JNI调用到Java层的代码,也可以在Java层声明native方法的调用入口。
NDK Version: 21.4.7075529
关联文章:
二、Native 的数据类型
接下来我们先介绍一下JNI层定义的数据类型。
2.1 基本类型
JNI 层的基本数据类型:
// jni.h 文件
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
Java 层与JNI 层基本数据类型的映射关系:
JNI 层的基本数据类型只要在 Java 的基本数据类型前加一个
j
即可。如byte -> jbyte
。
Java 类型 | Native类型 | 有无符合 | 字长 |
---|---|---|---|
boolean | jboolean | 无符号 | 8字节 |
byte | jbyte | 有符号 | 8字节 |
char | jchar | 无符号 | 16字节 |
short | jshort | 有符号 | 16字节 |
int | jint | 有符号 | 32字节 |
long | jlong | 有符号 | 64字节 |
float | jfloat | 有符号 | 32字节 |
double | jdouble | 有符号 | 64字节 |
void | void | - | - |
2.2 引用类型
JNI 层的引用类型:
如果使用C++语言编写,则所有引用派生自 jobject 根类,如下所示。
// jni.h 文件
class _jobject ;
class _jclass : public _jobject ;
class _jstring : public _jobject ;
class _jarray : public _jobject ;
class _jobjectArray : public _jarray ;
class _jbooleanArray : public _jarray ;
class _jbyteArray : public _jarray ;
class _jcharArray : public _jarray ;
class _jshortArray : public _jarray ;
class _jintArray : public _jarray ;
class _jlongArray : public _jarray ;
class _jfloatArray : public _jarray ;
class _jdoubleArray : public _jarray ;
class _jthrowable : public _jobject ;
Java 层与JNI 层引用类型的映射关系:
JNI 中提供了一系列的引用类型,这些引用类型和Java中的类型是一一对应的。
Java 类型 | Native类型 |
---|---|
Object | jobject |
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
java.lang.Object[] | jobjectArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
2.3 jclass 与 jobject 的使用场景
- 为了让 JNI 层具备访问 Java 中的类和对象的能力,JNI 层使用 jobject 指代 Java 层对象,使用 jclass 指代 Java 层的类。
- jobject 与 jclass 通常作为 JNI 函数的第二个参数。
- 当所声明 Native 方法是静态方法时,对应参数类型是 jclass,因为静态方法不依赖对象实例,而依赖于类。
- 当所声明 Native 方法不是静态方法时,对应参数类型是 jobject。
下面是具体的代码演示:
public class MainActivity extends AppCompatActivity
static
System.loadLibrary("jnitestdemo");
// 非静态方法
public native String stringFromJNI();
// 静态方法
public static native String staticStringFromJNI();
extern "C"
JNIEXPORT jstring JNICALL
Java_com_elson_jnitestdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) //传入jobject,指代MainActivity对象。
std::string hello = "invoke normal fun";
return env->NewStringUTF(hello.c_str());
extern "C"
JNIEXPORT jstring JNICALL
Java_com_elson_jnitestdemo_MainActivity_staticStringFromJNI(
JNIEnv *env,
jclass clazz) //传入jclass,指代MainActivity类。
std::string hello = "invoke static fun";
return env->NewStringUTF(hello.c_str());
三、属性ID和方法ID
属性和方法的ID其实是一个C结构体类型的指针:
// 结构体
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID* jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */
小结:
- _jfieldID:表示Java层的一个类的属性类型,是一个结构体,而 jfieldID 是结构体的一个指针类型。Native层可以使用 jfieldID 这个指针对属性进行赋值。
- _jmethodID:表示Java层的某个类的方法类型,也是一个结构体,而 jmethodID 是结构体的一个指针类型。
四、方法签名(Signatures)
JNI使用的是Java虚拟机的签名描述方式,具体的类型对照关系如下表所示:
Type Signature | Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L + fully-qualified-class; | 类的权限定描述符:如String -> Ljava.lang.String |
[ + type | type[] :属性描述。如 float[] -> [F |
( arg-types ) ret-type | method type:方法描述 |
下面举个实际的例子来说明一下:
Java:public long javaFun(int n, String s, int[] arr);
方法签名:(ILjava/lang/String;[I)J
五、JNINativeMethod
接下来我们看下 JNI 中表示一个方法的结构体 JNINativeMethod。
typedef struct
const char* name;
const char* signature;
void* fnPtr;
JNINativeMethod;
说明:
- 变量name:代表 Java 中函数的名字。
- 变量signature:用字符串是描述了Java中函数的参数和返回值,即方法签名。
- 变量fnPtr:是一个函数指针,指向 native 函数。前面都要接
void *
。 - 第一个变量与第三个变量是对应的,一个是java层方法名,对应着第三个参数的 native 方法名字。
示例:
下面我们以 Framework 层的 MessageQueue.java 为例来说明:
// MessageQueue.class
public final class MessageQueue
private native static long nativeInit();
private native static void nativeWake(long ptr);
对应的 JNI 代码:/frameworks/base/core/jni/android_os_MessageQueue.cpp
// frameworks/base/core/jni/android_os_MessageQueue.cpp
static const JNINativeMethod gMessageQueueMethods[] =
// 第1个参数,对应java层的方法名。
// 第2个参数,对应java层的方法签名(参数和返回值类型)。
// 第3个参数,指向native层对应方法的指针。
"nativeInit", "()J", (void*)android_os_MessageQueue_nativeInit ,
"nativeWake", "(J)V", (void*)android_os_MessageQueue_nativeWake ,
// 省略部分注册代码。
;
// 对应java层的MessageQueue.nativeInit()方法。因为nativeInit是静态方法,所以native方法对于的第2个参数是jclass类型。
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz)
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue)
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
// 对应java层的MessageQueue.nativeWake()方法。
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr)
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
小结:
- 因为Java层调用的是 static 的 native 方法,所以对应的 JNI 方法的第二个参数类型是 jclass。
- JNINativeMethod 结构体中,第一个变量对应java层的方法;第二个变量对应Java层的方法签名;第三个变量对应的是 native 方法名字。
六、全局JNINativeMethod的注册流程
我们知道在 Java 层可以直接调用 JNI 层的代码,但上面的 gMessageQueueMethods[] 数组只是将 Java 层方法与 JNI 层的方法进行映射,在实际调用之前还需要将这个映射关系注册到全局的映射表里,这样才调用 native 代码时,可以从全局映射表里直接查找对应的方法。
下面我们来分析一下它的注册流程,通过源码查看网站的全局搜索功能直接搜,可以定位到 register_android_os_MessageQueue()
方法在 AndroidRuntime.cpp
中被调用,具体如下:
// frameworks/base/core/jni/android_os_MessageQueue.cpp
int register_android_os_MessageQueue(JNIEnv* env)
int res = RegisterMethodsOrDie(env, "android/os/MessageQueue",
gMessageQueueMethods, NELEM(gMessageQueueMethods));
jclass clazz = FindClassOrDie(env, "android/os/MessageQueue");
gMessageQueueClassInfo.mPtr = GetFieldIDOrDie(env, clazz, "mPtr", "J");
gMessageQueueClassInfo.dispatchEvents = GetMethodIDOrDie(env, clazz,
"dispatchEvents", "(II)I");
return res;
// frameworks/base/core/jni/AndroidRuntime.cpp
static const RegJNIRec gRegJNI[] =
REG_JNI(register_com_android_internal_os_RuntimeInit),
REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
// 省略
// 此处就是注册我们MessageQueue映射关系的地方。
REG_JNI(register_android_os_MessageQueue),
// 省略
REG_JNI(register_android_app_Activity),
REG_JNI(register_android_app_ActivityThread),
// frameworks/base/core/jni/AndroidRuntime.cpp
// Register android native functions with the VM.
int AndroidRuntime::startReg(JNIEnv* env)
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
env->PushLocalFrame(200);
// 实际使用gRegJNI数组的地方
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0)
env->PopLocalFrame(NULL);
return -1;
env->PopLocalFrame(NULL);
return 0;
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
static const String8 startSystemServer("start-system-server");
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
// 虚拟机的启动
if (startVm(&mJavaVM, &env, zygote) != 0)
return;
// 创建虚拟机
onVmCreated(env);
// 这里执行JNI的注册
if (startReg(env) < 0)
return;
// 通过字符串拼接启动指定文件内的main方法。
// 略
小结:
- 在 AndroidRuntime 启动的时候,就把全局的 JNI 映射关系表注册到进程中。
六、JavaVM指针
JavaVM 是虚拟机在JNI层的代表,一个进程只有一个JavaVM,所有的线程共享。
// jni.h
struct _JavaVM;
#if defined(__cplusplus)
typedef _JavaVM JavaVM; //C++重新定义了一个名字。
struct _JavaVM
const struct JNIInvokeInterface* functions;
// C++部分的定义
#if defined(__cplusplus)
jint DestroyJavaVM()
return functions->DestroyJavaVM(this);
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
return functions->AttachCurrentThread(this, p_env, thr_args);
jint DetachCurrentThread()
return functions->DetachCurrentThread(this);
jint GetEnv(void** env, jint version)
return functions->GetEnv(this, env, version);
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args);
#endif /*__cplusplus*/
;
struct JNIInvokeInterface
void* reserved0;
void* reserved1;
void* reserved2;
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
;
七、JNIEnv指针
JNIEnv (Java Native Interface Environment) 是一个JNI接口指针(每个线程独有一个 JNIEnv 指针),指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地的方法通过JNI函数来访问JVM中的数据结构,详情如下图:
JNIEnv 结构体中的功能大体可以分为下表所示的几个部分:
- Class操作
- 反射操作
- 对象字段 & 方法操作
- 类的静态字段 & 静态方法操作
- 字符串操作
- 锁操作
- 数组操作
- 注册和反注册native方法
- 异常Exception操作
- 引用的操作
详细分析请参考文章:NDK(三):JNIEnv解析 。
八、小结
本文主要介绍了如下几点:
- Java 层与 JNI 层的基本数据类型和引用类型的对应关系。
- jclass 与 jobject 的使用场景差异,这与 Java 层和字节码层的数据类型对应关系类似。
- 属性ID和方法ID。
- 方法签名的对应关系。
- JNINativeMethod 的结构体和 JNINativeMethod 的注册流程。
- JavaVM 指针。
- JNIEnv 指针。
Android JNI&NDK编程小结及建议
前言
由于网上关于JNI/NDK相关的知识点介绍的比较零散而且不具备参照性,所以写了这篇JNI/NDK笔记,便于作为随时查阅的工具类型的文章,本文主要的介绍了在平时项目中常用的命令、JNI数据类型、签名等,
常用命令
javac 编译java源文件生成.class文件
由于JNI对应的头文件由javah工具根据对应的.class文件生成,所以在进行JNI编程之前,写好Java代码后需要先编译,在使用javah生成对应的头文件
javah -jni自动生成头文件
举例说明:
- 生成普通的JNI头文件
javah -classpath path -jni -d outputdirpath com.mrljdx.JavaNativeCode
- 在Java函数中包含Android相关的参数代码,则需要在classpath中添加android.jar包的绝对路径地址
javah -classpath path:$ANDROID_HOME/path/android.jar -jni -d outputdirpath com.mrljdx.JavaNativeCodeWithAndroid
javap -s -p 查看函数签名
-s: 显示签名(只显示public类型的签名) -p:显示所有函数、成员变量的签名
举例说明:
javap -classpath pacakage_path_dir -s -p com.mrljdx.JavaCode
JNI数据类型和类型签名
数据类型
JNI的数据类型包括:基本类型和引用类型。这一点和Java的语言特性一致,基本类型包括jboolean、jchar、jint、jlong、jbyte、jshort、jfloat、jdouble、void,与Java类型的对应关系如下:
JNI类型 | Java类型 | 描述 |
jboolean | boolean | 无符号8位整型 |
jbyte | byte | 有符号8位整型 |
jchar | char | 无符号16位整型 |
jshort | short | 有符号16位整型 |
jint | int | 32位整型 |
jlong | long | 64位整型 |
jfloat | float | 32位整型 |
jdouble | double | 64位整型 |
void | void | 无类型 |
JNI中引用类型主要有类、对象和数组,这点也上符合Java的语法规范,对应的关系如下:
JNI 类型 | Java引用类型 | 描述 |
jobject | Object | Object类型 |
jclass | Class | Class类型 |
jstring | String | String类型 |
jobjectArray | Object[] | 对象数组 |
jbooleanArray | boolean[] | boolean数组 |
jbyteArray | byte[] | byte数组 |
jcharArray | char[] | char数组 |
jshortArray | short[] | short数组 |
jintArray | int[] | int数组 |
jlongArray | long[] | long数组 |
jfloatArray | float[] | float数组 |
jdoubleArray | double[] | double数组 |
jthrowable | Throwable | Throwable |
JNI类型签名
JNI的类型签名标识了一个特定的Java类型,这个类型可以是类和方法,也可以是数据类型。
- 类型签名
类的签名采用”L+包名+类名+;”标识,包名中将.
替换为/
即可。
比如String类的签名:
Ljava/lang/String;
注意末尾的;
属于签名的一部分。
再比如Android中Context类的签名:
Landroid/content/Context;
- 基本数据类型签名
基本数据类型的签名采用一系列大写字母来标识,如下:
Java类型 | 签名 |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
可以发现除了 long
基本数据类型的签名为J
之外其他的都比较容易辨识,估计是由于之前的类类型的签名开头为L+包名+类名+;
设计者为了区分所以签名为J
- 数组的类型签名
数组的类型签名比起类类型和基本数据类型的要稍微复杂一点,不过还是很好理解的。对于数组来说,它的签名为[+类型签名
,举例说明:
String[] 数组类型对应的签名:
[Ljava/lang/String;
可以发现,就是在String的类签名前加了个[
同理基本数据类型签名int[]的签名:
[I
注意这里基本类型后面是不带分号的。
那么多维数组呢?可以类推,int[][] 的签名为[[I
,而String[][]的签名为[[Ljava/lang/String;
- 方法的签名
在JNI中会经常需要在C/C++代码中调用Java的函数,这时候就会用到方法的签名。方法的签名为(+参数类型签名+)+返回值类型签名
,比如:
方法: boolean login(String username,String password)
的方法签名如下:
(Ljava/lang/String;Ljava/lang/String)B
如果这里不理解的话,请再去看看之前关于基本类型,类类型的签名部分内容。
小技巧:使用 类似于
javap -classpath pathdir -s -p com.sample.JavaCode
的 javap -s -p 命令也可以帮助查看一些类中各种方法和成员变量的签名。
JNI相关命名解释
- 函数名的格式遵循规则:
Java_包名_类名_方法名
- JNIEXPORT、JNICALL、JNIEnv和jobject 都是JNI标准中所定义的类型或者宏
- JNIEnv * : 指向JNI环境的指针,可以通过JNIEnv * 访问JNI提供的接口方法
- JNIEXPORT、JNICALL:是jni.h中所定义的宏。
注:JNIEnv * 可以简单的理解为Java和C/C++ 之间相互调用的桥梁,我们可以通过JNIEnv * 调用C/C++定义的方法,也可以在C/C++中通过JNIEnv * 来调用Java类中的方法。下面将会讲到C/C++中调用Java的方法,注意JNIEnv *的作用。
在C/C++中调用Java方法
首先说明一点,在Android开发过程中使用NDK主要是为了提高代码的安全性,有些游戏公司可能是为了方便利用已有的C/C++开源库来进行平台移植,其实在性能提升方面,NDK的作用并不是很明显。所以有时候一些在Java中实现起来非常简单的代码放在JNI里面做会显得吃力不讨好,所以干脆就直接在JNI中调用Java的方法,我们只把加密和验证的一些逻辑写到JNI层就行了。
在JNI中调用Java方法流程如下:
- 在Java中定义一个静态方法供JNI调用,注意要是静态的。
- 在JNI中利用env来调用Java中定义的静态方法
- 调用声明好的静态方法
可能流程说的比较抽象,用代码简单说明一下:
- 定义静态方法:
//对应包名:com.mrljdx.jni.HelloJNI public static void helloJava() System.out.println("Hello JavaCode");
- JNI声明静态方法:
static void static_helloJava(JNIEnv *env) jclass clazz = env->FindClass("com/mrljdx/jni/HelloJNI"); jmethodID mid = env->GetStaticMethodID(clazz, "helloJava", "()V"); env->CallStaticVoidMethod(clazz, mid);
- 调用声明好的静态方法:
static_helloJava(env);
在AndroidStudio中NDK编程配置注意事项:
- 在项目的
gradle.properties
中添加ndk支持:
android.useDeprecatedNdk=true
- 配置
build.gradle
看代码注释:
defaultConfig minSdkVersion 9 targetSdkVersion 23 versionCode 1 versionName "1.0" //配置ndk 支持 ndk //编译的so库名称 libsecurity.so moduleName "security" //指定编译后的库支持的平台 abiFilters "armeabi", "mips", "x86", "armeabi-v7a" //用于指定应用应该使用哪个标准库,此处添加c++库支持 stl "stlport_static"
- 在AndroidStudio中写JNI代码有一个比较爽的地方,就是Android.mk系统会在编译时自动帮你生成,你只需要配置build.gradle就行了。注意jni相关代码需要放在
src/main/jni
目录下。如果对gradle配置不了解可
小结
在我们做产品的时候,应该考虑该用JNI&NDK的时候就用,一切出发点是基于用户的体验和数据安全,我觉得在以下几种情况下建议使用NDK:
- 重用现有的代码,比如C/C++的代码在Android中的重用。
- 数据安全,比如将Http的请求加密和解密算法放在NDK中去实现,这样可以提高应用的安全。
- 提升性能,由于Android设备制造商在手机中给每个应用分配了可用的最大RAM,有时候为了性能考虑,可以通过Native代码向系统来“借”一些内存,尽量少的使用系统分配给应用的内存。
以上是关于NDK:JNI 的数据结构的主要内容,如果未能解决你的问题,请参考以下文章
JNI/NDK开发指南—— JNI开发流程及HelloWorld
JNI/NDK开发指南——JVM查找java native方法的规则
Android jni/ndk编程二:jni数据类型转换(primitive,String,array)