答: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 类型 | 描述 | 长度(字节) |
---|---|---|---|
boolean | jboolean | unsigned char | 1 |
char | jchar | unsigned short | 2 |
short | jshort | signed short | 2 |
float | jfloat | signed float | 4 |
double | jdouble | signed double | 8 |
int | jint、jsize | signed int | 2 或 4 |
long | jlong | signed long | 4 或 8(LP64) |
byte | jbyte | signed char | 1 |
Class | jclass | Java Class 类对象 | / |
String | jstrting | Java 字符串对象 | / |
Object | jobject | Java 对象 | / |
byte[] | jbyteArray | byte 数组 | / |
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 类型 | 字段描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
引用类型 | 以 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();
}
jmethodID cid;
jmethoidID stringId;
JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) {
cid = (*env)->GetMethodID(env, cls, “callback”, “()V”);
jclass stringClazz = (*env)->FindClass(env,“java/lang/String”);
stringId = (*env)->GetMethodID(env,stringClazz,“”,“([C)V”);
}
3.4 两种缓存 ID 方式的对比和使用场景
在大多数情况下,应该尽可能在静态初始化时缓存字段 ID 和方法 ID,因为使用时缓存存在一些局限性:
- 1、每次使用前都要检查缓存有效;
- 2、字段 ID 和方法 ID 在 Java 类卸载 (unload) 时会失效,因此需要确保类卸载之后不会继续使用这个 ID。而静态初始化时缓存在类加载 (load) 时重新检索 ID,因此不用担心 ID 失效。
当然,使用时缓存也不是一无是处。如果无法修改 Java 代码源码,使用时缓存是必然的选择。另一个优势在于,使用时缓存相当于懒初始化,可以按需检索 ID,而静态初始化时缓存相当于提前初始化,会一次性检索所有 ID。尽管如此,大多数情况下还是会使用静态初始化时缓存。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
总结
其实要轻松掌握很简单,要点就两个:
- 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
- 多练。 (视频优势是互动感强,容易集中注意力)
你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
,让我们一起学习成长!**](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算