上篇文章NDK(一):编写第一个JNI项目,讲到了怎样用Android Studio创建一个项目去编写JNI代码,接下来,就具体介绍JNI与Java之间的调用。
包括简单的参数传递回调,创建pthread线程,以及静动态注册
JNI数据类型
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型 |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
宏定义输出日志语句
// native-lib.cpp文件
#include
// 宏定义jni的输出日志语句,方便使用
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,"StudyNDK",__VA_ARGS__);
参数介绍
extern "C”
指示编译器这部分代码按C语言进行编译
JNIEXPORT和JNICALL
JNIEXPORT 和 JNICALL,定义在jni_md.h
头文件中,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数,在Java虚拟机加载的时候会链接对应的native方法。
JNIEXPORT:
-
在 Windows 中,定义为
#define JNIEXPORT __declspec(dllexport)
因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。
-
在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为
define JNIEXPORT __attribute__ ((visibility ("default")))
JNICALL:
- 在Windows中定义为:
_stdcall
,一种函数调用约定 - 在类Unix中无定义,可以省略不加
Java调用Native
不传递参数
// java文件
Log.i(TAG, "stringFromJNI() = "+stringFromJNI());
public native String stringFromJNI();
// native-lib.cpp
extern "C" JNIEXPORT jstring
JNICALL
Java_com_guidongyuan_studyndk_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
LOGI("Java调用native")
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
//output
Java调用native
stringFromJNI() = Hello from C++
传递变量
// java文件
Log.i(TAG, "passValueToJNI() = "+passValueToJNI(100, "passToJNI"));
/** 传递基本数据类型和String类型参数给JNI */
public native String passValueToJNI(int intValue,String strValue);
// native-lib.cpp文件
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passValueToJNI(JNIEnv *env, jobject instance,
jint intValue, jstring strValue_) {
LOGI("基本数据类型:intValue = %d\n", intValue);
const char *strValue = env->GetStringUTFChars(strValue_, 0);
LOGI("string数据类型:strValue = %s\n", strValue);
// 释放
env->ReleaseStringUTFChars(strValue_, strValue);
return env->NewStringUTF("passValueToJNI 回调");
}
// output
基本数据类型:intValue = 100
string数据类型:strValue = passToJNI
passValueToJNI() = passValueToJNI 回调
传递数组
// java文件
int[] intArrays = new int[]{0, 1, 2};
String[] strArrays = new String[]{"zero","first","second"};
Log.i(TAG, "passArrayToJNI() = "+passArrayToJNI(intArrays, strArrays));
// 验证在JNI的修改是否有效
for (int i = 0;i < intArrays.length;i++){
Log.i(TAG, "修改后 int数据为:i = " + intArrays[i]);
}
/** 传递数组给JNI */
public native String passArrayToJNI(int[] intArray, String strArray[]);
// native-lib.cpp文件
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passArrayToJNI(JNIEnv *env, jobject instance, jintArray intArray_, jobjectArray strArray) {
// 获得基本数据类型数组
// 参数intArray_类型为jintArray,可以看到是typedef _jintArray* jintArray;这样定义的
// 传递的为指针地址,指向数组首元素地址
// 获取数组长度
int intLength = env->GetArrayLength(intArray_);
// 如果为Boolean则调用GetBooleanArrayElements不同的对应
jint *intArray = env->GetIntArrayElements(intArray_, NULL);
for (int i = 0; i < intLength; ++i) {
LOGI("int数据为:i = %d ",*(intArray+i));
// 因为传递的为指针地址,所以,在这里进行修改,会影响到java代码中的值
*(intArray + i) = *(intArray + i) + 10;
}
// 释放
env->ReleaseIntArrayElements(intArray_, intArray, 0);
// 获得字符串类型数组
int strLength = env->GetArrayLength(strArray);
for (int i = 0; i < strLength; ++i) {
// object类型转成jstring类型
jstring str = static_cast(env->GetObjectArrayElement(strArray, i));
// 需要转成char* 类型再输出,否则会出错
const char *c_str = const_cast(env->GetStringUTFChars(str, 0));
LOGI("string数据为 i = %s ",c_str);
env->ReleaseStringUTFChars(str, c_str);
}
return env->NewStringUTF("passArrayToJNI 回调");
}
// output
int数据为:i = 0
int数据为:i = 1
int数据为:i = 2
string数据为 i = zero
string数据为 i = first
string数据为 i = second
passArrayToJNI() = passArrayToJNI 回调
// 可以看到,传递到JNI被修改的参数有效了
修改后 int数据为:i = 10
修改后 int数据为:i = 11
修改后 int数据为:i = 12
传递对象
传递的对象,通过GetObjectClass()
可以获取到对应的class对象,然后通过该class对象调用对象的成员属性,方法。这里先不介绍,放到Native调用Java章节再介绍。
// java文件
Log.i(TAG, "passBeanToJNI = "+passBeanToJNI(new Bean()));
/** 传递对象 */
public native String passBeanToJNI(Bean bean);
// native-lib.cpp
extern "C"
JNIEXPORT jstring JNICALL
Java_com_guidongyuan_studyndk_MainActivity_passBeanToJNI(JNIEnv *env, jobject instance, jobject bean) {
// 传递对象
// 获取java对应的class对象
jclass beanClass = env->GetObjectClass(bean);
return env->NewStringUTF("passBeanToJNI 回调");
}
// output
passBeanToJNI = passBeanToJNI 回调
Native调用Java
基本数据类型签名
基本数据类型的签名采用一系列大写字母来表示, 如下表所示:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
调用Java方法
Native调用Java的流程
-
GetObjectClass()
获取jclass对象 -
GetMethodID()
传入jclass对象、方法名称和方法参数数据类型签名,获取方法Id -
CallVoidMethod()
传入方法id和参数
// Java文件
public native void callJNI();
public void callFromJNI(){
Log.i(TAG, "callFromJNI");
}
public void callFromJNI(int i) {
Log.i(TAG, "callFromJNI i = "+i);
}
public void callFromJNI(String string) {
Log.i(TAG, "callFromJNI string = "+string);
}
public static void callStaticFromJNI(){
Log.i(TAG, "callStaticFromJNI");
}
// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {
// 获取jclass对象
jclass _jclass = env->GetObjectClass(instance);
// 调用无参方法
jmethodID methodId = env->GetMethodID(_jclass, "callFromJNI", "()V");
env->CallVoidMethod(instance, methodId);
// 调用int类型参数的方法
jmethodID methodWithIntId = env->GetMethodID(_jclass, "callFromJNI", "(I)V");
env->CallVoidMethod(instance, methodWithIntId, 100);
// 调用string类型参数的方法
jmethodID methodWithStringId = env->GetMethodID(_jclass, "callFromJNI", "(Ljava/lang/String;)V");
env->CallVoidMethod(instance, methodWithStringId, env->NewStringUTF("callJNI 回调"));
// 调用静态方法
jmethodID methodStaticId = env->GetStaticMethodID(_jclass, "callStaticFromJNI", "()V");
// 第一个参数,传入的为jclass对象,而不是jobject
env->CallStaticVoidMethod(_jclass, methodStaticId);
}
// output
callFromJNI
callFromJNI i = 100
callFromJNI string = callJNI 回调
callStaticFromJNI
调用Java变量
// Java文件
public int age = 18;
public String name = "yuan";
public static String school = "GDUT";
callJNI();
Log.i(TAG, "修改后 age = " +age + ",name = "+name + ",school = "+school);
// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {
// 调用int数据类型变量
jfieldID fieldIntId = env->GetFieldID(_jclass, "age", "I");
int ageInt = env->GetIntField(instance, fieldIntId);
LOGI("获取Java变量 age = %d ", ageInt);
// 修改变量内容
env->SetIntField(instance, fieldIntId, 19);
// 调用String数据类型变量
jfieldID fieldStrId = env->GetFieldID(_jclass, "name", "Ljava/lang/String;");
jstring nameStr = static_cast(env->GetObjectField(instance, fieldStrId));
const char* nameStrChar = env->GetStringUTFChars(nameStr, 0);
LOGI("获取Java变量 name = %s ",nameStrChar);
// 修改string变量内容
env->SetObjectField(instance, fieldStrId, env->NewStringUTF("Change yuan"));
// 调用静态变量
jfieldID fieldStaticId = env->GetStaticFieldID(_jclass, "school", "Ljava/lang/String;");
jstring schoolStr = static_cast(env->GetStaticObjectField(_jclass, fieldStaticId));
const char* schoolStrChar = env->GetStringUTFChars(schoolStr, 0);
LOGI("获取Java静态变量 school = %s ",schoolStrChar);
// 修改static变量内容
env->SetStaticObjectField(_jclass, fieldStaticId, env->NewStringUTF("Change GDUT"));
}
// output
获取Java变量 age = 18
获取Java变量 name = yuan
获取Java静态变量 school = GDUT
修改后 age = 19,name = Change yuan,school = Change GDUT
创建Java对象
上面介绍的,都是通过传递当前对象到native方法中,如果要使用其他对象,就需要自行创建了。
// Bean.java文件
package com.guidongyuan.studyndk;
public class Bean {
private static final String TAG = "StudyNDK";
private int i = 0;
public int getI() {
Log.i(TAG, "getI i = "+i);
return i;
}
public void setI(int i) {
Log.i(TAG, "setI i = "+i);
this.i = i;
}
}
// native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_callJNI(JNIEnv *env, jobject instance) {
// 创建对象,需要传递完整包路径
jclass beanClass = env->FindClass("com/guidongyuan/studyndk/Bean");
jmethodID constructMethodId = env->GetMethodID(beanClass, "", "()V");
jobject beanObject = env->NewObject(beanClass, constructMethodId);
jmethodID getIId = env->GetMethodID(beanClass,"getI","()I");
jint iValue = env->CallIntMethod(beanObject,getIId);
LOGI("获取Java 变量 i = %d",iValue);
jmethodID setIId = env->GetMethodID(beanClass, "setI", "(I)V");
env->CallVoidMethod(beanObject, setIId, 1);
}
// output
getI i = 0
获取Java 变量 i = 0
setI i = 1
创建Native线程
// Java文件
createThread();
public native void createThread();
public void updateUI(){
Log.i(TAG, "updateUI");
}
// native-lib.cpp
JavaVM *_vm;
jobject _instance = 0;
// Java中执行完System.loadLibrary,就会自动调用该方法
int JNI_OnLoad(JavaVM *vm, void *re) {
LOGI("调用JNI_Onload");
_vm = vm;
return JNI_VERSION_1_6;
}
void *pthreaTask(void *args){
LOGI("pthreaTask");
JNIEnv *env = nullptr;
jint i = _vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK){
return 0;
}
jclass _jclass = env->GetObjectClass(_instance);
jmethodID fieldId = env->GetMethodID(_jclass, "updateUI", "()V");
env->CallVoidMethod(_instance, fieldId);
_vm->DetachCurrentThread();
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_createThread(JNIEnv *env, jobject instance) {
// 把instant设置为全局引用
_instance = env->NewGlobalRef(instance);
pthread_t _pthread;
pthread_create(&_pthread, 0, pthreaTask, NULL);
// 避免线程方法还没有执行,该方法就先执行完毕,把instance释放掉了
pthread_join(_pthread, 0);
env->DeleteGlobalRef(_instance);
LOGI("createThread 执行结束");
}
// output
pthreaTask
updateUI
createThread 执行结束
JavaVM与JNIEnv
JNIEnv
JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的。JNIEnv的主要作用有两点:
- 调用Java的方法。
- 操作Java(获取Java中的变量和对象等等)
通过上面的代码实例,可以看到JNIEnv的作用,但是,在多线程中,因为不能跨进程,所以,需要通过JavaVM获取当前线程的JNIEnv。
JavaVM
JavaVM,是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。
通过JavaVM的AttachCurrentThread()
函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。记得在线程退出前,要调用DetachCurrentThread()
函数来释放资源。
JNI_OnLoad
怎样获取到JavaVM的引用呢?可以通过JNI_Onload
函数。Java代码在调用System.loadLibrary()
函数时, 内部就会去查找so中的JNI_OnLoad
函数,如果存在此函数则会调用。并且会传递JavaVM的引用,把其设置为全局引用就可以了。
// JNI_OnLoad会告诉 VM 此 native 组件使用的 JNI 版本
// 对应了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6
// 如果使用JDK1.8,也有 JNI_VERSION_1_8
// 使用上,使用哪个都可以
JavaVM *_vm;
int JNI_OnLoad(JavaVM *vm,void *re){
_vm = vm;
return JNI_VERSION_1_6;
}
pthread_create参数传递
上面实例代码中,把instance传递到其他线程中,是通过声明为一个全局引用,pthread_create()
方法中,发现第4个参数为传递的变量,在这里花费很多时间,一直在探索为啥把instance传递进去,确实无效的。测试结果发现,如果是基本数据类型,是可以传递的,jobject确实没办法。具体原因暂时一直没找到。
// Java文件
// native-lib.cpp
void *pthreaTask(void *args){
LOGI("pthreaTask");
JNIEnv *env;
jint i = _vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK){
return 0;
}
jint value = *((jint *)args);
LOGI("jint 值 value = %d",value);
_vm->DetachCurrentThread();
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_guidongyuan_studyndk_MainActivity_createThread(JNIEnv *env, jobject instance) {
// 传递int数据类型
int i = 100;
int* pInt = &i;
pthread_t _pthread;
pthread_create(&_pthread, 0, pthreaTask, pInt);
pthread_join(_pthread, 0);
}
// output
jint 值 value = 100
静态注册和动态注册
静态注册:在此之前我们一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 来进行与java方法的匹配,这种方式我们称之为静态注册。
动态注册:动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式。
不过在Android Studio中,写了native方法后可以自动添加静态方法,也不太需要去写特别上的方法,所以,就看使用的取舍了。
上面介绍的,通过JNI_OnLoad()
可以获取到JavaVM的引用,该方法在动态注册中也使用到。
// Java文件
public native void dynamicRegister();
public native static String dynamicRegisterStatic(int i);
// native-lib.cpp
// 需要动态注册native方法的类名
// 混淆的时候,要注意,不能被混淆,否则会失败
static const char* mClassName = "com/guidongyuan/studyndk/MainActivity";
void dynamicRegisterNative(JNIEnv *env, jobject instance){
LOGI("dynamicRegisterNative 动态注册");
}
jstring dynamicRegisterNativeStatic(JNIEnv *env, jobject instance, jint i){
return env->NewStringUTF("dynamicRegisterNativeStatic 动态注册");
}
// 需要动态注册的方法数组
// {"java本地方法名","签名",java方法对应jni中的方法名}
static const JNINativeMethod method[] = {
{"dynamicRegister", "()V", (void *)dynamicRegisterNative},
{"dynamicRegisterStatic", "(I)Ljava/lang/String;", (jstring *)dynamicRegisterNativeStatic}
};
int JNI_OnLoad(JavaVM *vm, void *re) {
LOGI("调用JNI_Onload");
_vm = vm;
JNIEnv *env = nullptr;
int i = vm->AttachCurrentThread(&env, 0);
if (i != JNI_OK) {
return 0;
}
jclass mainActivityCls = env->FindClass(mClassName);
// 传递方法数组的名称和数目
// 注册 如果小于0则注册失败
i = env->RegisterNatives(mainActivityCls,method,sizeof(method)/ sizeof(JNINativeMethod));
if(i != JNI_OK )
{
return -1;
}
return JNI_VERSION_1_6;
}
完整代码
- 上面的实例代码都上传到github地址,可以进行详细查看
参考资料
Android JNI编程—JNI基础
-
googlesamples/android-ndk
google官方ndk的demo,基本覆盖了各种jni的写法,强烈推荐参考的dmeo
-
JNI完全指南(十)——JavaVM与JNIEnv
对JavaVM与JNIEnv介绍得比较详细的一篇文章
Android深入理解JNI(二)类型转换、方法签名和JNIEnv
-
Android NDK — Native 线程 pthread
详细介绍pthread的文章