android jni 结构体_一文深解JNI,从此不再是路人

本文详细介绍了JNI的定义、作用,以及在Android开发中为何需要JNI。通过原理分析,展示了JavaVM和JNIEnv的区别,解释了JNI的静态和动态注册方式,并探讨了动态注册时的JNI函数参数个数错误问题及其解决方案。此外,还提到了JNI开发中的内存泄漏、字符串转换和代码混淆问题,提供了解决这些问题的建议。
摘要由CSDN通过智能技术生成

概述

定义

JNI是Java Native Interface的缩写,它是一种允许运行于JVM的Java程序去调用(反之亦然)本地代码(通常JNI面向的本地代码是用C、C++以及汇编语言编写的)的编程框架。

为什么需要JNI

我们在Android开发中,大部分使用的编程语言是Java语言,但是如果我们遇到如下场景,Java语言显然不能满足我们的要求。

  1. 应用程序需要一些平台相关的feature的支持。

  2. 兼容以前的用其他语言书写的代码库。

  3. 应用程序的某些关键操作对运行速度要求较高。

上述场景的源码大部分是用C/C++实现的,而Java与C/C++属于不同的编程语言,有不同的数据类型和执行方式,因此Java不能直接调用C/C++。如果我们想要用Java调用C/C++程序,就必须需要一种桥接技术,因此,JNI技术便应运而生。JNI技术是Java语言里面本来就有的,并非Android自创,只是Android使用的JNI标准更简单。

原理

JNI对于应用本身来说,可以看做一个代理模式。对于开发者来说,需要使用C/C++来实现一个代理程序(JNI程序)来实际操作目标原生函数,Java程序中则是jvm通过加载并调用此JNI程序来间接地调用目标原生函数,JNI调用过程及原理如下图所示。

84188e1aef4e9eef0e728c230a2606d2.png

JNI的代码结构

通过原理我们知道,JNI就像一座桥,将Java世界和Native世界间的天堑变成了通途,那么JNI代码到底长什么样呢?我们以实现JNI方法动态注册的入口函数JNI_Onload()为例,分析一下JNI代码结构。

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
        JNIEnv* env = NULL;    //获取JNIEnv    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return -1;    }    assert(env != NULL);    //注册函数    if (!registerNatives(env)) {
            return -1;    }    //返回jni的版本    return JNI_VERSION_1_6;}

从代码中我们会发现两个比较重要的变量:JavaVM和JNIEnv。那么,这两个变量是什么意思呢?下面我们来具体了解一下。

Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回。JVM与JavaVM的关系如下图所示。

8ad4e089730c6ebc24f14c0f860400b9.png

  • JavaVM

JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM。JavaVM结构中封装了一些函数指针(或叫函数表结构),这些函数指针主要是对JVM进行操作的接口。

  • JNIEnv

JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。因此,不同线程的JNIEnv是不同的,也不能相互共享使用。JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或者调用Java方法。JVM与JNIEnv的关系如下图所示。

38d33d2f48bac1302973746b47af1a6d.png

JNIEnv是一个JNI interface pointer,指向一个线程相关的结构,线程相关的结构指向JNI函数指针数组,该数组中存放了大量的JNI函数指针,这些指针指向具体的JNI函数。其对应结构图如下所示。

d1c2b147781a8580305f9b75136f58b6.png

  • JavaVM与JNIEnv的区别

JavaVM是Java虚拟机在JNI层的代表,JNI全局只有一个;而JNIEnv是JavaVM在线程中的代码,每个线程都有一个,JNI可能有很多个JNIEnv。

JNI的注册与加载

JNI的两种注册时机

  • Android系统启动过程中Zygote注册,可通过查询AndroidRuntime.cpp中的gRegJNI,看看是否存在对应的register方法。

  • 调用System.loadLibrary()方式注册。

JNI的两种注册方式

要使Java层的native方法能够正常调用到JNI层中对应的方法,首先要对Java层中定义的native方法进行注册,注册方式有两种:静态注册和动态注册。

  • 静态注册

静态注册是初学者常用的一种注册方式,网上也有很多介绍,其原理为:

根据函数名来建立Java方法与JNI函数的一一对应关系,其主要注册过程如下图所示。

3d0c8015a34fa3d587a4a57d217b26e7.png

静态注册的实现步骤如下。

  1. 编写一个Java类,调用加载对应so文件方法并定义需要的native方法。

  2. 使用JDK命令生成对应头文件。

  3. 将生成的头文件拷贝到jni目录下。

  4. 在jni目录下创建C/C++文件,并引入生成的头文件,实现相关函数。

优点:理解和使用方式简单, 使用相关工具按流程操作就行, 出错率低。

缺点:必须按照函数规则,方法名很长,灵活性低,运行时查找函数效率低。

  • 动态注册

动态注册的原理为:在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来注册Java方法与JNI函数的一一对应关系。我们在调用System.loadLibrary()的时候,会在对应的C/C++文件中回调一个名为JNI_Onload()的函数,在这个函数中一般是做一些初始化相关操作,我们可以在这个方法里面注册函数。其主要注册过程如下图所示。

645f5be128249689c335536a80ee8548.png

动态注册实现步骤如下。

f5bfeb74d6f25d08435f5667825e7cdf.png

通过上述步骤即可实现JNI的动态注册。当需要添加函数时,只需在Java文件中声明native方法,在C/C++文件中实现,并且在JNINativeMethod数组中添加元素并指明对应关系,最后编译生成so库即可。

优点:灵活性高,修改效率高,安全性较高。

缺点:新手有点难理解,可能会由于搞错签名、方法名导致注册失败。

so文件加载流程

通过动态注册和编译生成的so文件最终会被Java层的System.loadLibrary()函数加载,其详细加载和调用流程如下。

48993851b9a793979688a51343469760.png

源码分析:

  • [System.java] loadLibrary

@CallerSensitivepublic static void loadLibrary(String libname) {
       /**     调用Runtime的loadLibrary0方法    */    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);}
  • [Runtime.java] loadLibrary0

 void loadLibrary0(Class> fr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值