初探ndk的世界
Posted 半岛铁盒里的猫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初探ndk的世界相关的知识,希望对你有一定的参考价值。
更多博文,请看音视频系统学习的浪漫马车之总目录
上一篇初探ndk的世界(一)主要介绍了ndk的背景和java和C++如何交互,如果还没看过上一篇,那最好先看一下,因为这一篇将继续上一篇没有讲完的ndk话题,让我们对ndk的世界接触得更加旷阔一点。
ndk相关:
初探ndk的世界(一)
初探ndk的世界(二)
处理Java对象
上一篇曾讲过将Java的String对象作为方法参数传入到C++层中,那么普通的自定义对象又是如何处理的呢?
Native中修改Java传过来的对象
先创建一个Java类Person:
public class Person
private String name;
private int age;
public String getName()
return name;
public void setName(String name)
this.name = name;
public int getAge()
return age;
public void setAge(int age)
this.age = age;
@Override
public String toString()
return "Person" +
"name='" + name + '\\'' +
", age=" + age +
'';
Java层中添加Native方法:
public native Person setInfoForPerson(Person person);
C++层中实现Native方法:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndkdemo_MainActivity_setInfoForPerson(JNIEnv *env, jobject thiz, jobject person)
// 找到对象的Java类
jclass personClass = env->FindClass("com/example/ndkdemo/Person");
// 对应的Java属性
jfieldID name = env->GetFieldID(personClass, "name", "Ljava/lang/String;");
jfieldID age = env->GetFieldID(personClass, "age", "I");
//属性赋值,person为传入的Java对象
env->SetObjectField(person, name, env->NewStringUTF("wangtao"));
env->SetIntField(person, age, 20);
return person;
要对Java对象的属性进行修改依然是反射操作,首先拿到属性的id,然后调用JNIEnv 的set(类型名)Field就可以修改。
还是之前说过的3步曲:
- 获取Java对象对应的class对象在Native层的映射jclass 对象。
- 通过jclass 对象得到属性的id。
- 通过属性id去修改值。
测试下,在Java中MainActivity的onCreate方法中添加:
Person person = setInfoForPerson(new Person());
Log.d("MainActivity","setInfoForPerson:" + person.toString());
运行下:
D/MainActivity: setInfoForPerson:Personname=‘wangtao’, age=20
赋值成功,很稳!
Native创造一个Java对象并返回给Java层
Java中添加Native方法:
public native Person getNewPerson();
Native中实现:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndkdemo_MainActivity_getNewPerson(JNIEnv *env, jobject thiz)
jclass personClass = env->FindClass("com/example/ndkdemo/Person");
// 获取类的构造函数,记住这里是调用无参的构造函数
jmethodID id = env->GetMethodID(personClass, "<init>", "()V");
// 创建一个新的对象
jobject person_ = env->NewObject(personClass, id);
jfieldID name = env->GetFieldID(personClass, "name", "Ljava/lang/String;");
jfieldID age = env->GetFieldID(personClass, "age", "I");
env->SetObjectField(person_, name, env->NewStringUTF("wangtao"));
env->SetIntField(person_, age, 20);
return person_;
和上面一个套路,唯一不同的就是为了创建新对象,要先获得Person的构造方法id,然后通过构造方法id创建Person对象,接着又是上面说的三部曲修改Person对象属性值。
测试一下?不测了,肯定稳的~~
引用
在jni中,无论是从Java层传给Native层或者是Native层创建的Java对象,本质都是Native层持有指向jvm堆中对象的一个引用,因为jvm是采用gc root的引用计数法确定对象是否要被回收,所以当Native层持有了指向Java对象的引用之后,管理对象的生命周期,防止内存泄漏就显得格外重要了。
局部引用
在jni中,无论是从Java层作为方法参数传给Native层或者是Native层方法中创建的Java对象(jobject等jni对象也属于),默认为被局部引用指向,即会在方法返回的时候自动释放引用,也可以在方法返回前手动释放。和Java一样,释放引用的目的是告诉jvm这个对象这边已经不再需要使用,让jvm决定是否回收。
举个栗子:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_testLocalReference(JNIEnv *env, jobject thiz)
jclass personClass1 = env->FindClass("com/example/ndkdemo/Person");
// 获取类的构造函数,记住这里是调用无参的构造函数
jmethodID id = env->GetMethodID(personClass1, "<init>", "()V");
// 创建一个新的对象
jobject person = env->NewObject(personClass1, id);
//也可以显式指定为局部引用
// person = env->NewLocalRef(person);
env->DeleteLocalRef(person);
//后面不再使用person对象
这里person 是在Native中创建的jni对象,本质是指向jvm堆中的person对象的局部引用,一旦引用被释放,后面不能再使用person这个引用。(有兴趣可以试试,保证闪退~)
全局引用
局部引用在Native方法执行完就会自动释放,那假如我们需要一个能跨方法使用,或者跨线程使用的对象呢?当然有局部,就会有全局引用,如果按照Java习惯的写法,就会这样写:
//定义一个“全局引用”
jclass personClass;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_testReference(JNIEnv *env, jobject thiz)
//第二次进入虽然personClass不为null,但是指向的对象已经被释放了,成为悬空指针
if (personClass == nullptr)
personClass = env->FindClass("com/example/ndkdemo/Person");
LOGD("testReference GetMethodID");
// 获取类的构造函数,记住这里是调用无参的构造函数
jmethodID id = env->GetMethodID(personClass, "<init>", "()V");
这样只有第一次调用testReference方法的时候创建了personClass 对象,以后调用testReference方法不再创建,运行下,好吧,成功闪退。
为什么呢?因为在Java这样写,jvm不会看到personClass 引用一直指向堆中对应对象,jvm不会回收。但是这里用一个普通的引用引用该对象,那不好意思,一旦方法执行完,jvm就回收该对象,那么personClass这个引用在第二次执行testReference方法的时候确实不为null,但是它已经成为一个悬空指针。
所以这里使用全局引用才是正道:
//定义一个引用
jobject personGlobalReference;
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndkdemo_MainActivity_testGlobalReference(JNIEnv *env, jobject thiz)
jclass personClass1 = nullptr;
if (personGlobalReference == nullptr)
personClass1 = env->FindClass("com/example/ndkdemo/Person");
// 获取类的构造函数,记住这里是调用无参的构造函数
jmethodID id = env->GetMethodID(personClass1, "<init>", "()V");
// 创建一个新的对象
jobject person = env->NewObject(personClass1, id);
//关键语句,将person 包装为全局引用再赋给personGlobalReference
personGlobalReference = env->NewGlobalRef(person);
jfieldID name = env->GetFieldID(personClass1, "name", "Ljava/lang/String;");
jfieldID age = env->GetFieldID(personClass1, "age", "I");
env->SetObjectField(personGlobalReference, name, env->NewStringUTF("wangtao"));
env->SetIntField(personGlobalReference, age, 20);
return personGlobalReference;
全局引用相当于告诉jvm,这个对象我还要用,你千万不要释放,所以在手动释放之前,jvm不会将该对象释放,所以要切记要在不需要使用该对象的时候手动释放该全局引用:
env->DeleteGlobalRef(personGlobalReference);
同样,一旦释放,就不能再次使用该引用。
弱引用
我们知道,Java为了防止内存泄漏,经常在内部类或者异步等情况下会使用弱引用,那么Native层同样也有这样的需求,又想要有全局引用的效果,又不想因此组织jvm对对象的回收。和Java一样,和全局引用很相似,只是需要再多判断一步弱引用指向的对象是否已经被回收即可:
//定义一个引用
jobject personWeakReference;
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndkdemo_MainActivity_testWeakReference(JNIEnv *env, jobject thiz)
jclass personClass1 = nullptr;
//判断弱引用指向的对象是否已经被回收
jboolean isEqual = env->IsSameObject(personWeakReference, NULL);
if (personWeakReference == nullptr || isEqual)
personClass1 = env->FindClass("com/example/ndkdemo/Person");
// 获取类的构造函数,记住这里是调用无参的构造函数
jmethodID id = env->GetMethodID(personClass1, "<init>", "()V");
// 创建一个新的对象
jobject person = env->NewObject(personClass1, id);
// //关键语句,将person 包装为弱引用再赋给personWeakReference
personWeakReference = env->NewWeakGlobalRef(person);
env->DeleteLocalRef(person);
return personWeakReference;
此时personWeakReference 引用了Java层的person对象,但是并不阻止jvm回收person对象,所以在下次使用的时候,需要听通过env->IsSameObject方法判断弱引用引用的对象是否已经被jvm回收。
处理数组
Native层同样可以处理Java传进来的数组,这里分为2种情况,一种是Native层创建数组再返回给Java,另一种是修改Java传进来的数组:
创建数组再返回给Java
Java创建Native方法:
public native int[] newIntArray();
Native层实现如下:
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_example_ndkdemo_MainActivity_newIntArray(JNIEnv *env, jobject thiz)
jint nativeArr[3] = 1,2,3;
jintArray javaArray = env->NewIntArray(3);
//将C数组的修改同步到Java数组
env->SetIntArrayRegion(javaArray,0,3,nativeArr);
return javaArray;
在Native层中,jintArray 就是Java中int[]“平行世界”的映射,不过在Native中jintArray 的使用确实比较别扭,不能一步到位,要先创建好jint 数组,然后再创建jintArray 对象,最后将jint 数组通过SetIntArrayRegion方法赋值给jintArray ,然后把jintArray 返回给Java。
修改Java传进来的数组
这里又分为2种方式:
通过操作数组副本修改Java数组
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_example_ndkdemo_MainActivity_changeArrayByCopy(JNIEnv *env, jobject thiz, jintArray javaArray)
//获得Java传递进来数组的长度
jsize length = env->GetArrayLength(javaArray);
//定义一个C数组
//将Java数组区复制到jint数组中
jint nativeArr[length];
env->GetIntArrayRegion(javaArray,0,length,nativeArr);
//修改元素的值
for(int i=0;i<length;i++)
nativeArr[i]=nativeArr[i]*2;
//从C数组向Java数组提交所做的修改。0为同步数据的起始index,length为同步数据个数
env->SetIntArrayRegion(javaArray,0,length,nativeArr);
return javaArray;
这里核心就是先通过GetIntArrayRegion拷贝传进来的Java数组到jint数组nativeArr,修改nativeArr后再通过SetIntArrayRegion方法将nativeArr同步到Java数组。
通过数组指针修改Java数组
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_MainActivity_getArraySumByPointer(JNIEnv *env, jobject thiz,jintArray javaArray)
jint* nativeArray;
jboolean isCopy;
jint sum = 0;
jsize length = env->GetArrayLength(javaArray);
//用GetArrayElements函数获取指向数组元素的直接指针,isCopy表示指针指向的数组是否是从Java数组拷贝出来的副本
nativeArray = env->GetIntArrayElements(javaArray, &isCopy);
for(int i=0;i<length;i++)
sum += nativeArray[i];
//释放nativeArray指针,并将数据同步到Java数组如果GetIntArrayElements有发生Copy
env->ReleaseIntArrayElements(javaArray, nativeArray, 0);
return sum;
这种方式是先通过GetIntArrayElements得到数组的指针,这里注意第二个参数,是一个返回值参数,系统通过这个参数告诉我们返回的指针指向的数组是否是原Java数组还是原Java数组的拷贝。
这里操作数组结束之后,切记要调用ReleaseIntArrayElements释放指针nativeArray,这个方法还有一个重点,就是在GetIntArrayElements返回的isCopy是true的情况下,还能将对副本的处理同步到Java数组。
第三个参数是指明具体处理方式:
mode actions
0 copy back the content and free the elems buffer(拷贝副本内容到原始数组并且释放数组指针)
JNI_COMMIT copy back the content but do not free the elems buffer(拷贝副本内容到原始数组,但不释放数组指针)
JNI_ABORT free the buffer without copying back the possible changes(释放数组指针,但是不拷贝副本内容到原始数组)
方法动态注册
之前讲的Native都是Java_ + 类A全名 +方法b拼接而成的名字去和Java对应的Native方法做映射的,这里的缺点也是很明显的,就是一旦加载动态链接库的Java类修改名字,那Native的方法也得全部修改,于是除了这一种,还有另一种应对修改更加得心应手的方法注册方式:动态注册。
何为动态注册,就是在执行Native代码的某个时刻手动注册这些方法,这里jni选择的时刻就是当动态链接库加载的时候(即Java中调用System.loadLibrary的时候)。
这里步骤分为3步:
- 创建好Java的Native方法和对应的C++层方法,注意现在C++方法的名字可以自由发挥。
- 在C++中创建一个JNINativeMethod数组,指明C++方法和Java方法的映射关系。
- 在C++的JNI_OnLoad方法中完成对Native方法的注册。
以下详细说明:
步骤1:
Java层创建2个Native方法:
native void dynamicNative();
native String dynamicNative(int i);
这里为了更好地区分不同方法,所以专门使用了2个重载方法。
C++层也创建2个方法,名字随意:
extern "C"
JNIEXPORT void JNICALL
dynamicNative1(JNIEnv *env, jobject thiz)
LOGD("dynamicNative1 动态注册");
extern "C"
JNIEXPORT jstring JNICALL
dynamicNative2(JNIEnv *env, jobject thiz, jint i)
LOGD("dynamicNative2 动态注册");
return env->NewStringUTF("我是动态注册的dynamicNative2方法");
步骤2:
创建一个JNINativeMethod数组,指明C++方法和Java方法的映射关系:
//需要动态注册的方法数组
static const JNINativeMethod mMethods[] =
"dynamicNative","()V", (void *)dynamicNative1,
"dynamicNative", "(I)Ljava/lang/String;", (jstring *)dynamicNative2
;
JNINativeMethod 是一个结构体:
typedef struct
//Java的Native方法名
const char* name;
//Java的Native方法描述符
const char* signature;
//C++中的函数指针
void* fnPtr;
JNINativeMethod;
步骤3:
首先要讲下JNI_OnLoad函数是什么?
JNI_OnLoad函数是当动态链接库被加载时回调的一个函数,该函数返回当前动态链接库需要的jni版本给jvm,一般可以做一些初始化的工作,比如方法的注册。
JavaVM* _vm = nullptr;
jint JNI_OnLoad(JavaVM* vm, void* reserved)
//JavaVM* vm是代表虚拟机的一个对象
_vm = vm;
JNIEnv* env = NULL;
//获得当前线程的JniEnv
int r = vm->GetEnv((void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK)
return -1;
jclass mainActivityCls = env->FindClass( mClassName);
// 注册 如果小于0则注册失败
r = env->RegisterNatives(mainActivityCls,mMethods,2);
if(r != JNI_OK )
return -1;
return JNI_VERSION_1_4;
在上一篇文章说过,JNIEnv对象是线程独有的,这里可以通过参数JavaVM* vm,来获取当前线程对应的JNIEnv指针,再通过JNIEnv获取加载的Java类,这里即MainActivity的jclass对象,然后通过env->RegisterNatives(mainActivityCls,mMethods,2);将方法映射数组和加载的Java类进行最终的动态注册。
jni创建线程
Java层能够创建新线程,当然C++层肯定也可以,当然,当C++创建的线程需要和Java层进行交互的时候,需要一些特殊的处理。
这里的例子是Java调用一个Native方法在C++创建一个线程,然后回调一个Java方法。
Java中创建一个Native和一个普通Java方法:
//触发C++创建线程的方法
native void createNativeThread();
//C++创建的线程回调的Java方法
private void callBackForNewThread()
Log.d("MainActivity","callBackForNewThread Thread:" + Thread.currentThread());
C++中创建createNativeThread对应的方法:
jobject _instance = nullptr;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_createNativeThread(JNIEnv *env, jobject thiz)
pthread_t pid;
//用全局引用将MainActivity实例保存起来,供子线程使用
_instance = env->NewGlobalRef(thiz);
//创建新线程运行task方法
pthread_create(&pid,0,task,0);
新线程具体运行的函数:
void *task(void *args)
JNIEnv *env;
//将本地当前线程附加到jvm,并获得jnienv
//成功则返回0
_vm->AttachCurrentThread(&env, 0);
//获取jclass对象
jclass activityClass = env->GetObjectClass(_instance);
//获取Java callBackForNewThread方法的jmethodID
jmethodID callBackMethod = env->GetMethodID(activityClass, "callBackForNewThread", "()V");
std::string hello = "I am CallBack";
jstring s = env->NewStringUTF(hello.c_str());
//调用Java方法
env->CallVoidMethod(_instance, callBackMethod);
//分离
_vm->DetachCurrentThread();
return 0;
关键点在于需要调用
_vm->AttachCurrentThread(&env, 0);
获取对应线程的JNIEnv 对象,拿到JNIEnv对象,就相当于拿到通往Java平行世界的钥匙,然后再按照之前的反射方式调用Java层的callBackForNewThread方法。
最后记得调用:
//分离线程
_vm->DetachCurrentThread();
测试下,在MainActivity的onCreate方法中添加:
createNativeThread();
Log.d("MainActivity","main Thread:" + Thread.currentThread());
运行下:
D/MainActivity: main Thread:Thread[main,5,main]
D/MainActivity: callBackForNewThread Thread:Thread[Thread-3,10,main]
前一个log是打印出运行Activity的主线程,后一个log是运行callBackForNewThread方法的线程,即C++中创建的线程。
这是一个好习惯,因为它可以释放所有该线程持有的锁,以避免阻塞其他锁甚至引起死锁。
总结
本文介绍了ndk中C++层如何处理Java对象、数组以及方法的动态注册、C++创建新线程并在新线程中和Java交互的内容。下一篇终于要开始真正的主题,开始真正进入音视频的世界~ 下一篇:音视频开发基础知识之YUV颜色编码
如果觉得本文有帮助,别忘了点赞关注哦~
以上是关于初探ndk的世界的主要内容,如果未能解决你的问题,请参考以下文章