Android JNI
创始人
2025-05-31 10:13:08

1.JNI

JNI(Java Native Interface)指Java本地调用,作用是使Java与C/C++具有交互能力。JNI相当于翻译官,使两种语言能够互相理解相互调用。

NDK(Native Development kit)本地开发工具包,允许使用原生语言(C/C++)来实现应用程序的部分功能。

通过JNI技术可以实现Java调用C程序、C程序调用Java代码。

041f5748d75045f8b70b062952b6cc82.png

JNI的使用步骤:

①java声明native函数

②jni实现对应的c函数

③编译生成so库

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

为什么要使用JNI技术呢?因为当需要进行大量数据计算时,原生代码的计算速度远远快于Java:

e7390d4d059046e4a9def383bcaab1eb.png

 

2.JNI函数注册

开发中使用JNI,一般是定义Java native方法,并写好对应的C方法实现。那么在Java代码中调用Java native方法时,虚拟机是怎么知道并调用so库里对应的C方法的呢?其实Java native方法与C方法的对应关系是通过注册实现的。

JNI函数分为静态注册和动态注册两种方式。

①静态注册

静态注册就是通过固定的命名规则映射Java和Native函数。即C中按照JNI规范书写函数名:Java_包名_类名_方法名。

通过javac和javah编译出头文件,然后实现对应的cpp文件,这就是静态注册的方式。这种调用方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配就会报错。

举例:

//Java层代码JniSdk.java

public class JniSdk {

    static {

        System.loadLibrary("test_jni");

    }

    public native String showJniMessage();

}

//Native层代码test_jni.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_jnidemo_JniSdk_showJniMessage函数。这个映射是JVM实现的。

在调用showJniMessage函数时JVM会从JNI库寻找对应的函数并调用。寻找时的规则:由于JniSdk.java的包名是com.example.jnidemo,那么showJniMessage方法完整的路径就是com.example.jnidemo.JniSdk.showJniMessage。而.在C里面有特殊的函数,所以JVM就将它替换成了_,并在前面加了Java_标识,就变成了上面的方法。

e77fd49bf5f24f35a7e22be41da81770.png

静态注册的优缺点:

系统默认方式,使用简单;

灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称;

②动态注册

使用静态注册方式时,每次使用JNI都要先声明native,然后编译成class,再生成头文件,最后再按照特定的规则去实现native函数。这一整套流程下来不仅繁琐,很多步骤还是没必要的(没必要生成.h文件,只要在.c文件里面根据对应的规则声明函数即可)。而且JVM在根据静态注册匹配的规则调用函数时效率也比较低。因此有了动态注册的方法。

动态注册就是不用默认的映射规则,直接由开发者告诉JVM java中的native函数对应的是C文件里的哪个函数。

动态注册一般是通过重写JNI_OnLoad函数,用jint RegisterNative(j class clazz, const JNINativeMethod* methods, jint nMethods)函数将Java中定义的native函数和C/C++中定义的函数进行映射。即在JNI_OnLoad中指定Java Native函数与C实现函数的对应关系。

所以动态注册在调用Native方法之前就已经知道它在JNI中对应的函数指针,这需要用到一个结构体来描述两者之间的关系:

typedef struct {

    const char* name; //对应java中native的函数名

    const char* signature; //java中native函数的函数签名

    void* fnPtr; //JNI中对应的函数指针

} JNINativeMethod;

举例:

//Java层定义Native函数

public class JniSdk {

    static {

        System.loadLibrary("test_jni");

    }

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

    public native void dumpMessage();

}

//.cpp中动态注册

/*定义函数映射关系。需要注册的函数列表放在JNINativeMethod类型的数组中。参数:1.java中用native关键字声明的函数名;2.签名(传进来参数类型和返回值类型的说明) ;3.C/C++中对应函数的函数名(地址)*/

static 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;

    //获取JNIEnv

    if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK) {

        LOGI("on jni load , get env failed");

        return JNI_VERSION_1_2;

    }

    //指定类的路径(把.换成/),通过FindClass方法来找到对应的类

    jclass clazz = env->FindClass("com/example/jnidemo/JniSdk");

    //注册函数,参数分别为java类、所要注册的函数数组、注册函数的个数(也可以用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;

}

在调用System.loadlibrary函数时,JVM会回调上面的JNI_OnLoad函数,就是在这个函数里通过env->RegisterNatives进行的动态注册,其中env是jni函数实现的核心。

动态注册原理图:

bee6f91eb27a4952a0320f9c4f03cac8.png

8cb117a248e0454487ece3b758d6a87b.png

动态注册的优缺点:

函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系;

灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)

 

4.函数签名

函数签名可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名,它们是一一对应的关系。

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

int numAdd(int a, int b)

这里面有两个参数都是int,并且返回值也是int。所以的函数签名是(II)I。而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V。

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

 

5.JNIEnv

java函数和jni函数一旦建立映射关系后,在java层调用native函数就变得很简单了。但是一般程序并不仅仅是这么简单的需求,大多数的时候还需要jni函数调用java层的函数,比如进行后台文件操作后将结果通知到上层,这个时候就需要jni调用java的函数了。这时就要JNIEnv出场了,JNIEnv可以说是贯穿了整个JNI技术的核心。

JNIEnv代表了Java环境,通过JNIEnv*就可以对Java端的代码进行操作,如:创建Java对象NewObject或NewString、调用Java对象的方法CallMethod、获取Java对象的属性GetField等。

通过JNIEnv *env调用Java层代码,如获得某个字段、获取某个函数、执行某个函数等:

//获得某类中定义的字段id

jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) {

    return functions->GetFieldID(this, clazz, name, sig);

}

//获得某类中定义的函数id

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) {

    return functions->GetMethodID(this, clazz, name, sig);

}

这里与Java的反射比较类似,参数:clazz表示类的class对象;name表示字段名、函数名;sig:如果是字段表示字段类型的描述符,如果是函数表示函数结构的描述符。

举例:

//Java代码

public class Hello{

     public int property;

     public int fun(int param, int[] arr){

          return 100;

     }

}

//JNI C/C++代码

JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){

    jclass myClazz = env->GetObjectClass(obj);

    jfieldId fieldId_prop = env -> GetFieldId(myClazz, "property", "I");

    jmethodId methodId_fun = env -> GetMethodId(myClazz, "fun", "(I[I)I");

}

 

Java语言的执行环境是Java虚拟机JVM,JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建JVM虚拟机时被返回。JNI全局仅仅有一个,JavaVM是Java虚拟机在JNI层的代表,一个JVM对应一个JavaVM结构。一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。JNIEnv是一个线程相关的函数表结构体,该结构体代表了Java在本线程的执行环境。不同线程的JNIEnv是不同,也不能相互共享使用。在本地代码中通过JNIEnv的函数表来操作Java数据或调用Java方法。

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

 

6.JNI函数

常见的JNI方法:

#include

#include

extern "C" JNIEXPORT jstring JNICALL

Java_com_test_testnativec_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

}

JNI方法结构与Java方法类似,同样包含方法名、参数、返回类型,只不过多了一些修饰词、特定参数类型而已。

①extern "C"

作用:避免编绎器按照C++的方式去编绎C函数。

如果删掉extern “C”,重新生成so,运行app,结果会直接闪退:UnsatisfiedLinkError: No implementation found for java.lang.String com.test.testnativec.MainActivity.stringFromJNI()

通过反编译so文件会发现,去掉extern “C” 后函数名字竟然被修改了:

//保留extern "C"

000000000000ea98 T 

Java_com_test_testnativec_MainActivity_stringFromJNI

//去掉extern "C"

000000000000eab8 T 

_Z40Java_com_test_testnativec_MainActivity_stringFromJNIP7_JNIEnvP8_jobject

所以,如果希望编译后的函数名不变,应通知编译器使用C的编译方式编译该函数(即:加上关键字:extern “C”)。

②JNIEXPORT、JNICALL

JNIEXPORT用来表示该函数是否可导出(即:方法的可见性);JNICALL用来表示函数的调用规范(如:__stdcall)。

可以通过点击JNIEXPORT、JNICALL关键字跳转到jni.h中的定义,如下图:

18ef8cc7cded42af9ea8828721812d7c.png

通过查看jni.h中的源码,原来JNIEXPORT、JNICALL是两个宏定义。

宏可以这样理解:宏JNIEXPORT代表的就是右侧的表达式:__attribute__ ((visibility ("default")));或者也可以说:JNIEXPORT是右侧表达式的别名。宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等。

attribute___((visibility ("default"))) 描述的是“可见性”属性visibility。default表示外部可见,类似于public修饰符 (即:可以被外部调用);hidden表示隐藏,类似于private修饰符 (即:只能被内部调用)。

如果想使用hidden隐藏方法,可这么写:

#include

#include

extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL

Java_com_test_testnativec_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

}

重新编译、运行,结果闪退了。原因是函数Java_com_test_testnativec_MainActivity_stringFromJNI已被隐藏,而在java中调用该函数时,找不到该函数,所以抛出了异常。

宏JNICALL右边是空的,说明只是个空定义。宏JNICALL代表的是右边定义的内容,那么java代码也可直接使用右边的内容(空)替换调JNICALL(即去掉JNICALL关键字),编译后运行,调用so仍然是正确的。

 

7.demo测试

①搭建环境

启动Android Studio --> 打开SDK Manager --> SDK Tools:

227c7930e27d4f399272d80885f63f35.png

选择NDK、CMake、LLDB(调试Native时会使用),选择Apply进行安装,等安装成功后,NDK开发所依赖的环境也就齐全了。

②Native C++项目创建(HelloWord案例)

新建项目,选择Native C++:

ee9417d9d84e4b70810622b82fa6f8e4.png

新创建的项目,默认已包含完整的native示例代码、cmake配置:

673757243a83429585abead93165d375.png

 cf386dce77aa4152b77fef6bf4dfc9ca.png

这样就可以自己定义Java native方法,并在cpp目录中写native实现了,很方便。

可以直接使用工程默认生成的native-lib.cpp,简单调整一下native实现方法的代码:

10bfce23e4d14f7fa219d68504b64cd8.png

因Native C++工程默认已配置好了CMakeLists.txt和gradle,所以可直接运行工程看效果。

JNI交互效果已经看到了,说明CMake编译成功了。

③现在分析一下CMake生成的库文件与apk中的库文件

Android工程编译时,会执行CMake编译,在 工程/app/build/.../cmake/ 中会产生对应的so文件:

837b268f52874fa89db938a67f94967e.png

继续编译安卓工程,会根据build中的内容生成*.apk安装包文件。反编译apk安装包文件,查找so库文件。原来在apk安装包中,so库都被存放在lib目录中:

022d02d297b5427e82799e4a3aa240b1.png

那CMake是如何编译生成so库的呢?其实CMake是基于CMakeLists.txt文件和gradle配置实现编译Native类的。

看一下CMakeLists.txt文件:

#cmake最低版本要求

cmake_minimum_required(VERSION 3.4.1)

# 配置库生成路径。CMAKE_CURRENT_SOURCE_DIR指cmake库的源路径,通常是build/.../cmake/;/../jniLibs/指与CMakeList.txt所在目录的同级目录:jniLibs (如果没有会新建);ANDROID_ABI指生成库文件时,采用gradle配置的ABI策略(即:生成哪些平台对应的库文件)

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

#添加库

add_library(

        # 库名

        native-lib

        # 类型:SHARED 是指动态库,对应的是.so文件,STATIC 是指静态库,对应的是.a文件

        SHARED

        # native类路径

        native-lib.cpp)

# 查找依赖库

find_library( 

        # 依赖库别名

        log-lib

        # 希望加到本地的NDK库名称,log指NDK的日志库

        log)

# 链接库,建立关系( 此处就是指把log-lib 链接给 native-lib使用 )

target_link_libraries( 

        # 目标库名称(native-lib 是要生成的so库)

        native-lib

        # 要链接的库(log-lib 是上面查找的log库)

        ${log-lib})

再看一下app的gradle又是如何配置CMake的呢?

apply plugin: 'com.android.application'

android {

    compileSdkVersion 29

    buildToolsVersion "29.0.1"

    defaultConfig {

        applicationId "com.qxc.testnativec"

        minSdkVersion 21

        targetSdkVersion 29

        versionCode 1

        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        //定义cmake默认配置属性

        externalNativeBuild {

            cmake {

                cppFlags ""

            }

        }

    }

    //定义cmake对应的CMakeList.txt路径(重要)

    externalNativeBuild {

        cmake {

            path "src/main/cpp/CMakeLists.txt"

        }

    }

    sourceSets {

        main {

            jniLibs.srcDirs = ['jniLibs']//指定lib库目录

        }

    }

}

接着,重新build就会在cpp相同目录级别位置生成jniLibs目录,so库也在其中了:

bf3c2281f538496eafdf45010daefa53.png

 

 

 

相关内容

热门资讯

Linux学习之端口、网络协议... 端口:设备与外界通讯交流的出口 网络协议:   网络协议是指计算机通信网...
kuernetes 资源对象分... 文章目录1. pod 状态1.1 容器启动错误类型1.2 ImagePullBackOff 错误1....
STM32实战项目-数码管 程序实现功能: 1、上电后,数码管间隔50ms计数; 2、...
TM1638和TM1639差异... TM1638和TM1639差异说明 ✨本文不涉及具体的单片机代码驱动内容,值针对芯...
Qt+MySql开发笔记:Qt... 若该文为原创文章,转载请注明原文出处 本文章博客地址:https://h...
Java内存模型中的happe... 第29讲 | Java内存模型中的happen-before是什么? Java 语言...
《扬帆优配》算力概念股大爆发,... 3月22日,9股封单金额超亿元,工业富联、鸿博股份、鹏鼎控股分别为3.0...
CF1763D Valid B... CF1763D Valid Bitonic Permutations 题目大意 拱形排列࿰...
SQL语法 DDL、DML、D... 文章目录1 SQL通用语法2 SQL分类3 DDL 数据定义语言3.1 数据库操作3.2 表操作3....
文心一言 VS ChatGPT... 3月16号,百度正式发布了『文心一言』,这是国内公司第一次发布类Chat...
CentOS8提高篇5:磁盘分...        首先需要在虚拟机中模拟添加一块新的硬盘设备,然后进行分区、格式化、挂载等...
Linux防火墙——SNAT、... 目录 NAT 一、SNAT策略及作用 1、概述 SNAT应用环境 SNAT原理 SNAT转换前提条...
部署+使用集群的算力跑CPU密... 我先在开头做一个总结,表达我最终要做的事情和最终环境是如何的,然后我会一...
Uploadifive 批量文... Uploadifive 批量文件上传_uploadifive 多个上传按钮_asing1elife的...
C++入门语法基础 文章目录:1. 什么是C++2. 命名空间2.1 域的概念2.2 命名...
2023年全国DAMA-CDG... DAMA认证为数据管理专业人士提供职业目标晋升规划,彰显了职业发展里程碑及发展阶梯定义...
php实现助记词转TRX,ET... TRX助记词转地址网上都是Java,js或其他语言开发的示例,一个简单的...
【分割数据集操作集锦】毕设记录 1. 按要求将CSV文件转成json文件 有时候一些网络模型的源码会有data.json这样的文件里...
Postman接口测试之断言 如果你看文字部分还是不太理解的话,可以看看这个视频,详细介绍postma...
前端学习第三阶段-第4章 jQ... 4-1 jQuery介绍及常用API导读 01-jQuery入门导读 02-JavaScri...
4、linux初级——Linu... 目录 一、用CRT连接开发板 1、安装CRT调试工具 2、连接开发板 3、开机后ctrl+c...
Urban Radiance ... Urban Radiance Fields:城市辐射场 摘要:这项工作的目标是根据扫描...
天干地支(Java) 题目描述 古代中国使用天干地支来记录当前的年份。 天干一共有十个,分别为:...
SpringBoot雪花ID长... Long类型精度丢失 最近项目中使用雪花ID作为主键,雪花ID是19位Long类型数...
对JSP文件的理解 JSP是java程序。(JSP本质还是一个Servlet) JSP是&#...
【03173】2021年4月高... 一、单向填空题1、大量应用软件开发工具,开始于A、20世纪70年代B、20世纪 80年...
LeetCode5.最长回文子... 目录题目链接题目分析解题思路暴力中心向两边拓展搜索 题目链接 链接 题目分析 简单来说࿰...
unity的C#学习——浮点常... 浮点常量 在C#中,一个浮点常量是由整数部分、小数点、小数部分和指数部分组成。浮点常量...
Angular 开发NPM第三... 准备工作 首先已经安装过node以及angular以及注册过npm账号 新建项目 ng new ...
【Linux Manpage】... NAME libi2c - publicly accessible functions provid...