文章目录

  • 前言
  • 一、JNIEnv和JavaVM
  • 1.JNIEnv
  • 2.JavaVM
  • 二、JNI方法的静态注册和动态注册
  • 1.静态注册
  • 2.动态注册
  • 三.Java层和native层的类型对应关系以及相互传值
  • 1.Java类型对应jni类型
  • 2.Java和native相互传值
  • 四.NDK 使用进阶
  • 1.三种引用的区别和使用
  • 2.在native创建线程,调用Java层的方法。
  • 总结



前言

随着现在5g时代的到来,短视频技术越发的流行了,大家都在争着做短视频的app。当初看到抖音的时候我震惊了,和我以前用支付宝和淘宝一样震惊,用淘宝和支付宝结合,我能在家买到我想要的任何东西,我可以用支付宝充校园卡,交水电费等等,非常的方便。抖音出来的时候我看到的是各种漂亮的小姐姐,有趣的电影短视频剪辑,美食菜谱,生活技巧等应有尽有…,然而最吸引我的还是抖音电商。让大家能在娱乐的时候购物,这是一个双重体验。逛淘宝京东都是目的性的,我想起了啥才去逛,比如我想买衣服了,想买手机了我才想起他们,但是用抖音是在娱乐的过程中发现我还缺啥。真的很方便,我预言,在未来的十年,短视频电商一定会渐渐的抢占淘宝和京东这类的电商的份额。并且在其中占大头。说了这么多其实就是想赶紧抓住这个机会,学习音视频的开发。但是说到音视频的开发就不得不涉及到Android NDK的开发,今天的文章主要是为了介绍Android NDK的开发入门知识,虽说是入门,但是不是零基础入门那种,如果连ndk开发的基本流程都不知道的话请移步其他博客。本篇文章建议坐在电脑旁跟着敲一遍,印象会更深刻。


一、JNIEnv和JavaVM

1.JNIEnv

JNIEnv 可以理解成一个上下文,里面封装了jni的方法指针,JNIenv只在创建它的线程里有效,不能跨线程传递,不同线程之间的JNIEnv相互独立,互不影响

android ndk 编译原理分析 android ndk视频教程_音视频

2.JavaVM

javaVm是虚拟机在JNI层的代表,每个进程只有一个,所有的线程共享一个JavaVM.当我们在native层创建了一个线程,若是想要和Java层通信时,也就是说需要使用JNIEnv对象,但是JNIEnv在不同的线程中不共享,这时候就需要使用到JavaVM的javaVm->AttachCurrentThread(&env,0)方法把native线程附加到JVM,使用完后使用javaVm->DetachCurrentThread();解除附加到JVM的native线程


二、JNI方法的静态注册和动态注册

1.静态注册

当我们开发ndk的时候,会在Java中声明native方法,在c++文件中实现声明的方法,在 Java中声明native方法很简单,加一个native关键字就行,如下所示:

public native void stringFromJNI(boolean b,
                                       byte b1,
                                       char c,
                                       short s,
                                       long l,
                                       float f,
                                       double d,
                                       String name,
                                       int age,
                                       int []i,
                                       String[] str,
                                       Person person,
                                       boolean[] bArray);

    public native Person getPerson();

声明完native方法后的下一步就是实现它,在c++文件中我们需要注册native方法,当Java层调用native的方法时,会执行native中实现的对应方法,这时候注册就分两种,一种是静态注册。

extern "C"
JNIEXPORT jobject JNICALL
Java_com_loveyoung_jnistudy_MainActivity_getPerson(JNIEnv *env, jobject instance) {...}

其实就是把“com.loveyoung.jnistudy.MainActivity”中的点换成了下划线再加上要调用的方法,这就是静态注册,这种注册方法实现简单,但是缺点显而易见,那就是native方法名要写很长,当Java的native方法所在的类发生改变时,那么修改会很麻烦,比如Java的A类中有100个native方法,再native层的c++文件中使用静态注册的方法注册了这些方法,假如我们的A类改名字了,改成了B类。那么c++文件中的所有方法名字都得改。

2.动态注册

和静态注册不同,动态注册更加的优雅,只是实现的过程稍微复杂了那么一点点。当我们在Java层定义完native方法后,在C++文件中按照如下的步骤注册方法:
(1)Java中定义native方法

public native void dynamicRegister(String name);

(2)在c++文件中写一个方法实现你要在native层完成的功能,例如:

extern "C" //支持c
JNIEXPORT void JNICALL //告诉虚拟机这时jni函数
native_dynamicRegister(JNIEnv *env,jobject instance,jstring name){
    const char *j_name = env->GetStringUTFChars(name, nullptr);
    LOGD("动态注册:%s",j_name)
    //释放
    env->ReleaseStringUTFChars(name,j_name);
}

(3)用一个静态数组保存你的Java native方法和C++文件中的方法等关联关系

static const JNINativeMethod jniNativeMethod[] = {
        {"dynamicRegister","(Ljava/lang/String;)V",(void *) (native_dynamicRegister)}
};

数组中多个方法等关联关系可以在“{}”之间用逗号分隔,其中每个元素各部分的含义为:

{"Java中的native方法名(dynamicRegister)"
 ,"Java中的native方法名的签名((Ljava/lang/String;)V)"
 ,(void *) (native_dynamicRegister)(C++文件中定义的方法名)}

(4)重写JNI_OnLoad函数,这个函数定义在jni.h头文件中,当我们在Java层使用System.loadLibrary时会调用它:

static const char *classPathName = "com/loveyoung/jnistudy/MainActivity";
JavaVM *javaVm;
jobject instance;
extern "C" //支持c
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *javaVm,void *pVoid){
    JNIEnv *jniEnv = nullptr;
    jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv),JNI_VERSION_1_6);
    LOGD("result->%d",result);
    if(result != JNI_OK){
        return JNI_ERR;
    }
    jclass mainActivityClass = jniEnv->FindClass(classPathName);
 //这里的jniNativeMethod就是第三步中咱们定义的Java和native层方法关联的数组
    jniEnv->RegisterNatives(mainActivityClass,jniNativeMethod,sizeof(jniNativeMethod)/sizeof(JNINativeMethod));//动态注册的数量
    return JNI_VERSION_1_6;
}

使用动态注册的方法,到时候如果Java层有变动,C++层只需修改classPathName的值为新类的值以及Java native方法和C++文件中的方法等关联关系数组就可以啦,如果我们增加多个动态注册的方法,可以如下设置:

static const JNINativeMethod jniNativeMethod[] = {
        {"dynamicRegister","(Ljava/lang/String;)V",(void *) (native_dynamicRegister)},
        {"dynamicTestException","(Ljava/lang/String;)V",(void *)(native_dynamicTestException)},
        {"test1","(Ljava/lang/String;)V",(void *)(native_test4)},
        {"nativeCount","()V",(void *) (native_count)},
        {"testThread","()V",(void *)(native_testThread)},
        {"releaseThread","()V",(void *) (native_releaseThread)}
};

需要注意的是,方法签名一定要对,否则无法调用到相应的函数


三.Java层和native层的类型对应关系以及相互传值

1.Java类型对应jni类型

我们都知道Java的类型分为引用类型和基本类型,对于基本类型,咱们在jni层都有相应的对应类型,例如,Java层的int,在jni层会有一个jint对应,同样,float会有个jfloat对应…,具体的大家可以去看下网上的其他博客,今天不多说。引用类型传递过来统一都是一个jobject。

2.Java和native相互传值

类似于音视频的一些功能,比如编解码、美颜算法,裁剪,瘦脸算法等都会放到native层实现。这时美颜算法我们用的最多的就是调节各种美颜参数,剪辑视频等功能,都是需要和用户交互,而Android开发中交互在Java层,所以就需要把用户调节的参数传递到native层,这时就需要将Java层的参数传递到native层,具体的演示如下所示:
(1)Java层的native方法定义:

public native void stringFromJNI(boolean b,
                                       byte b1,
                                       char c,
                                       short s,
                                       long l,
                                       float f,
                                       double d,
                                       String name,
                                       int age,
                                       int []i,
                                       String[] str,
                                       Person person,
                                       boolean[] bArray);
调用:
在Activity的onCreate()方法中调用
stringFromJNI(true,
                (byte)1,
                'A',
                (short) 3,
                4,
                3.3f,
                2.2d,
                "zhongxj",
                28,
                new int[]{1,2,3,4,5,6,7},
                new String[]{"I","love","you"},
                new Person("walt",27),
                new boolean[]{false,true}
        );

(2)native层的接收;

extern "C"
JNIEXPORT void JNICALL
Java_com_loveyoung_jnistudy_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject instance,
        jboolean jboolean1,
        jbyte jbyte1,
        jchar jchar1,
        jshort jshort1,
        jlong jlong1,
        jfloat jfloat1,
        jdouble jdouble1,
        jstring name,
        jint age,
        jintArray array,
        jobjectArray strArr,
        jobject person,
        jbooleanArray bArray) {

    //1.接收Java传递过来的boolean 值
    jboolean b_boolean = jboolean1;
    LOGD("boolean->%d",b_boolean);

    //2.接收Java传递过来的byte值
    jbyte c_byte = jbyte1;
    LOGD("jbyte->%d",c_byte);

   //3.接收Java传递过来的char值
   jchar j_char = jchar1;
   LOGD("j_char->%c",j_char);

   //4.接收Java传递过来的short值
   jshort j_short = jshort1;
   LOGD("j_short->%d",j_short)

   //5.接收Java传递过来的long值
   jlong j_long = jlong1;
   LOGD("j_long->%lld",j_long)

   //6. 接收Java传递过来的float值
    jfloat j_float = jfloat1;
    LOGD("j_float->%f",j_float)

    //7. 接收Java传递过来的double值
    jdouble j_double = jdouble1;
    LOGD("j_double->%f",j_double)

    //8.接收Java传递过来的String值
    const char *j_string = env->GetStringUTFChars(name,nullptr);
    LOGD("j_string->%s",j_string)

    //9.接收Java传递过来的int值

    jint j_int = age;
    LOGD("j_int->%d",j_int)

    //10.打印Java传递过来的int数组
    jint *intArray = env->GetIntArrayElements(array,nullptr);
            //拿到数组长度
            jsize intArraySize = env->GetArrayLength(array);
            for (int i=0;i<intArraySize;i++){
                LOGD("intArray:%d",intArray[i])
            }

            //用完记得释放数组
            env->ReleaseIntArrayElements(array,intArray,0);

   //11.打印Java传递过来的String数组

   jsize strAttrLength = env->GetArrayLength(strArr);
            for(int i = 0;i<strAttrLength;i++){
                jobject jobject1 = env->GetObjectArrayElement(strArr,i);
                //强制转成JNI string
                auto stringArrData = static_cast<jstring> (jobject1);

                //转 string
                const char *itemStr = env->GetStringUTFChars(stringArrData,nullptr);
                LOGD("String[%d]:%s",i,itemStr)

                //回收String[]
                env->ReleaseStringUTFChars(stringArrData,itemStr);
            }

            //12.打印Java传递过来的Object 对象
            //1获取字节码
            const char *person_class_str = "com/loveyoung/jnistudy/Person";
            //2 转jni class
            jclass person_class = env->FindClass(person_class_str);

            //3 拿到方法签名
            const char *sig = "()Ljava/lang/String;";

            jmethodID jmethodId = env->GetMethodID(person_class,"getName",sig);

            jobject obj_string = env->CallObjectMethod(person,jmethodId);
            jstring perStr = static_cast<jstring >(obj_string);
            const char *itemStr2 = env->GetStringUTFChars(perStr,NULL);
            LOGD("Person:%s",itemStr2);
            env->DeleteLocalRef(person_class);//回收
            env->DeleteLocalRef(person);//回收

            //13.打印Java传递过来的boolean 数组
            jsize booleanArratLength = env->GetArrayLength(bArray);
            jboolean *booleanArray = env->GetBooleanArrayElements(bArray,NULL);
            for(int i = 0;i<booleanArratLength;++i){
                bool b = booleanArray[i];
                jboolean b2 = booleanArray[i];
                LOGD("boolean:%d",b)
                LOGD("boolean:%d",b2)
            }

            env->ReleaseBooleanArrayElements(bArray,booleanArray,0);

}

注释中写得很清楚,就不多说了。照着敲一遍理解会更深刻哦~~~

四.NDK 使用进阶

1.三种引用的区别和使用

(1)局部引用
局部应用是通过NewLocalRef和DeleteLocalRef方法创建和释放的。局部引用只在创建它的线程中有效,通常不用删除局部引用,他们会在native方法中返回时全部自动释放,但是建议不再使用的时候手动释放掉局部引用,避免内存的过度使用

1.在 Java层定义测试局部引用的方法

public native void testLocalRef(String name);

2.使用动态注册将方法注册到native层

native_testLocalRef(JNIEnv *env, jobject instance, jstring name) {
    const char *nameStr = env->GetStringUTFChars(name, nullptr);
    LOGE("测试局部引用:%s", nameStr)
    if (personClass == NULL) {
        const char *person_class = "com/loveyoung/jnistudy/Person";
        jclass jclass1 = env->FindClass(person_class);
        personClass = static_cast<jclass>(env->NewLocalRef(jclass1));
        LOGE("personClass == NULL execute")
    }
    //java Person 构造方法实例化
    const char *sig = "()V";
    const char *method = "<init>";
    jmethodID init = env->GetMethodID(personClass, method, sig);//这个地方personClass无法使用,因为是局部引用
    //创建出来
    env->NewObject(personClass, init);
}

全局引用

代码中由于是局部引用,所以在后面无法被使用。要解决这个问题,需要使用全局引用,全局引用是通过NewGlobalRef和DeleteGlobalRef方法创建和释放一个全局引用,全局引用能在多个线程中被使用,且不会被GC回收,需要手动释放,而与之对应的是弱全局引用,它是通过newWeakGlobalRef和DeleteWeakGrobalRef,创建和释放一个弱全局引用,弱全局引用类似于全局引用,唯一的区别是他不会阻止被GC回收。

native_testLocalRef(JNIEnv *env, jobject instance, jstring name) {
    const char *nameStr = env->GetStringUTFChars(name, nullptr);
    LOGE("测试局部引用:%s", nameStr)
    if (personClass == NULL) {
        const char *person_class = "com/loveyoung/jnistudy/Person";
        //personClass = env->FindClass(person_class);//局部引用不能再后续的调用中重复使用,所以需要构建全局引用来解决这个问题
        //提升全局解决不能重复使用的问题
        jclass jclass1 = env->FindClass(person_class);
        personClass = static_cast<jclass>(env->NewGlobalRef(jclass1));
        LOGE("personClass == NULL execute")
    }

    //java Person 构造方法实例化
    const char *sig = "()V";
    const char *method = "<init>";
    jmethodID init = env->GetMethodID(personClass, method, sig);
    //创建出来
    env->NewObject(personClass, init);

    //显示删除全局引用
    env->DeleteGlobalRef(personClass);
    personClass = NULL;
}

2.在native创建线程,调用Java层的方法。

假如我们有这样一个场景,在native层创建一个线程,然后在创建的线程中调用Java层的方法更新界面UI,这时我们可以像下面这样实现:

Java层的方法声明

public native void testThread();
    public native void releaseThread();

定义一个方法用于更新UI

public void updateUI(){
        if(Looper.getMainLooper() == Looper.myLooper()){
            new AlertDialog.Builder(MainActivity.this)
                    .setTitle("更新UI")
                    .setMessage("Native 线程运行在主线程,可以直接更新UI")
                    .setPositiveButton("OK",null)
                    .show();
        }else{
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    new AlertDialog.Builder(MainActivity.this)
                            .setTitle("更新UI")
                            .setMessage("Native 线程运行在子线程切换为主线程更新UI")
                            .setPositiveButton("OK",null)
                            .show();
                }
            });
        }
    }

在C++ 文件中,声明一个函数自定义一个线程

void *customThread(void *pVoid) {
    //调用的话一定需要jniENV *env
    //JNIEnv *env 无法跨线程,只有JavaVm才可以跨进程

    JNIEnv *env = NULL;
    int result = javaVm->AttachCurrentThread(&env, 0);//把native线程附加到JVM
    if (result != 0) {
        return 0;
    }

    jclass mainActivityClass = env->GetObjectClass(instance);
    //拿到MainActivity的UpdateUI
    const char *sig = "()V";
    jmethodID updateUI = env->GetMethodID(mainActivityClass, "updateUI", sig);
    env->CallVoidMethod(instance, updateUI);

    //解除附加到JVM的native线程
    javaVm->DetachCurrentThread();
    return 0;
}

分别调用创建线程和释放线程的方法

extern "C"
JNIEXPORT void JNICALL
native_testThread(JNIEnv *env, jobject thiz) {
    instance = env->NewGlobalRef(thiz);//声明成全局的,就不会被释放,所以可以在线程里面用
    pthread_t pthread;
    pthread_create(&pthread, 0, customThread, instance);
    pthread_join(pthread, 0);

}
extern "C"
JNIEXPORT void JNICALL
native_releaseThread(JNIEnv *env, jobject thiz) {
    if (NULL != instance) {
        env->DeleteGlobalRef(instance);
        instance = NULL;
    }

}

总结

本节中主要介绍了如下知识:
(1)jni方法和Java层方法相互传值和类型
(2)jni方法的动态注册和静态注册的区别和使用
(3)三种引用的区别和使用
(4)最后是创建线程然后调用Java层方法更新UI的方法