NDK _ 带你点亮 JNI 开发基石符文 (一)(1)

  • 2、使用 javac 命令编译源文件,生成 HelloWorld.class 字节码文件;
  • 3、使用 javah 命令导出 HelloWorld.h 头文件,头文件里包含了本地方法的函数原型;
  • 4、使用 C/C++ 实现函数原型;
  • 5、编译本地代码,生成 Hello-World.so 动态库文件;
  • 6、在 Java 代码中调用 System.loadLibrary(…) 加载 so 文件;
  • 7、使用 Java 命令运行 HelloWorld 程序。

源码不在这里展示了,你可以下载 Demo 查看,下载路径:HelloJni。这里只展示 JNI 函数声明:

JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);

2.2 细节解释

下面,我总结了新手容易疑惑的几个问题:

  • 问题 1:头文件为什么要加#ifndef #define #endif?

答:避免头文件被多个文件引用时重复编译,所以把头文件的内容放在 #ifndef 和 #endif 中间。常见模板如下:

#ifndef <宏>
#define <宏>
内容…
#endif

  • 问题 2:为什么要使用 extern “C” ?

答:extern "C"表示即使处于 C++ 环境,也要全部使用 C 的标准进行编译。我们可以在 jni.h 文件中找到答案:因为 JNI 方法中的 JavaVM 和 JNIEnv 最终都调用到了 C 中的 JNIInvokeInterface_ 和 JNINativeInterface_。(todo 论据不充分)

jni.h

struct JNIEnv_;
struct JavaVM_;

#ifdef cplusplus
typedef JNIEnv
JNIEnv;
typedef JavaVM
JavaVM;
#else
typedef const struct JNINativeInterface_ *JNIEnv; // 结构体指针
typedef const struct JNIInvokeInterface_ *JavaVM; // 结构体指针
#endif

无论 C 还是 C++,最终调用到 C 的定义
struct JNIEnv_ {
const struct JNINativeInterface_ *functions;


}

struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;


}

  • 问题 3:为什么 JNI 函数名是 Java_com_xurui_HelloWorld_sayHi?

答:这是 JNI 函数静态注册约定的函数命名规则,当 Java 虚拟机调用 native 方法时,需要执行对应的 JNI 函数,而 JNI 函数注册讨论的就是如何确定 native 方法与 JNI 函数之间的映射关系,有两种方法:静态注册和动态注册。静态注册采用的是基于约定的命名规则,无重载时采用「短名称」规则,有重载时采用「长名称」规则。更多详细的分析在我之前的一篇文章里讨论过:NDK | 带你梳理 JNI 函数注册的方式和时机

  • 问题 4:关键词 JNIEXPORT 是什么意思?

答:JNIEXPORT 是一个宏定义,表示一个函数需要暴露给共享库外部使用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:

Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)

Linux 平台:
#define JNIIMPORT
#define JNIEXPORT attribute ((visibility (“default”)))

  • 问题 5:关键词 JNICALL 是什么意思?

答:JNICALL 是一个宏定义,表示一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:

Windows 平台 :
#define JNICALL __stdcall // __stdcall 是一种函数调用参数的约定 ,表示函数的调用参数是从右往左。

Linux 平台:
#define JNICALL

问题 6:第一个参数 JNIEnv* 是什么? 答:第一个参数是 JNIEnv 指针,指向一个 JNI 函数表。通过这些 JNI 函数可以让本地代码访问 Java 虚拟机的内部数据结构。JNIEnv 指针还有一个作用,就是屏蔽了 Java 虚拟机的内部实现细节,使得本地代码库可以透明地加载到不同的 Java 虚拟机实现中去(牺牲了调用效率)。

问题 7:第二个参数 jobject 是什么? 答:第二个参数根据 native 方法是静态方法还是实例方法有所不同。对于静态 native 方法,第二个参数 jclass 代表 native 方法所在类的 Class 对象。对于实例 native 方法,第二个参数 jobject 代表调用 native 的对象。

2.3 类型的映射关系

Java 类型在 JNI 中都会映射为 JNI 类型,具体映射关系定义在 jni.h 文件中,jbyte, jint 和 jlong 和运行环境有关,定义在 jni_md.h 文件中。总结如下表:

Java 类型JNI 类型描述长度(字节)
booleanjbooleanunsigned char1
charjcharunsigned short2
shortjshortsigned short2
floatjfloatsigned float4
doublejdoublesigned double8
intjint、jsizesigned int2 或 4
longjlongsigned long4 或 8(LP64)
bytejbytesigned char1
ClassjclassJava Class 类对象/
StringjstrtingJava 字符串对象/
ObjectjobjectJava 对象/
byte[]jbyteArraybyte 数组/

3. JNI 调用 Java 代码

这一节我们来讨论如何在 JNI 中访问 Java 字段和方法,在本地代码中访问 Java 代码,需要使用 ID 来访问字段或方法。频繁检索 ID 的过程相对耗时,通常我们还需要缓存 ID 来优化性能的方法。

3.1 JNI 访问 Java 字段

本地代码访问 Java 字段的流程分为两步:

  • 1、通过 jclass 获取字段 ID,例如:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");
  • 2、通过字段 ID 访问字段,例如:Jstr = env->GetObjectField(thiz, Fid);

需要注意:Ljava/lang/String;是实例字段name的字段描述符,严格来说,所谓「字段描述符」其实是 JVM 字节码中描述字段的规则,和 JNI 无直接关系。使用 javap 命令也可以自动生成字段描述符和方法描述符,Android Studio 也会帮助自动生成。完整的字段描述符规则如下表:

Java 类型字段描述符
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV
引用类型以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。
例如 String 的字段描述符为 Ljava/lang/String;

Java 字段分为静态字段和实例字段,本地代码获取或修改 Java 字段主要是使用以下 6 个方法:

  • GetFieldId:获取实例方法的字段 ID
  • GetStaticFieldId:获取静态方法的字段 ID
  • GetField:获取类型为 Type 的实例字段(例如 GetIntField)
  • SetField:设置类型为 Type 的实例字段(例如 SetIntField)
  • GetStaticField:获取类型为 Type 的静态字段(例如 GetStaticIntField)
  • SetStaticField:设置类型为 Type 的静态字段(例如 SetStaticIntField)

native-lib.cpp

extern “C”
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, jobject thiz) {
// 获取 jclass
jclass clz = env->GetObjectClass(thiz);
// 静态字段 ID
jfieldID sFieldId = env->GetStaticFieldID(clz, “sName”, “Ljava/lang/String;”);
// 访问静态字段
if (sFieldId) {
jstring jStr = static_cast(env->GetStaticObjectField(clz, sFieldId));
// 转换为 C 字符串
const char *sStr = env->GetStringUTFChars(jStr, NULL);
LOGD(“静态字段:%s”, sStr);
env->ReleaseStringUTFChars(jStr, sStr);
jstring newStr = env->NewStringUTF(“静态字段 - Peng”);
if (newStr) {
env->SetStaticObjectField(clz, sFieldId, newStr);
}
}
// 实例字段 ID
jfieldID mFieldId = env->GetFieldID(clz, “mName”, “Ljava/lang/String;”);
// 访问实例字段
if (mFieldId) {
jstring jStr = static_cast(env->GetObjectField(thiz, mFieldId));
// 转换为 C 字符串
const char *sStr = env->GetStringUTFChars(jStr, NULL);
LOGD(“实例字段:%s”, sStr);
env->ReleaseStringUTFChars(jStr, sStr);
jstring newStr = env->NewStringUTF(“实例字段 - Peng”);
if (newStr) {
env->SetObjectField(thiz, mFieldId, newStr);
}
}
}

3.2 JNI 调用 Java 方法

本地代码访问 Java 方法与访问 Java 字段类似,访问流程分为两步:

  • 1、通过 jclass 获取「方法 ID」,例如:Mid = env->GetMethodID(jclass, "helloJava", "()V");
  • 2、通过方法 ID 调用方法,例如:env->CallVoidMethod(thiz, Mid);

需要注意:()V是实例方法helloJava的方法描述符,严格来说「方法描述符」是 JVM 字节码中描述方法的规则,和 JNI 无直接关系。

Java 方法分为静态方法和实例方法,本地代码调用 Java 方法主要是使用以下 5 个方法:

  • GetMethodId:获取实例方法 ID
  • GetStaticMethodId:获取静态方法 ID
  • CallMethod:调用返回类型为 Type 的实例方法(例如 GetVoidMethod)
  • CallStaticMethod:调用返回类型为 Type 的静态方法(例如 CallStaticVoidMethod)
  • CallNonvirtualMethod:调用返回类型为 Type 的父类方法(例如 CallNonvirtualVoidMethod)

native-lib.cpp

extern “C”
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, jobject thiz) {
// 获取 jclass
jclass clz = env->GetObjectClass(thiz);
// 静态方法 ID
jmethodID sMethodId = env->GetStaticMethodID(clz, “sHelloJava”, “()V”);
if (sMethodId) {
env->CallStaticVoidMethod(clz, sMethodId);
}
// 实例方法 ID
jmethodID mMethodId = env->GetMethodID(clz, “helloJava”, “()V”);
if (mMethodId) {
env->CallVoidMethod(thiz, mMethodId);
}
}

3.3 缓存 ID
  • 为什么要缓存 ID:访问 Java 层字段或方法时,需要先利用字段名 / 方法名和描述符进行检索,获得 jfieldID / jmethodID。这个检索过程比较耗时,优化方法是将字段 ID 和方法 ID 缓存起来,减少重复检索。

  • 缓存 ID 的方法:缓存字段 ID 和 方法 ID的方法主要有两种:使用时缓存 + 初始化时缓存,主要区别在于缓存发生的时机和缓存 ID 的时效性。

使用时缓存:

使用时缓存是指在首次访问字段或方法时,将字段 ID 或方法 ID 存储在静态变量中。这样在将来再次调用本地方法时,就不需要重复检索 ID 了。例如:

jstring MyNewString(JNIEnv* env, jchar* chars, jint len) {
// 静态字段
static jmethodID cid = NULL;

jclass stringClazz = (*env)->FindClass(env,“java/lang/String”);
if(NULL == cid) {
cid = (*env)->GetMethodID(env,stringClazz,“”,“([C)V”);
}
jcharArray elemArr = (*env)->NewCharArray(env,len);
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
jstring result = (*env)->NewObject(env, stringClazz, cid, elemArr);
(*env)->DeleteLocalRef(env,elemArr);
(*env)->DeleteLocalRef(env,stringClazz);
return result
}

提示: 多个线程访问这个本地方法,会使用相同的缓存 ID,会出现问题吗?不会,多个线程计算的字段 ID 或方法 ID 其实是相同的。

静态初始化时缓存:

静态初始化时缓存是指在 Java 类初始化的时候,提前缓存字段 ID 和方法 ID。例如:

private static native void initIDs();

static {
// Java 类初始化
System.loadLibrary(“InstanceMethodCall”);
initIDs();

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-eEa3W4Xz-1720097857765)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值