NDK:JNI 的数据结构

Posted Elson_6

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NDK:JNI 的数据结构相关的知识,希望对你有一定的参考价值。

文章目录

一、概述

JNI(全名Java Native Interface)Java native接口,其可以让一个运行在Java虚拟机中的Java代码被调用或者调用native层的用C/C++编写的基于本机硬件和操作系统的程序。简单理解为就是一个连接Java层和Native层的桥梁。

开发者可以在native层通过JNI调用到Java层的代码,也可以在Java层声明native方法的调用入口。

NDK Version21.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类型有无符合字长
booleanjboolean无符号8字节
bytejbyte有符号8字节
charjchar无符号16字节
shortjshort有符号16字节
intjint有符号32字节
longjlong有符号64字节
floatjfloat有符号32字节
doublejdouble有符号64字节
voidvoid--

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类型
Objectjobject
java.lang.Classjclass
java.lang.Throwablejthrowable
java.lang.Stringjstring
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 SignatureJava Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L + fully-qualified-class;类的权限定描述符:如String -> Ljava.lang.String
[ + typetype[] :属性描述。如 float[] -> [F
( arg-types ) ret-typemethod 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方法流程如下:


  1. 在Java中定义一个静态方法供JNI调用,注意要是静态的。
  2. 在JNI中利用env来调用Java中定义的静态方法
  3. 调用声明好的静态方法

可能流程说的比较抽象,用代码简单说明一下:


  1. 定义静态方法:
    ​//对应包名:com.mrljdx.jni.HelloJNI public static void helloJava() System.out.println("Hello JavaCode"); ​
  2. 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); ​
  3. 调用声明好的静态方法:
    ​static_helloJava(env); ​

在AndroidStudio中NDK编程配置注意事项:


  1. 在项目的​​gradle.properties​​中添加ndk支持:
    ​android.useDeprecatedNdk=true ​
  2. 配置​​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" ​
  3. 在AndroidStudio中写JNI代码有一个比较爽的地方,就是Android.mk系统会在编译时自动帮你生成,你只需要配置build.gradle就行了。注意jni相关代码需要放在​​src/main/jni​​目录下。如果对gradle配置不了解可

小结

在我们做产品的时候,应该考虑该用JNI&NDK的时候就用,一切出发点是基于用户的体验和数据安全,我觉得在以下几种情况下建议使用NDK:


  1. 重用现有的代码,比如C/C++的代码在Android中的重用。
  2. 数据安全,比如将Http的请求加密和解密算法放在NDK中去实现,这样可以提高应用的安全。
  3. 提升性能,由于Android设备制造商在手机中给每个应用分配了可用的最大RAM,有时候为了性能考虑,可以通过Native代码向系统来“借”一些内存,尽量少的使用系统分配给应用的内存。


以上是关于NDK:JNI 的数据结构的主要内容,如果未能解决你的问题,请参考以下文章

JNI开发流程-JNI/NDK

JNI/NDK开发指南—— JNI开发流程及HelloWorld

JNI/NDK开发指南——JVM查找java native方法的规则

Android jni/ndk编程二:jni数据类型转换(primitive,String,array)

JNI/NDK开发指南——JNI数据类型及与Java数据类型的映射关系

JNI/NDK开发指南——JNI数据类型及与Java数据类型的映射关系