草庐IT

Android JNI详解

疯过无痕201314 2023-05-08 原文

目录

一、前言:

二、JNI简介

三、JNI函数注册

3.1静态注册:

3.2 动态注册

四、函数签名

 4.1 什么是函数签名:

4.2 为什么需要函数的签名:

4.3 如何获取函数的签名

五、JNIEnv

5.1 何为JNIEnv:

5.2 通过JNIEnv调用java对象方法

5.3 跨线程如何调用java方法

六、垃圾回收


一、前言:

        本篇文章是针对android 开发过程中的使用的jni技术做一些的原理上的解析,不再介绍具体的jni的使用,关于如何在android中使用jni开发的教程可以去网上搜索然后自行尝试。本篇文章主要介绍的比如jni函数的注册、jni和java层的线程映射关系等

二、JNI简介

        2.1 JNI 是Java Native Interface的缩写,表示"Java本地调用"。通过JNI技术可以实现

                Java调用C程序

                C程序调用Java代码

我们的android源码中有很多代码都是Jni的实现的。例如MediaScanner的实现。就是通过jni的技术让我们在java层扫描到媒体相关的资源的

         2.2 JNi的使用步骤:

                Jni的使用的步骤大概可以如下几步

                a) java声明native函数

                b) jni实现对应的c函数

                c) 编译生成so库

                d) java 加载so库,并调用native函数

三、JNI函数注册

        我们知道,jni的函数在使用的时候是直接调用java层的native函数的,我们平时在开发jni的时候,可能就是按部就班的,定义一个类,生命几个native函数,然后使用javac、javah等命令,然后再实现对应的C文件,最终编译成so库使用。那么java层的native函数是如何调用到c语言那边的函数的呢?这边就设计到JNI函数的注册问题了。JNI函数分为静态注册和动态注册的方式

3.1静态注册:

        静态注册的方式我们平时用的比较多。我们通过javac和javah编译出头文件,然后再实现对应的cpp文件的方式就是属于静态注册的方式。这种调用的方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配,则会报错。具体的匹配的规则是什么样的呢?可以看一下下面的例子


//Java 层代码JniSdk.java
public class JniSdk {
    static {
        System.loadLibrary("test_jni");
    }

    public native String showJniMessage();
}


//Native层代码 jnidemo.cpp
extern "C"
JNIEXPORT jstring JNICALL Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage
  (JNIEnv* env, jobject job) {
    return env->NewStringUTF("hello world");
}

从上面的例子可以看到java层的showJniMessage函数对应的就是c语言那边的Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage函数。那么这个是如何映射的呢?其实这个映射是JVM实现的。我们在调用showJniMessage函数的时候,JVM会从JNI库寻找对应的函数并调用。寻找的时候按照如下规则:

我们的JniSdk.java的包名是com.example.dragon.androidstudy.jnidemo。那么showMessage方法完整的路径就是com.example.dragon.androidstudy.jnidemo.JniSdk.showJniMessage。而.在C里面有特殊的函数,所以JVM就将其他替换成了_。并再前面加了Java_标识,就变成了上面的方法

3.2 动态注册

        不知道大家注意到没有,每次使用JNI的时候,都要先生命native,然后编译成class。在生成头文件。最后再按照特定的规则去实现native函数。这一整套流程下来不仅繁琐,很多步骤还是没必要的(我们根据没必要生成.h文件,只要在.c文件里面根据对应的规则声明函数即可)。而且JVM在根据静态注册匹配的规则调用函数的时候效率也会比较低。因此有了动态注册的方法,所谓的动态注册,就是我们不用默认的映射规则,直接由我们告诉JVM。java的native函数对应的是C文件里面的哪个函数。废话不多说,直接上例子

public class JniSdk {

    static {
        System.loadLibrary("test_jni");
    }


    public static native int numAdd(int a, int b);

    public native void dumpMessage();
}

JNINativeMethod g_methods[] = {
        {"numAdd", "(II)I", (void*)add},
        {"dumpMessage","()V",(void*)dump},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    j_vm = vm;
    JNIEnv *env = NULL;
    if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK) {
        LOGI("on jni load , get env failed");
        return JNI_VERSION_1_2;
    }
    jclass clazz = env->FindClass("com/example/dragon/androidstudy/jnidemo/JniSdk");
    //clazz对应的类名的完整路径。把.换成/   g_methods定义的全局变量     1  是g_methods的数组长度。也可以用sizeof(g_methods)/sizeof(g_methods[0])
    jint ret = env->RegisterNatives(clazz, g_methods, 2);
    if (ret != 0) {
        LOGI("register native methods failed");
    }
    return JNI_VERSION_1_2;
}

上面的JNI_OnLoad函数是在我们通过System.loadlibrary函数的时候,JVM会回调的一个函数,我们就是在这里做的动态注册的事情,可以看到,我们通过env->RegisterNatives注册。这个env是jni函数实现的核心,我们后面再讲。这里主要讲解一下g_methods对象,下面是JNINativeMethods结构体的定义

typedef struct {
    const char* name;   //对应java中native的函数名
    const char* signature;  //java中native函数的函数签名
    void*       fnPtr;  //C这边实现的函数指针
} JNINativeMethod;

其中signature指的是函数的签名。这个在下个小节具体讲解

四、函数签名

        根据第三小节中聊到的,我们在动态注册的时候需要用到函数的签名。那么什么是函数的签名?为什么需要函数的签名呢?如何获取函数的签名?接下来针对这三个问题做一个讲解

 4.1 什么是函数签名:

        所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识么?答案当然是否定的

4.2 为什么需要函数的签名:

        我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才唯一构成一个函数标识,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到一个函数

4.3 如何获取函数的签名

        函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3.....)返回值类型。例如我们上面的numAdd函数一样。他在java层的函数声明是

int numAdd(int a, int b)

这里面有两个参数都是int,并且返回值也是int。所以的函数签名是(II)I。而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V。具体的函数类型对应的签名的映射关系如下

类型标识Java类型类型标识Java类型
ZbooleanFfloat
B     byteDdouble
CcharL/java/language/StringString
Sshort[I        int[]
I        int[Ljava/lang/object        Object[]
JlongVvoid

我们如果自己手动去写这种签名的话。很容易出错,有一个工具可以很方便的列出每个函数的签名,我们可以先通过javac命令编译出class文件。然后再通过javap -s -p xxx.class命令列出这个class文件所有的函数签名

public native java.lang.String showJniMessage();
    descriptor: ()Ljava/lang/String;

  public static native int numAdd(int, int);
    descriptor: (II)I

  public native void dumpMessage();
    descriptor: ()V

五、JNIEnv

        前面几个小节都是讲了java函数和jni函数的一个映射和调用关系,一旦映射关系建立之后,在java层调用native函数就变得很简单了。但是我们的程序并不仅仅是这么简单的需求,绝大多数的时候需要jni函数调用java层的函数,比如我们进行后台文件操作后将结果通知到上层,这个时候就需要调用到java的函数了。那么这个时候该如何实现呢?这个时候就要到我们的主角JNIEnv出场了。这个JNIEnv可以说是贯穿了整个JNI技术的核心,因此我们会着重讲解这个

5.1 何为JNIEnv:

        JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体,这个结构体是和线程相关的。并且C函数里面的线程与java函数中的线程是一一对应关系。也就是说,如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个


mShowTextBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                JniSdk.getInstance().dumpArgs(arg);
                JniSdk.getInstance().dumpArgsBak(arg);
            }
        });


void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg function, env :%p", env);
}

void dumpArgBak(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg bak function, env :%p", env);
}

//实际输出
//on dump arg function, env :0xb7976980
//on dump arg bak function, env :0xb7976980

可以看到,在不同的两个JNI接口里面打印的JNIEnv是一样的,如果是不同的线程,打印的JNIEnv是不一样的。感兴趣的朋友可以自己去做实验

5.2 通过JNIEnv调用java对象方法

        通过JNIEnv调用方法大致可以分为以下两步:

        a、获取到对象的class,并且通过class获取成员属性

        b、通过成员属性设置获取对应的值或者调用对应的方法

这个网上的例子很多,具体的不在本篇的讨论的范围。大家可以自行百度实验



public class JniSdk {
    private int mIntArg = 5;
    public int getArg() {
        return mIntArg;
    }
}

void dump(JNIEnv *env, jobject obj) {
    LOGI("this is dump message call: %p", obj);
    jclass jc = env->GetObjectClass(obj);
    jmethodID  jmethodID1 = env->GetMethodID(jc,"getArg","()I");
    jfieldID  jfieldID1 = env->GetFieldID(jc,"mIntArg","I");
    jint  arg1 = env->GetIntField(obj,jfieldID1);
    jint arg = env->CallIntMethod(obj, jmethodID1);
    LOGI("show int filed: %d, %d",arg, arg1);
}

这边需要注意的一点是:如果jni方法是通过static方式调用的话,这边的jobject表示的是jclass对象,需要进行强转,并不表示一个独立的对象

5.3 跨线程如何调用java方法

上面的java调用jni或者jni调用java的方法,大家感觉没什么特别之处对吧。无非就是调用JNIEnv的几个接口而已。但事实上并非如此,上面可以直接调用的原因是java调用到jni层的时候始终都在同一个线程,因此再jni层可以直接操作从java层传递下来的JNIEnv对象来实现各种操作。但是如果是在JNI层创建的一个额外的线程想调用Java方法呢?这个时候又该如何操作呢?

上面说过,JNIEnv是与线程一一对应的。实际上这里的一一对应是指跟java和jni的线程共同对应。什么意思呢?这里可以用几张图来表示

上面的图表示的JVM内部维护一个关于线程映射的表,一个java线程和一个jni线程共同拥有一个JNIEnv。如果java线程调用native函数的时候,JVM还没有为这两个线程建立起映射关系,那么就会新创建一个JNIEnv并且传递到jni线程,如果之前已经有创建过映射关系。那么就直接采用原来的JNIEnv 。如5.1所描述的那样,两个JNIEnv的对象是相同的。反之也一样,如果jni调用java线程的话,那么需要向JVM申请获取到已经映射的JNIEnv,如果之前未映射过的话。那么就重新创建一个。这个方法就是AttachCurrentThread。



JNIEnv *g_env;

void *func1(void* arg) {
    LOGI("into another thread");
    //使用全局保存的g_env,进行操作java对象的时候程序会崩溃
    jmethodID  jmethodID1 = g_env->GetMethodID(jc,"getArg","()I");
    jint arg = g_env->CallIntMethod(obj, jmethodID1);
    
    //通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃
    JNIEnv *env;
    j_vm->AttachCurrentThread(&env,NULL);

}

void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg function, env :%p", env);
    g_env = env;
    pthread_t *thread;
    pthread_create(thread,NULL, func1, NULL);
}

上面的demo表示JNIEnv跟每个线程是捆绑的,无法在线程B访问到线程A的JNIEnv。所以通过保存g_env的方式去使用是不行的。而是应该要通过AttachCurrentThread方法进行获取新的JNIEnv,然后再进行调用

六、垃圾回收

        我们都知道java创建的对象是由垃圾回收器来回收和释放内存的。那么java的那种方式在jni那边是否行得通的呢?答案是否?在JNI层。如果使用ObjectA = ObjectB的方式来保存变量的话。这种是没办法保存变量的。随时会被回收,我们必须要通过env->NewGlobalRef和env->NewLocalRef的方式来创建,还有一个env->NewWeakGlobalRef(这种很少使用)

        那么这两种的生命周期如何呢?这边直接给出结论:

        NewLocalRef创建的变量再函数调用结束后会被释放掉

        NewGlobalRef创建的变量除非手动delete掉,否则会一直存在

至此,JNI的运行机制的理解已经总结结束了

有关Android JNI详解的更多相关文章

  1. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  2. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  3. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  4. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  5. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  6. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  7. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  8. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

  9. ElasticSearch之 ik分词器详解 - 2

    IK分词器本文分为简介、安装、使用三个角度进行讲解。简介倒排索引众所周知,ES是一个及其强大的搜索引擎,那么它为什么搜索效率极高呢,当然和他的存储方式脱离不了关系,ES采取的是倒排索引,就是反向索引;常见索引结构几乎都是通过key找value,例如Map;倒排索引的优势就是有效利用Value,将多个含有相同Value的值存储至同一位置。分词器为了配合倒排索引,分词器也就诞生了,只有合理的利用Value,才会让倒排索引更加高效,如果一整个Value不进行任何操作直接进行存储,那么Value和key毫无区别。分词器Analyzer通常会对Value进行操作:一、字符过滤,过滤掉html标签;二、分

  10. Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解) - 2

    题外话:抑郁场,开局一小时只出A,死活想不来B,最后因为D题出锅ura才保住可怜的分。但咱本来就写不到DB-LongLegs(数论)本题题解法一学自同样抑郁的知乎作者幽血魅影的题解,有讲解原理。法二来着知乎巨佬cup-pyy(大佬说《不难发现》呜呜)题意三种操作:向上走mmm步向右走mmm步给自己一次走的步数加111,即使得m=m+1m=m+1m=m+1问从(0,0)(0,0)(0,0)走到(a,b)(a,b)(a,b)的最小操作次数,值得注意的是操作三不可逆。解析假设我们最终一步的大小增长到mmm,那么在这个过程中我能以[1,m][1,m][1,m](当步数增长到该数时)之间的任何数字向上或

随机推荐