初探ndk的世界

Posted 易水南风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初探ndk的世界相关的知识,希望对你有一定的参考价值。

上一篇初探ndk的世界(一)主要介绍了ndk的背景和java和C++如何交互,如果还没看过上一篇,那最好先看一下,因为这一篇将继续上一篇没有讲完的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步曲:

  1. 获取Java对象对应的class对象在Native层的映射jclass 对象。
  2. 通过jclass 对象得到属性的id。
  3. 通过属性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步:

  1. 创建好Java的Native方法和对应的C++层方法,注意现在C++方法的名字可以自由发挥。
  2. 在C++中创建一个JNINativeMethod数组,指明C++方法和Java方法的映射关系。
  3. 在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交互的内容。下一篇终于要开始真正的主题,开始真正进入音视频的世界~

如果觉得本文有帮助,别忘了点赞关注哦~

以上是关于初探ndk的世界的主要内容,如果未能解决你的问题,请参考以下文章

初探ndk的世界

初探ndk的世界

初探ndk的世界

初探ndk的世界

Android Studio NDK初探

“小世界理论”初探