NDK 开发:JNI 互调

上一篇:NDK 开发:环境配置

本文章所用的工具版本

  • 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_VERBOSE
ANDROID_LOG_DEBUG
ANDROID_LOG_INFO
ANDROID_LOG_WARN
ANDROID_LOG_ERROR
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容