一.语法分析
接着上一节的NDK例子,能看到native-lib.cpp的代码:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_carey_myndk_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
首先是引入了jni头文件和string库。其中方法写法中有几个关键字:
JNIEXPORT: 是宏定义,表示允许该函数被外部调用。
JNICALL: 是用于约束函数入栈顺序和堆栈清理规则。
如果删除了上面两个关键字,代码也不会报错,系统编译时候会自动加上这俩关键字。
再看方法的名称Java_com_carey_myndk_MainActivity_stringFromJNI,这个是命名规范,是方法的唯一标识,以确保 Java 代码能够正确地调用到对应的本地(native)方法。规则如下:
Java_<包名(用点替换为下划线)><类名(用下划线连接单词)><方法名>
JNIEnv: 是一个结构体指针
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
jmethodID FromReflectedMethod(jobject method)
{ return functions->FromReflectedMethod(this, method); }
jfieldID FromReflectedField(jobject field)
{ return functions->FromReflectedField(this, field); }
// 省略...
jstring NewStringUTF(const char* bytes)
{ return functions->NewStringUTF(this, bytes); }
jobject NewObjectA(jclass clazz, jmethodID methodID, const jvalue* args)
{ return functions->NewObjectA(this, clazz, methodID, args); }
jclass GetObjectClass(jobject obj)
{ return functions->GetObjectClass(this, obj); }
jboolean IsInstanceOf(jobject obj, jclass clazz)
{ return functions->IsInstanceOf(this, obj, clazz); }
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
在这里调用了它的NewStringUTF()方法。这样通过JNIEnv可以访问Java中的方法、属性、创建对象、加载类等等。
jobject: 有两个含义:1.如果你的方法是native方法不是静态方法,那么jobject代表该方法对应的Java对象;2.如果你的native方法是静态方法,那么jobject代表该方法对应的class对象。
jstring: 它是jni中基本的数据类型,通过jni.h头文件中能看到下面代码:
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
为了方便理解,把java、jni和c/c++中类型的对应关系总结下,见下表:
extern "C":是一种特殊的语法,用于告诉C++编译器以C语言的方式来处理被extern "C"
包裹的代码或声明。
二.实现在C中访问java属性成员
在MainActivity中定义属性成员name, 并声明native方法:
public String name = ",你好!"; // 昵称
public native String updateName(); // 更新昵称
创建新的native方法后,会爆红,鼠标放在方法名上点击提示会在cpp文件中自动创建该native方法的实现:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_carey_myndk_MainActivity_updateName(JNIEnv *env, jobject thiz) {
// TODO: implement updateName()
}
我们在这个方法中去实现调用MainActivity中的name属性,代码如下,已增加注释:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_carey_myndk_MainActivity_updateName(JNIEnv *env, jobject jobj) {
// 获取class对象
jclass cls = (*env).GetObjectClass(jobj);
// 获取对象中的属性,参数1 class对象,参数2 属性名,参数3 属性签名
jfieldID fid = (*env).GetFieldID(cls, "name", "Ljava/lang/String;");
// 获取属性值,参数1 class对象 参数2 属性
jstring jStr = (jstring)(*env).GetObjectField(jobj, fid);
// 将jstring类型转成c/c++ 字符串类型
const char* str = (*env).GetStringUTFChars(jStr, NULL);
// 分配足够的空间来存储新字符串,包括 "Carey" 和 str 以及终止符 '\0'
size_t newStrLen = strlen("Carey") + strlen(str) + 1;
// 分配内存空间
char *newStr = (char *)malloc(newStrLen);
if (newStr == NULL) {
// 内存分配失败,处理错误
(*env).ReleaseStringUTFChars(jStr, str);
return (*env).NewStringUTF("");
}
// 拼接字符串
strcpy(newStr, "Carey");
strcat(newStr, str);
// 创建新的Java字符串
jstring newJStr = (*env).NewStringUTF(newStr);
// 更新Java对象中的属性值
(*env).SetObjectField(jobj, fid, newJStr);
// 释放资源
free(newStr);
(*env).ReleaseStringUTFChars(jStr, str);
// 返回新值
return newJStr;
}
最后在MainActivity中调用updateName方法去显示更改后的昵称:
String newName = updateName(); // 更新name
tv.setText(newName);
效果如图:
三.jni中属性和对应的属性签名
在JNI(Java Native Interface)中,属性签名用于标识Java中的字段类型,以便在C/C++代码中正确访问这些字段。JNI中的属性签名与Java中的数据类型有着严格的对应关系。以下是对JNI中属性签名对应关系的详细解释:
1.基本类型签名
2.引用类型签名 JNI中的引用类型签名用于表示Java中的类、接口、数组等复杂类型。
1.类与接口
- 引用类型的签名以“L”开头,以“;”结尾。
- 在“L”和“;”之间,使用“/”代替Java中的点(.)来表示包名和类名。
- 例如,
java.lang.String
的签名为Ljava/lang/String;
。
2.数组
- 数组类型的签名以“[”开头,后跟数组元素的签名。
- 对于多维数组,每增加一维就增加一个“[”。
- 例如,
int[]
的签名为[I
,String[]
的签名为[Ljava/lang/String;
,int[][]
的签名为[[I
。
例子: 比如下面代码,有几个类成员:
public class Person {
private String name;
private int age;
private boolean isAlive;
private String[] hobbies;
}
在JNI中,这些字段的签名将分别为:
name
:Ljava/lang/String;
age
:I
isAlive
:Z
hobbies
:[Ljava/lang/String;
在JNI代码中,当需要访问Java对象的字段时,需要使用GetFieldID
函数来获取字段的ID。此时,需要提供Java类的jclass
对象、字段的名称以及字段的签名。例如:
jclass cls = (*env)->GetObjectClass(jobject);
jfieldID fid_name = (*env)->GetFieldID(cls, "name", "Ljava/lang/String;");
jfieldID fid_age = (*env)->GetFieldID(cls, "age", "I");
jfieldID fid_isAlive = (*env)->GetFieldID(cls, "isAlive", "Z");
jfieldID fid_hobbies = (*env)->GetFieldID(cls, "hobbies", "[Ljava/lang/String;");
通过获取到的字段ID,就可以使用GetObjectField
、GetIntField
、GetBooleanField
等函数来访问Java对象的字段了。
四.总结
学习jni ndk开发还是需要一定的c/c++基础,大家可以补充下这块的基础知识。下一篇我们会继续学习jni语法。喜欢的可以点赞+收藏。感谢!