前言
随着现在5g时代的到来,短视频技术越发的流行了,大家都在争着做短视频的app。当初看到抖音的时候我震惊了,和我以前用支付宝和淘宝一样震惊,用淘宝和支付宝结合,我能在家买到我想要的任何东西,我可以用支付宝充校园卡,交水电费等等,非常的方便。抖音出来的时候我看到的是各种漂亮的小姐姐,有趣的电影短视频剪辑,美食菜谱,生活技巧等应有尽有…,然而最吸引我的还是抖音电商。让大家能在娱乐的时候购物,这是一个双重体验。逛淘宝京东都是目的性的,我想起了啥才去逛,比如我想买衣服了,想买手机了我才想起他们,但是用抖音是在娱乐的过程中发现我还缺啥。真的很方便,在未来的十年,短视频电商一定会渐渐的抢占淘宝和京东这类的电商的份额。并且在其中占大头。说了这么多其实就是想赶紧抓住这个机会,学习音视频的开发。但是说到音视频的开发就不得不涉及到Android NDK的开发,今天的文章主要是为了介绍Android NDK的开发入门知识,虽说是入门,但是不是零基础入门那种,如果连ndk开发的基本流程都不知道的话请移步其他博客。本篇文章建议坐在电脑旁跟着敲一遍,印象会更深刻。
1.JNIEnv
JNIEnv 可以理解成一个上下文,里面封装了jni的方法指针,JNIenv只在创建它的线程里有效,不能跨线程传递,不同线程之间的JNIEnv相互独立,互不影响
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);
}
2.全局引用
上面代码中由于是局部引用,所以在后面无法被使用。要解决这个问题,需要使用全局引用,全局引用是通过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,这时我们可以像下面这样实现:
1.Java层的方法声明
public native void testThread();
public native void releaseThread();
2.定义一个方法更新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();
}
});
}
}
3.在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;
}
4.分别调用创建线程和释放线程的方法
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的方法
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓