转载

NDK 开发:JNI 互调

本文章所用的工具版本

  • Android Studio 3.6.3

  • Gradle 5.6.4

  • NDK 21.3.6528147

  • CMake 3.10.2

什么是 JNI?

  • JNI 的全称是 Java Native Interface,从名称上面翻译,它是一个 Java 和 C 语言的接口,通过这个翻译我们基本可以判定,这个 JNI 其实就是 Java 语言和 C 语言之间通讯的桥梁。

为什么要有 JNI?

  • 因为 Java 和 C 之间无法直接通讯,Java 和 JavaScript 也同理,无法直接通过代码显式调用,这中间需要一个翻译官来做这件事,而 JNI 出现的目的就是为了解决 Java 和 C 这两个不同语言之间的通讯问题。

开胃菜

  • 在正式进入主题之前,我们先讲一下如何将一个普通的项目改造成一个 NDK 项目

NDK 开发:JNI 互调

  • 创建一个 cpp 文件夹,这个文件夹和 java 是同级目录

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 然后在这个文件夹下面创建一个 cpp 文件

  • cpp 文件其实就是 c++ 源码,到了这里可能大多数人又有一个疑问涌上心头,刚刚不是说 Java 和 C,怎么到这里就变成 C++ 了呢?

  • 这里解释一下,C++ 是 C 的超集,兼容大部分 C 语法,我们可以理解 C++ 是 C 的子类,拥有 C 的特性,同时又在这上面扩展了另外的一些特性。

  • 那么 C++ 相比 C 又有什么不同呢?其实最大的不同在于,C 语法的设计思想是面向过程的,而 C++ 语法的设计思想是面向对象。

  • 之所以用 C++ 而不用 C 的目的很简单,Java 也是面向对象的语言,C++ 语言对于 Java 程序员来说比较容易接受,看 C++ 的代码就像在看 Java 代码差不多。

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 在 cpp 文件夹下再创建一个 CMake 文件

NDK 开发:JNI 互调

  • 在 CMake 文件中配置一些 NDK 开发相关的参数

NDK 开发:JNI 互调

  • 在 Gradle 中配置一些 CMake 相关的参数

  • 到这里就结束了?其实还有关键一步,如果我们没有配置好的话,会直接导致我们无法对 C++ 的代码进行断点调试

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 在项目配置选择 Debug 类型,Studio 提供了四种配置

  • Java Only:只断点 Java 层的代码

  • Native Only:只断点 Native 层的代码

  • Detect Automatically:自动检测

  • Dual(Java + Native):两种都用

  • 默认是 Java Only,这样会导致我们无法直接在项目中断点 C/C++ 的代码,所以在这里我们应该选择 Detect Automatically 或者 Dual(Java + Native)选项

  • 到这里就已经成功将一个普通的项目改造成 NDK 项目了,这只是一个开胃菜,接下来让我们正式进入主题

主菜

NDK 开发:JNI 互调

  • 我们创建一个 Java 类,在静态代码块中加载 so 库

NDK 开发:JNI 互调

  • 需要注意的是:这里的 so 库的名称不是根据 cpp 文件的名称来定的,而是根据 CMake 中的配置而定的,只是现在为了演示(偷懒),定义成同一个名称而已。但是 so 库生成的文件名称最终会以 CMake 文件配置的为准。

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 另外系统 API 给我们提供了两种加载 so 的方式,第一种直接加载 apk 中的 so 文件,第二种是通过文件地址来加载 so 文件,一般情况下我们用第一种就可以了,第二种一般是在用在热修复框架上面,它的实现方式也很简单,通过修改静态代码块中的代码,将要加载的 so 的文件重新指向,加载目标从 apk 包转移到应用的内部存储中(data/data/包名/lib),在这之前热修复框架会提前下载好 so 文件存放到此处。

NDK 开发:JNI 互调

  • 为了演示 Java 和 C++ 之间的相互调用,我们创建了两个方法,第一个方法是 Java 调用 C++ 的代码,第二个方法是 C++ 回调 Java 代码

  • 需要留意的是,Java 调用 C++ 的方法要被 native 修饰,表明这是一个本地方法,方法体不需要有任何实现

NDK 开发:JNI 互调

  • 然后我们在 Native 层中创建一个跟 Java 层对应的方法

  • C++ 代码?大多数人看到这里就望而止步了,其实这里面的代码很简单,接下来让我们一步步解析这个 这些代码的含义和作用

NDK 开发:JNI 互调

  • 这个 include 在 Java 层上其实跟 import 差不多,但是在 C++ 文件中它不叫导包,而是叫引入头文件

NDK 开发:JNI 互调

  • 这块我们可以理解成

NDK 开发:JNI 互调

  • Java 中的 JNI 方法要被 native 修饰,那 Native 层中的 JNI 方法同样也不例外

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 需要特别留意的是,Native 层方法的返回值类型的定义位置有点奇特,和 Java 是不太一样的,至于为何 Java 上的返回值是 String 类型,而到了 Native 上的返回值却是 jstring 类型,这个问题待会会讲到。

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • Native 层中的 JNI 方法要和 Java 层中的 JNI 方法要对应上,在 Native 层中 JNI 方法的命名格式为 Java_包名 类名 方法名,之所以用下划线而不用小数点是因为方法名不能带特殊符号,无论是在 Java 代码上还是 C/C++ 代码上,这种情况都是不允许出现的,否则无法编译通过。

NDK 开发:JNI 互调

  • 接下来让我们先看一下这两个参数的含义,我相信大多数人的心里已经有答案了

NDK 开发:JNI 互调

  • 这个 jobject 其实就是外层的 Java 对象,具体是什么对象,代码提示已经告诉我们了

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 而 jstring 其实就是 Java 方法中传入的参数,只不过在 Java 上叫 String,而在 Native 叫 jstring,参数这块也是一一对应的

Java 类型 JNI 别名 C 类型
boolean jboolean unsigned char
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint int
long jlong long
float jfloat float
double jdouble double
String jstring char*
Class jclass /
Object jobject /
  • 我们先来看一张表,关于 Java 类型、JNI 别名、C 类型之间的对应表

  • 由于 Java 和 C 语言之间无法直接调用,但是这两种语言的基本数据类型是不一样的,例如 Java 中有 boolean 类型, 而在 C 中就没有这种类型,但是 C 语言还是有 if else 判断的,那么它是怎么判断 true 或者 false 的呢?正如表上所示,使用 char 类型,当 char 的值是 0 就是 false,非 0 就是 true。

  • 两种语言的数据结构存在巨大差异,基于这种情况,JNI 重新定义了一些类型,以便和 Java 上的类对应上,而这些类本质上还是属于 C 语言中的类。

NDK 开发:JNI 互调

  • 看了这几句代码,忽然心中出现一种似曾相识的感觉,但是始终说不出来是什么

  • 这种实现其实很类似于我们使用 Java 中的反射,属于隐式调用,由于 Native 无法显式调用 Java 代码,所以也采用了隐式调用。而这里面的 API 和 Java 的其实差不多,换汤不换药,这里不再多讲。

NDK 开发:JNI 互调

  • JNIEnv 可以说是整个 JNI 的核心类,是 Java 和 C 通讯的桥梁,它可以协助我们将 JNI 类型转换成 C 类型,不仅如此,调用 Java 对象的方法,获取或者修改属性,都是由 JNIEnv 来做。

  • JNIEnv 是一个结构体的一级指针,与其他类型的对象不一样的地方是,类型后面带了星号,使用的时候不能通过对象点方法名来调用,而是只能通过对象->方法名来调用。

  • 看完了普通 Java 方法调用 Native 方法,接下来看一下静态 Java 方法是如何调用 Native 方法的

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 通过仔细对比,和之前的那种方式其实都差不多,但是有一个地方不太一样

NDK 开发:JNI 互调

  • 如果是 Java 层的 Native 方法是静态的,那么 Native 层中的方法第二个参数类型就是 jclass,这个 jclass 我们可以看做 Java 上面的 Class 类型。这种模式其实跟我们在 Java 方法体上面定义同步锁的差不多,如果被 synchronized 修饰的方法是非静态方法,那么同步锁的锁对象就是 类名.this ,如果被 synchronized 修饰的方法是静态方法,那么同步锁的锁对象就是 类名.class

  • 上面就是 Java 和 Native 方法之间的互相调用,接下来让我们简单看一下 Native 层是如何获取和修改 Java 对象的属性值

NDK 开发:JNI 互调

NDK 开发:JNI 互调

  • 这些代码已经不用再讲了,我相信大部分人都懂

甜点

char* 和 jstring 互转

jstring string;

// jstring 转 char*

const char* cc = env->GetStringUTFChars(string, 0);

// char* 转 jstring

jstring ss = env->NewStringUTF(cc);

打印日志

加入头文件

# include <android/log.h>

打印 char*

const char* cc = "6666666";

__android_log_print(ANDROID_LOG_DEBUG, "TAG", cc, NULL);

日志等级

ANDROID_LOG_VERBOSEANDROID_LOG_DEBUG

ANDROID_LOG_INFO

ANDROID_LOG_WARN

ANDROID_LOG_ERROR

作者:Android轮子哥

链接:https://www.jianshu.com/p/921a5142ae12

关注我获取更多知识或者投稿

NDK 开发:JNI 互调

NDK 开发:JNI 互调

原文  http://mp.weixin.qq.com/s?__biz=MzA3NzMxODEyMQ==&mid=2666456198&idx=1&sn=4f6cf277345c6d09e1c16f8e351ab06b
正文到此结束
Loading...