音视频开发的运用,是要学习JNI编程的。怎么样去学JNI?本篇我们就来学习JNI编程这门技术。
对于Java来说,因为是需要JVM来运行,所以性能上肯定没有C/C++这种语言高,所以Android就弥补了这个缺陷,使用了JNI特征,使用JNI可以让Java类的某些方法用原生实现,让调用原生(C/C++)方法能够像普通Java方法一样。
什么是JNI?
NJava Native Jntecface它是Java平台的一个特性(进不是Andreid系统特有的)e其主要是定义了一些N函数。让开发者可以通过用这些函数实现Java代码调用C/C++的代码,C/C++的代码也 可以调用Java的代码,这样就可以发挥各个语言的特点了。
JNI入门学习
1.数据类型和类型描述符
Java 中有两种数据类型:
- 基本数据类型: boolean 、char、byte、int、short、long、float、double。
- 引用数据类型: String、Object[]、Class、Object 及其它类。
1.1 基本数据类型
基本数据类型可以直接与 C/C++ 的相应基本数据类型映射,如下表所示。JNI 用类型定义使得这种映射对开发人员透明。

1.2 引用类型:
与基本数据类型不同,引用类型对原生方法时不透明的,引用类型映射如下表所示。它们的内部数据结构并不直接向原生代码公开。

1.3 数据类型描述符
在 JVM 虚拟机中,存储数据类型的名称时,是使用指定的描述符来存储,而不是我们习惯的 int,float 等。

示例:
表示一个 String
Java 类型 : java.lang.StringJNI 描述符: Ljava/lang/String; (L + 类全名 + ;)
表示一个数组
Java 类型: String[] JNI 描述符: [Ljava/lang/String; Java 类型: int [] [] JNI 描述符: [[I表示一个方法
Java 方法: long func(int n, String s, int[] arr); JNI 描述符: (ILjava/lang/String;[I)JJava 方法: void func(); JNI 描述符: ()V
也可以使用命令 : javap -s 全路径 来获取方法签名

2.JNIEnv 和 JavaVm 介绍
2.1 JNIEnv :
JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法的指针。
JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv 彼此独立。
native 环境中创建的线程,如果需要访问 JNI,必须要调用 AttachCurrentThread 关联,并使用 DetachCurrentThread 解除链接。
2.2 JavaVm :
JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。
2.3 代码风格 (C/C++)
C: (*env)->NewStringUTF(env, “Hellow World!”);
C++: env->NewStringUTF(“Hellow World!”);
3.JNI API
4.对数据类型的操作
JNI 处理 Java 传递过来的数据
定义 native 函数
public class MainActivity extends AppCompatActivity {
/**
1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/** 1. Java 数据传递给 native */
test1(true,
(byte) 1,
‘,’,
(short) 3,
4,
3.3f,
2.2d,
“DevYK”,
28,
new int[]{1, 2, 3, 4, 5, 6, 7},
new String[]{“1”, “2”, “4”},
new Person(“阳坤”),
new boolean[]{false, true}
);
}
/**
Java 将数据传递到 native 中
*/
public native void test1(
boolean b,
byte b1,
char c,
short s,
long l,
float f,
double d,
String name,
int age,
int[] i,
String[] strs,
Person person,
boolean[] bArray
);
}
2.jni 处理 Java 传递过来的数据
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
extern “C”//支持 C 语言代码
JNIEXPORT void JNICALL
Java_com_devyk_ndk_1sample_MainActivity_test1(JNIEnv *env, jobject instance,
jboolean jboolean1,
jbyte jbyte1,
jchar jchar1,
jshort jshort1,
jlong jlong1,
jfloat jfloat1,
jdouble jdouble1,
jstring name_,
jint age,
jintArray i_,
jobjectArray strs,
jobject person,
jbooleanArray bArray_
) {
//1. 接收 Java 传递过来的 boolean 值
unsigned char b_boolean = jboolean1;
LOGD(“boolean-> %d”, b_boolean);
//2. 接收 Java 传递过来的 boolean 值
char c_byte = jbyte1;
LOGD(“jbyte-> %d”, c_byte);
//3. 接收 Java 传递过来的 char 值
unsigned short c_char = jchar1;
LOGD(“char-> %d”, c_char);
//4. 接收 Java 传递过来的 short 值
short s_short = jshort1;
LOGD(“short-> %d”, s_short);
//5. 接收 Java 传递过来的 long 值
long l_long = jlong1;
LOGD(“long-> %d”, l_long);
//6. 接收 Java 传递过来的 float 值
float f_float = jfloat1;
LOGD(“float-> %f”, f_float);
//7. 接收 Java 传递过来的 double 值
double d_double = jdouble1;
LOGD(“double-> %f”, d_double);
//8. 接收 Java 传递过来的 String 值
const char *name_string = env->GetStringUTFChars(name_, 0);
LOGD(“string-> %s”, name_string);
//9. 接收 Java 传递过来的 int 值
int age_java = age;
LOGD(“int:%d”, age_java);
//10. 打印 Java 传递过来的 int []
jint *intArray = env->GetIntArrayElements(i_, NULL);
//拿到数组长度
jsize intArraySize = env->GetArrayLength(i_);
for (int i = 0; i < intArraySize; ++i) {
LOGD(“intArray->%d:”, intArray[i]);
}
//释放数组
env->ReleaseIntArrayElements(i_, intArray, 0);
//11. 打印 Java 传递过来的 String[]
jsize stringArrayLength = env->GetArrayLength(strs);
for (int i = 0; i < stringArrayLength; ++i) {
jobject jobject1 = env->GetObjectArrayElement(strs, i);
//强转 JNI String
jstring stringArrayData = static_cast(jobject1);
//转 C String
const char *itemStr = env->GetStringUTFChars(stringArrayData, NULL);
LOGD(“String[%d]: %s”, i, itemStr);
//回收 String[]
env->ReleaseStringUTFChars(stringArrayData, itemStr);
}
//12. 打印 Java 传递过来的 Object 对象
//12.1 获取字节码
const char *person_class_str = “com/devyk/ndk_sample/Person”;
//12.2 转 jni jclass
jclass person_class = env->FindClass(person_class_str);
//12.3 拿到方法签名 javap -a
const char *sig = “()Ljava/lang/String;”;
jmethodID jmethodID1 = env->GetMethodID(person_class, “getName”, sig);
jobject obj_string = env->CallObjectMethod(person, jmethodID1);
jstring perStr = static_cast(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD(“Person: %s”, itemStr2);
env->DeleteLocalRef(person_class); // 回收
env->DeleteLocalRef(person); // 回收
//13. 打印 Java 传递过来的 booleanArray
jsize booArrayLength = env->GetArrayLength(bArray_);
jboolean *bArray = env->GetBooleanArrayElements(bArray_, NULL);
for (int i = 0; i < booArrayLength; ++i) {
bool b = bArray[i];
jboolean b2 = bArray[i];
LOGD(“boolean:%d”,b)
LOGD(“jboolean:%d”,b2)
}
//回收
env->ReleaseBooleanArrayElements(bArray_, bArray, 0);
}
输出:
输出:
native-lib: boolean-> 1
native-lib: jbyte-> 1
native-lib: char-> 44
native-lib: short-> 3
native-lib: long-> 4
native-lib: float-> 3.300000
native-lib: double-> 2.200000
native-lib: string-> DevYK
native-lib: int:28
native-lib: intArray->1:
native-lib: intArray->2:
native-lib: intArray->3:
native-lib: intArray->4:
native-lib: intArray->5:
native-lib: intArray->6:
native-lib: intArray->7:
native-lib: String[0]: 1
native-lib: String[1]: 2
native-lib: String[2]: 4
native-lib: Person: 阳坤
native-lib: boolean:0
native-lib: jboolean:0
native-lib: boolean:1
native-lib: jboolean:1
5.JNI 处理 Java 对象
定义一个 Java 对象
public class Person {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public String toString() {
return “Person{” +
“name=’” + name + ‘’’ +
“, age=” + age +
‘}’;
}
}
定义 native 接口
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/*处理 Java 对象/
String str = getPerson().toString();
text.setText(str);
}
public native Person getPerson();
}
根据上面代码我们知道,如果获取成功,手机屏幕上肯定会打印会显示数据。
JNI 的处理
extern “C”
JNIEXPORT jobject JNICALL
Java_com_devyk_ndk_1sample_MainActivity_getPerson(JNIEnv *env, jobject instance) {
//1. 拿到 Java 类的全路径
const char *person_java = “com/devyk/ndk_sample/Person”;
const char *method = “”; // Java构造方法的标识
//2. 找到需要处理的 Java 对象 class
jclass j_person_class = env->FindClass(person_java);
//3. 拿到空参构造方法
jmethodID person_constructor = env->GetMethodID(j_person_class, method, “()V”);
//4. 创建对象
jobject person_obj = env->NewObject(j_person_class, person_constructor);
//5. 拿到 setName 方法的签名,并拿到对应的 setName 方法
const char *nameSig = “(Ljava/lang/String;)V”;
jmethodID nameMethodId = env->GetMethodID(j_person_class, “setName”, nameSig);
//6. 拿到 setAge 方法的签名,并拿到 setAge 方法
const char *ageSig = “(I)V”;
jmethodID ageMethodId = env->GetMethodID(j_person_class, “setAge”, ageSig);
//7. 正在调用 Java 对象函数
const char *name = “DevYK”;
jstring newStringName = env->NewStringUTF(name);
env->CallVoidMethod(person_obj, nameMethodId, newStringName);
env->CallVoidMethod(person_obj, ageMethodId, 28);
const char *sig = “()Ljava/lang/String;”;
jmethodID jtoString = env->GetMethodID(j_person_class, “toString”, sig);
jobject obj_string = env->CallObjectMethod(person_obj, jtoString);
jstring perStr = static_cast(obj_string);
const char *itemStr2 = env->GetStringUTFChars(perStr, NULL);
LOGD(“Person: %s”, itemStr2);
return person_obj;
}
输出:

可以看到 native 返回数据给 Java 了。
6.JNI 动态注册
前面咱们学习的都是静态注册,静态注册虽然简单方便,但是也面临一个较大的问题,如果当前类定义的 native 方法名称改变或者包名改变,那么这一改也就面临在 cpp 中实现的也将改动,如果将要面临这种情况你可以试试 JNI 动态注册,如下代码所示:
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView text = findViewById(R.id.sample_text);
/**动态注册的 native */
dynamicRegister(“我是动态注册的”);
}
/**
动态注册
*/
public native void dynamicRegister(String name);
}
复制代码
cpp:
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
/**
TODO 动态注册
*/
/**
对应java类的全路径名,.用/代替
*/
const char *classPathName = “com/devyk/ndk_sample/MainActivity”;
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
//释放
env->ReleaseStringUTFChars(name, j_name);
}
/* 源码结构体
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
static const JNINativeMethod jniNativeMethod[] = {
{“dynamicRegister”, “(Ljava/lang/String;)V”, (void *) (native_dynamicRegister)}
};
/**
该函数定义在jni.h头文件中,System.loadLibrary()时会调用JNI_OnLoad()函数
*/
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
//通过虚拟机 创建爱你全新的 evn
JNIEnv *jniEnv = nullptr;
jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
if (result != JNI_OK) {
return JNI_ERR; // 主动报错
}
jclass mainActivityClass = jniEnv->FindClass(classPathName);
jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod,
sizeof(jniNativeMethod) / sizeof(JNINativeMethod));//动态注册的数量
return JNI_VERSION_1_6;
}
输出:
动态注册: 我是动态注册的
7.异常处理
异常处理是 Java 程序设计语言的重要功能, JNI 中的异常行为与 Java 中的有所不同,在 Java 中,当抛出一个异常时,虚拟机停止执行代码块并进入调用栈反向检查能处理特定类型异常的异常处理程序代码块,这也叫捕获异常。虚拟机清除异常并将控制权交给异常处理程序。相比之下, JNI 要求开发人员在异常发生后显式地实现异常处理流。
捕获异常:
JNIEvn 接口提供了一组与异常相关的函数集,在运行过程中可以使用 Java 类查看这些函数,比如代码如下:
public native void dynamicRegister2(String name);
/**
测试抛出异常
@throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException(“MainActivity testException NullPointerException”);
}
当调用 testException 方法时,dynamicRegister2 该原生方法需要显式的处理异常信息,JNI 提供了 ExceptionOccurred 函数查询虚拟机中是否有挂起的异常。在使用完之后,异常处理程序需要用 ExceptionClear 函数显式的清除异常,如下代码:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
}
抛出异常:
JNI 也允许原生代码抛出异常。因为异常是 Java 类,应该先用 FindClass 函数找到异常类,用 ThrowNew 函数可以使用化且抛出新的异常,如下代码所示:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
因为原生函数的代码执行不受虚拟机的控制,因此抛出异常并不会停止原生函数的执行并把控制权交给异常处理程序。到抛出异常时,原生函数应该释放所有已分配的原生资源,例如内存及合适的返回值等。通过 JNIEvn 接口获得的引用是局部引用且一旦返回原生函数,它们自动地被虚拟机释放。
示例代码:
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
1. 加载 native 库
*/
static {
System.loadLibrary(“native-lib”);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dynamicRegister2(“测试异常处理”);
}
public native void dynamicRegister2(String name);
/**
测试抛出异常
@throws NullPointerException
*/
private void testException() throws NullPointerException {
throw new NullPointerException(“MainActivity testException NullPointerException”);
}
}
native-lib.cpp 文件
#include <jni.h>
#include
#include <android/log.h>
#include
#define TAG “native-lib”
// VA_ARGS 代表 …的可变参数
#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, VA_ARGS);
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, VA_ARGS);
#define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, VA_ARGS);
/**
TODO 动态注册
*/
…
…
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
jclass clazz = env->GetObjectClass(instance);//拿到当前类的class
jmethodID mid =env->GetMethodID(clazz, “testException”, “()V”);//执行 Java 测试抛出异常的代码
env->CallVoidMethod(instance, mid); // 执行会抛出一个异常
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
//释放
extern “C” //支持 C 语言
JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数
native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) {
const char *j_name = env->GetStringUTFChars(name, NULL);
LOGD(“动态注册: %s”, j_name)
jclass clazz = env->GetObjectClass(instance);//拿到当前类的class
jmethodID mid =env->GetMethodID(clazz, “testException”, “()V”);//执行 Java 测试抛出异常的代码
env->CallVoidMethod(instance, mid); // 执行会抛出一个异常
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {//如果发生异常
env->ExceptionDescribe(); // 打印异常信息
env->ExceptionClear(); // 清除掉发生的异常
jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”);
env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java
}
//释放
JNI使用开发流程的步骤

步骤:
- 在Java中先声明一个native方法;
- 编译Java源文件javac得到.class文件;
- 通过javah -jni命令导出JNI的.h头文件;
- 使用Java需要交互的本地代码,实现在Java中声明的Native方法;
- 将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib);
-
通过Java命令执行Java程序,最终实现Java调用本地代码;
小结:
本文的JNI编程学习就学到这里,主要讲解了数据类型、类型描述符、JNI API数据类型的操作、如何处理Java对象、JNI动态注册、异常处理、JNI使用开发步骤以及对JNIEnv和JavaVm的介绍。学习JNI还需要更深入的学习,需要学习资料可私信博主获取。
音视频难吗?难,不骗大家,音视频的门槛是真的有点高,因为音视频特色就是C/C++开发,这让很多不少Android只接触了Java开发的朋友望而却步——这也是为什么会有这么多NDK高薪岗位存在的理由。
对于新入门的开发者来说,仅仅只是看大量的音视频开源库,像FFmpeg、MediaCodec这些API就已经很让人头大了。况且,只是使用API,根本不能适应实际工作中千变万化的需求。
介于以上的主要原因,音视频自学起来困难重重,学习成本非常高,效率极低。当然,所谓的难度只是对那些找不到方向的人来说,如果有正确的学习方向,辅以系统的学习资料,难度就会降低很多,学起来也高效很多。
网友评论