例子源码
测试例子源地址: 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); }