例子源码
测试例子源地址: https://github.com/roman10/android-ffmpeg-tutorial
JNI接口编程简要说明
JNI作为一种编程接口,是解决Java语言与C/C++语言之间的通信问题。
我们知道,Java代码编译的结果是字节码,这种码只能在Java虚拟机上运行,而C/C++编译最后的结果是机器码,能够直接在cpu上运行。要想解决字节码与机器码通信的问题,Java设计出JNI接口来让虚拟机来支持。要理解这个接口,我们得先从函数签名说起。
函数签名
函数签名是我自己随性说的,觉得这么理解会好些。可以理解为一个函数签名就代码一个函数。
记得以前研究C/C++动态库时,碰到的一个难点就是C的动态库与C++的动态库是不一样的。理由就是C++支持重载,而C语言不支持。要想生成C语言的动态库,需要加上 extern "C". 玩过C动态库的,对extern "C"肯定不陌生(都是泪呀),生成动态库时需要用它,而使用动态库时也得在声明中加上:extern "C",表示这是C的函数。
想想python 之弹的第一句话“Beautiful is better than ugly.” 而 extern "C"这么难看的代码出自我手,还真有点难为情。
扯远了, 这种纠结主要还是C和C++毕竟不是同一种语言。而每种语言编译时代码函数的函数签名是不一样的引起的。
int add(int a, int b) {
return a + b;
}
以上面的add为例,C语言表示为 add, 而C++表示为 add_int_int. C++这么表示就是为了支持重载,比如再有一个函数为 int add(int a){return a;} 时,签名就是 add_int, 好作区分。
而编译器是通过函数签名来查找指定函数的。 如果是C++编译器,而调用C的函数,就得告诉编译器这是个C函数,要用查找C函数的方法来找查这个C函数,所以需要加上 extern "C" 语句。
同理,Java为了使用C/C++的函数,也需要一个类似 extern "C"的标识来告诉Java编译器,这个是C/C++代码的函数,这个标识叫做 native.
native的作用就是告诉Java编译器,要在当前加载的库中查找与此函数定义相匹配的函数。这样就解决了JNI接品中函数查找问题。
JNI类型签名
native定义了函数查找的位置,而类型查找就得看类型签名了。
数据类型:
Java 类型 | 类型签名 |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | L |
float | F |
double | D |
类 | L全限定名;,比如String, 其签名为Ljava/lang/util/String; |
数组 | [类型签名, 比如 [B |
函数类型
格式:
(类型签名1类型签名2...)返回值类型签名
如String toString() 函数:"()Ljava/lang/String;"
函数定义规范
JNI为了能够让C代码能够访问Java代码,规定了JNI函数定义时,要必须传两个参数,位于函数参数的第1,第2个参数。以例子中的代码为例:
C/C++声明:
jint naMain(JNIEnv *pEnv, jobject pObj, jobject pMainAct, jstring pFileName, jint pNumOfFrames)
Java 声明:
private static native int naMain(MainActivity pObject, String pVideoFileName, int pNumOfFrames);
JNIEnv 表示整个JNI环境,通过他可以操作整修JAVA环境。
jobject pObj: 表示调用这个函数的类环境,如果以静态函数方式调用,则这个表示是声明此方法的类,如果作为普通函数,就表示为调用的类对象。因为调用方法由Java代码来确定,是不是静态方式调用,不确定。 所以感觉此参数运用得不多。 例子中是以静态方式调用的,所以传进来的就是MainActivity类。
其实的参数根据类形对应方式就一一对应了。
类型对应方式
基本类型对应方式:
Java类型 | 本地类型 | 说明 |
boolean | jboolean | 无符号,8位 |
byte | jbyte | 无符号,8位 |
char | jchar | 无符号,16位 |
short | jshort | 有符号,16位 |
int | jint | 有符号,32位 |
long | jlong | 有符号,64位 |
float | jfloat | 32位 |
double | jdouble |
|
void | void | N/A |
类类型对应方式:
<pre name="code" class="java">String 对应 <span style="font-family: Arial, Helvetica, sans-serif;">jstring </span>
<span style="font-family: Arial, Helvetica, sans-serif;">其它 对应 jobject</span>
函数定义三要素
JNI中通过下面的三要素来定义一个接口
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
例子中对应的代码有:
JNINativeMethod nm[1];
nm[0].name = "naMain";
nm[0].signature = "(Lroman10/tutorial/android_ffmpeg_tutorial01/MainActivity;Ljava/lang/String;I)I";
nm[0].fnPtr = (void*)naMain;
jclass cls = (*env)->FindClass(env, "roman10/tutorial/android_ffmpeg_tutorial01/MainActivity");
//Register methods with env->RegisterNatives.
(*env)->RegisterNatives(env, cls, nm, 1);
规则如下:
一: 前缀: Java_
二:类的全限定名, 用下划线进行分隔(_):
roman10_tutorial_android_ffmpeg_tutorial01_MainActivity (原来名字有下划线时怎么处理?)
三:再加下划线分隔符
四: 方法名:naMain
五:对于重载的本地方法,加上两个下划线(“__”), 后跟mangled参数签名。
我试了把naMain按这个规划测试,没测试通,不知道是什么原因!
反正给我感觉这方法比较麻烦。
回调JAVA函数
JNI传入
JNIEnv的目的就是能够用C的代码调用Java环境。例子中有几个地方调用了Java的函数。 注意都是用了签名。
1 查找Java的类,并向Java类中注册方法
1 查找Java的类,并向Java类中注册方法
jclass cls = (*env)->FindClass(env, "roman10/tutorial/android_ffmpeg_tutorial01/MainActivity");
//Register methods with env->RegisterNatives.
(*env)->RegisterNatives(env, cls, nm, 1);
2. 创建Bitmap类对象。
jclass javaBitmapClass = (jclass)(*pEnv)->FindClass(pEnv, "android/graphics/Bitmap");
jmethodID mid = (*pEnv)->GetStaticMethodID(pEnv, javaBitmapClass, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
。。。
jclass bitmapConfigClass = (*pEnv)->FindClass(pEnv, "android/graphics/Bitmap$Config");
jobject javaBitmapConfig = (*pEnv)->CallStaticObjectMethod(pEnv, bitmapConfigClass,
(*pEnv)->GetStaticMethodID(pEnv, bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;"), jConfigName);
//create the bitmap
return (*pEnv)->CallStaticObjectMethod(pEnv, javaBitmapClass, mid, pWidth, pHeight, javaBitmapConfig);
3. 回调类的方法
void SaveFrame(JNIEnv *pEnv, jobject pObj, jobject pBitmap, int width, int height, int iFrame) {
char szFilename[200];
jmethodID sSaveFrameMID;
jclass mainActCls;
sprintf(szFilename, "/sdcard/android-ffmpeg-tutorial01/frame%d.jpg", iFrame);
mainActCls = (*pEnv)->GetObjectClass(pEnv, pObj);
sSaveFrameMID = (*pEnv)->GetMethodID(pEnv, mainActCls, "saveFrameToPath", "(Landroid/graphics/Bitmap;Ljava/lang/String;)V");
LOGI("call java method to save frame %d", iFrame);
jstring filePath = (*pEnv)->NewStringUTF(pEnv, szFilename);
(*pEnv)->CallVoidMethod(pEnv, pObj, sSaveFrameMID, pBitmap, filePath);
LOGI("call java method to save frame %d done", iFrame);
}