本文章所用的工具版本
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 项目
创建一个 cpp 文件夹,这个文件夹和 java 是同级目录
然后在这个文件夹下面创建一个 cpp 文件
cpp 文件其实就是 c++ 源码,到了这里可能大多数人又有一个疑问涌上心头,刚刚不是说 Java 和 C,怎么到这里就变成 C++ 了呢?
这里解释一下,C++ 是 C 的超集,兼容大部分 C 语法,我们可以理解 C++ 是 C 的子类,拥有 C 的特性,同时又在这上面扩展了另外的一些特性。
那么 C++ 相比 C 又有什么不同呢?其实最大的不同在于,C 语法的设计思想是面向过程的,而 C++ 语法的设计思想是面向对象。
之所以用 C++ 而不用 C 的目的很简单,Java 也是面向对象的语言,C++ 语言对于 Java 程序员来说比较容易接受,看 C++ 的代码就像在看 Java 代码差不多。
在 cpp 文件夹下再创建一个 CMake 文件
在 CMake 文件中配置一些 NDK 开发相关的参数
在 Gradle 中配置一些 CMake 相关的参数
到这里就结束了?其实还有关键一步,如果我们没有配置好的话,会直接导致我们无法对 C++ 的代码进行断点调试
在项目配置选择 Debug 类型,Studio 提供了四种配置
Java Only:只断点 Java 层的代码
Native Only:只断点 Native 层的代码
Detect Automatically:自动检测
Dual(Java + Native):两种都用
默认是 Java Only,这样会导致我们无法直接在项目中断点 C/C++ 的代码,所以在这里我们应该选择 Detect Automatically 或者 Dual(Java + Native)选项
到这里就已经成功将一个普通的项目改造成 NDK 项目了,这只是一个开胃菜,接下来让我们正式进入主题
主菜
我们创建一个 Java 类,在静态代码块中加载 so 库
需要注意的是:这里的 so 库的名称不是根据 cpp 文件的名称来定的,而是根据 CMake 中的配置而定的,只是现在为了演示(偷懒),定义成同一个名称而已。但是 so 库生成的文件名称最终会以 CMake 文件配置的为准。
另外系统 API 给我们提供了两种加载 so 的方式,第一种直接加载 apk 中的 so 文件,第二种是通过文件地址来加载 so 文件,一般情况下我们用第一种就可以了,第二种一般是在用在热修复框架上面,它的实现方式也很简单,通过修改静态代码块中的代码,将要加载的 so 的文件重新指向,加载目标从 apk 包转移到应用的内部存储中(data/data/包名/lib),在这之前热修复框架会提前下载好 so 文件存放到此处。
为了演示 Java 和 C++ 之间的相互调用,我们创建了两个方法,第一个方法是 Java 调用 C++ 的代码,第二个方法是 C++ 回调 Java 代码
需要留意的是,Java 调用 C++ 的方法要被 native 修饰,表明这是一个本地方法,方法体不需要有任何实现
然后我们在 Native 层中创建一个跟 Java 层对应的方法
C++ 代码?大多数人看到这里就望而止步了,其实这里面的代码很简单,接下来让我们一步步解析这个 这些代码的含义和作用
这个 include 在 Java 层上其实跟 import 差不多,但是在 C++ 文件中它不叫导包,而是叫引入头文件
这块我们可以理解成
Java 中的 JNI 方法要被 native 修饰,那 Native 层中的 JNI 方法同样也不例外
需要特别留意的是,Native 层方法的返回值类型的定义位置有点奇特,和 Java 是不太一样的,至于为何 Java 上的返回值是 String 类型,而到了 Native 上的返回值却是 jstring 类型,这个问题待会会讲到。
Native 层中的 JNI 方法要和 Java 层中的 JNI 方法要对应上,在 Native 层中 JNI 方法的命名格式为 Java_包名类名方法名,之所以用下划线而不用小数点是因为方法名不能带特殊符号,无论是在 Java 代码上还是 C/C++ 代码上,这种情况都是不允许出现的,否则无法编译通过。
接下来让我们先看一下这两个参数的含义,我相信大多数人的心里已经有答案了
这个 jobject 其实就是外层的 Java 对象,具体是什么对象,代码提示已经告诉我们了
而 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 语言中的类。
看了这几句代码,忽然心中出现一种似曾相识的感觉,但是始终说不出来是什么
这种实现其实很类似于我们使用 Java 中的反射,属于隐式调用,由于 Native 无法显式调用 Java 代码,所以也采用了隐式调用。而这里面的 API 和 Java 的其实差不多,换汤不换药,这里不再多讲。
JNIEnv 可以说是整个 JNI 的核心类,是 Java 和 C 通讯的桥梁,它可以协助我们将 JNI 类型转换成 C 类型,不仅如此,调用 Java 对象的方法,获取或者修改属性,都是由 JNIEnv 来做。
JNIEnv 是一个结构体的一级指针,与其他类型的对象不一样的地方是,类型后面带了星号,使用的时候不能通过对象点方法名来调用,而是只能通过对象->方法名来调用。
看完了普通 Java 方法调用 Native 方法,接下来看一下静态 Java 方法是如何调用 Native 方法的
通过仔细对比,和之前的那种方式其实都差不多,但是有一个地方不太一样
如果是 Java 层的 Native 方法是静态的,那么 Native 层中的方法第二个参数类型就是 jclass,这个 jclass 我们可以看做 Java 上面的 Class 类型。这种模式其实跟我们在 Java 方法体上面定义同步锁的差不多,如果被 synchronized 修饰的方法是非静态方法,那么同步锁的锁对象就是
类名.this
,如果被 synchronized 修饰的方法是静态方法,那么同步锁的锁对象就是类名.class
上面就是 Java 和 Native 方法之间的互相调用,接下来让我们简单看一下 Native 层是如何获取和修改 Java 对象的属性值
这些代码已经不用再讲了,我相信大部分人都懂
甜点
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
关注我获取更多知识或者投稿