android jni 结构体_Android NDK(一) 理解JNI技术

本文深入探讨了Android NDK与JNI技术,解释了JNI的本质是Java与本地代码交互的接口,介绍了NDK的作用、JNI的应用场景、优势与劣势。文章详细阐述了JNI开发流程,包括数据类型、签名规则、JavaVM与JNIEnv的使用,并讨论了静态注册和动态注册的区别。此外,还分析了JNI在Android系统架构中的位置,强调了其在提高性能和实现特定功能方面的价值。
摘要由CSDN通过智能技术生成
6dae5cdbb58854c129d4f3dea667f2cd.png

开始之前,可以先提几个问题

  1. JNI的本质是什么,java和native如何实现互操作?
  2. JNI静态注册与动态注册可以混用吗?

首先是理论部分,先弄清楚NDK与JNI的概念。

NDK与JNI

NDK全称为Native Development Kit,是一套允许使用 C 和 C++ 等语言,以原生代码实现应用的工具集,包括但不限于编译工具、公共库等。

在Android NDK开发中,应用层就是借助JNI以实现对本地代码的调用。不只是应用层,JNI技术也广泛应用在Framework层中,主要源码在 framework/base/目录下

下图简单地从另一个角度看Android的系统架构,其中,JNI接口的实现在Android虚拟机,宿主环境包括Linux kernel。dee606f2d194213d200215618f0ac7d0.png

JNI技术

JNI是什么

JNI,全称Java Native Interface,是一种编程框架,使得Java虚拟机中的Java程序与本地应用/或库可以相互调用。本地程序一般是用其它语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。为了通俗地介绍JNI的作用,摘抄网上的一个比喻,就是中国讲普通话,日本讲日语,我们无法交流,如果此时我们都学会了英语,就可以用英语交流了。JNI就是java和本地语言之间交互的媒介。

54215d79699763ffeebcb98758ae99b1.png

上图,我们可以看到JNI接口的具体实现是在虚拟机。历史上存在过不同本地方法接口的实现,使得开发者不得不编写和维护多种不同的版本。JNI的目标就是提供一套统一的,考虑充分的标准接口,使得开发者可以只写一个版本的本地代码,在不同的Java虚拟机上运行,也就是实现虚拟机无关性。

应用场景

  1. 实现Java库无法提供的基于平台系统相关特性的功能
  2. 直接复用现有的本地开源库
  3. 使用C/C++开发对时间和性能要求很高的逻辑,如音视频,图片处理,游戏逻辑等
  4. 保护关键代码,增大反编译难度
  5. ...

优势与劣势

如上的应用场景就是使用JNI的优势,当然,任何技术都会在解决一个问题的同时带来其他新的问题,使用JNI也会有劣势:

  1. 使用JNI编程时,容易操作不当会引起崩溃。相比Java崩溃,Native崩溃难以捕获与定位
  2. 可能引起内存泄漏等
  3. JNI上下文环境切换开销

其他方案对比

有些替代方案可以允许Java与其他语言编写的代码进行互操作,例如:

  1. Java应用程序可以通过TCP/IP连接或者其他IPC通信机制与本地应用进行通信
  2. 可以利用分布式对象技术,如Java IDL API ...

但是,以上方案的共性是将Java应用程序和原生代码隔离在不同的进程空间中。而JNI技术是在相同的进程空间。使用其他替代方案的问题在于,进程间操作麻烦且低效,复制和传输数据会增加额外开销。

通过上文对JNI技术有了比较清晰的认识之后,我们再看看实战部分。

NDK/JNI开发流程

在Android Studio中,我们可以直接新建一个Native C++工程,用kotlin实现一个最简单的JNI调用。

  1. Java层声明Native方法
package com.devnan.ndk
class MainActivity : AppCompatActivity() {

external fun stringFromJNI(): String
...
}
  1. JNI层实现Java层声明的方法, 可以调用底层库或者回调Java层方法
//native-lib.cpp
extern "C" JNIEXPORT jstring JNICALLJava_com_devnan_ndk_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

  1. Java层加载编译后生成的动态库
// MainActivity
companion object {
init {
System.loadLibrary("native-lib")
}
}

  1. 定义cmake脚本,编译C/C++源码生成动态库so文件
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED native-lib.cpp)

虽然IDE帮我们做了大部分事情,但是还是要明白整个过程的本质。Android NDK开发中,在编译阶段,cmake会根据CMakeLists定义的规则,借助NDK工具包的交叉编译链将C/C++代码编译生成动态库so文件(当然也可以是静态库),最后gradle会通过package task将so一起打包到APK中,这是IDE以及gradle帮我们做的。另外,在安装阶段,Android系统安装apk时会通过PackageManagerService 将apk lib 目录下的 so 文件会被解压到对应apk目录,一般在 /data/app/package-name-xxx/lib 目录;在运行阶段,调用 System.loadLibrary会首先在对应apk目录下查找so,找不到才在系统目录下查找。这里涉及到so的拷贝和加载策略,详细分析会放到后续文章。

JNI数据类型

由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的映射。

基本数据类型:2756a772f5231f671fd4f282c2c105ea.png在NDK包的jni.h文件可以看到定义

//$NDK/sysroot/usr/include/jni.h

/* Primitive types that match up with Java equivalents. */
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 */

引用数据类型,详细可以查看jni.haa99dfc39e7a58bb7627a7f355678c77.png

JNI签名规则

由于Java支持方法重载,在JNI访问Java层方法时仅靠函数名无法唯一确定一个方法,因此JNI提供了一套签名规则,以使用一个字符串来唯一确定一个方法。其规则:(参数1类型签名参数2类型签名…)返回值类型签名,比如:
native方法: external fun stringFromJNI(bool:Boolean, str: String): String
类型签名: (ZLjava/lang/String;)Ljava/lang/String;
建议对class文件使用javap -s -p拿到签名信息,毕竟手写容易出错。8118dd27e68b04327a601bd79f085abd.png

JavaVM与JNIEnv

JNI技术中,native层怎么调用到java层呢?就是利用JavaVM 和 JNIEnv 这两个结构体。可以说,他们就是native通往java世界大门的钥匙。
JavaVM是进程虚拟机环境,每个进程有且只有一个JavaVM实例。Android应用进程启动时在AndroidRuntime.cpp的start()方法完成JavaVM的启动。
JNIEnv是线程上下文环境,每个线程有且只有一个JNIEnv实例,可以看到每个JNI方法的第一个参数就是JNIEnv。
同样,来看一下JavaVM在jni.h的定义

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

struct _JavaVM {
const struct JNIInvokeInterface* functions;

#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*/
};

以上可以看到,JavaVM的所有操作都是由结构体 JNIInvokeInterface 实现的。

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*);
};

前三个指针作为保留使用,后面为函数指针。根据函数名可以推测大概的用法,其中,AttachCurrentThread 函数可以把当前线程附着到虚拟机,getEnv 函数用来获取 JNIEnv 指针。JavaVM的开发使用场景是,若无法获取到线程的JNIEnv,比如C/C++函数回调到Java层,可以全局保存JavaVM,通过AttachCurrentThread将当前线程附着到 JavaVM 以拿到 JNIEnv,最后记得调用 DetachCurrentThread 函数,否则会发生内存泄露。

同理,也可以定位到JNIEnv最后由结构体JNINativeInterface实现。

struct JNINativeInterface {
...
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
jsize);
jclass (*FindClass)(JNIEnv*, const char*);

...
};

可以看到,JNINativeInterface包含了JNI编程中经常用到的JNI函数,可以说JNIEnv就是我们操作 Java 层的入口。篇幅有限,由于JNIEnv相关函数过多,不在此处详细描述。如果对Android ART虚拟机中JNI接口的实现感兴趣,可以看看 platform/art/runtime/jni 目录,JNIEnv函数实现应该在jni_internal.cc 文件。

JNI静态注册与动态注册

JNI技术中,java层怎么找到native方法呢?就是利用了JNI静态注册或动态注册。
顾名思义,静态注册,是根据函数名来建立Java方法和JNI方法间的对应关系。上文例子中,Java层 com.devnan.ndk.MainActivity的native方法 stringFromJNI ,对应的JNI方法是JNIEXPORT jstring JNICALL Java_com_devnan_ndk_MainActivity_stringFromJNI,即JNI规则是Java_包名_类名_方法名。Android应用层一般采用这种方式进行静态注册。其中,JNIEXPORT和JNICALL是两个宏定义。default表示外部可见,类似public,JNICALL在Linux平台只是一个空定义。

#define JNIEXPORT  __attribute__ ((visibility ("default")))
#define JNICALL

而动态注册原理是,在 Java 层调用 System.loadLibrary 时,底层最后会调用 JNI_OnLoad () 函数。在这个函数中通过JNIEnv 提供的 RegisterNatives() 方法,我们可以传递 JNINativeMethod 数组主动建立native方法和JNI方法的映射关系。JNINativeMethod在jni.h的定义如下:typedef struct { const char* name;  //native方法的方法名 const char* signature; //native方法的签名 void*       fnPtr; //JNI层对应实现的方法指针 } JNINativeMethod;

对应上文例子就是

//native-lib.h

const char *className = "com/devnan/ndk/MainActivity";

const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
};


//native-lib.cpp

```C++

/**
* JNI方法随意起名为"stringFromJNI"
*/
JNIEXPORT jstring JNICALL stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello JNI!";
return env->NewStringUTF(hello.c_str());
}

/**
* System.loadLibrary()最后会调用到JNI_OnLoad
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
assert(env != NULL);
if (!registerNatives(env)) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}

/**
* 调用JNIEnv函数RegisterNatives进行注册
*
* jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
* clazz: java类名,通过FindClass得到
* methods: JNINativeMethod结构体指针
* nMethods: 方法个数
*/
static int registerNatives(JNIEnv *env) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
return JNI_FALSE;
}

return JNI_TRUE;
}

可以看到,JNI动态注册比静态注册要麻烦,但是,动态注册效率更快。Android Framework层几乎都是动态注册,Android系统在启动时会执行一些系统函数的JNI注册,从而把映射关系注册到了虚拟机中。init进程启动zygote进程后, 在AndroidRuntime中会先启动虚拟机,然后开始注册JNI方法。简要流程如下:

//frameworks/base/cmds/app_process/app_main.cpp

- zygote进程启动(App_main.cpp执行main函数)
- AndroidRuntime.start
- startVm //启动虚拟机
- startReg //注册JNI函数
- register_jni_procs //遍历RegJNI数组进行注册

小结:
对比动态注册,静态注册的优点是使用简单明了,但是缺点是首次调用时需要Java虚拟机搜索JNI方法以建立映射关系,第一次的运行效率低。另外,静态注册和动态注册可以混用,也就是一部分native方法用静态注册,另一部分用动态注册。思考一下,如果一个native方法同时做了静态注册和动态注册会发生什么情况?那么最后调用的是动态注册的JNI方法的实现,因为 System.loadLibrary("native-lib")是一般放在static代码块,也就是在类初始化阶段中,通过动态注册已经在虚拟机保存了JNI的映射关系(kotlin的写法本质也一样),而静态注册是第一次调用native方法时才建立这种映射。

总结

本文主要介绍了JNI技术的背景、应用场景和优缺点等,并结合 Android 介绍了JNI的开发流程,JNI数据类型和签名规则,JavaVM与JNIEnv的概念,以及JNI静态注册和动态注册的用法和区别。代码已放到 https://github.com/devnan/ndk-practices

您的鼓励就是我最大的动力,欢迎交流,欢迎点个"在看"99b18b8c3e3ce77bc1790ad3b87571ce.png

d19166aa9f653e49f93cfe60a5f24833.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值