1. 程式人生 > >android 移植 ffmpeg (二) 原始碼分析 JNI程式設計說明

android 移植 ffmpeg (二) 原始碼分析 JNI程式設計說明

例子原始碼

測試例子源地址: 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;
可通過  RegisterNatives註冊介面, 來讓Java程式碼呼叫此函式。
jclass cls = (*env)->FindClass(env, "roman10/tutorial/android_ffmpeg_tutorial01/MainActivity");
	//Register methods with env->RegisterNatives.
	(*env)->RegisterNatives(env, cls, nm, 1);
當然JNI中還有另一種不用RegisterNatives方法, 就是特殊的函式定義,本人感覺這種方式太麻煩! 規則如下: 一: 字首: Java_ 二:類的全限定名, 用下劃線進行分隔(_): roman10_tutorial_android_ffmpeg_tutorial01_MainActivity  (原來名字有下劃線時怎麼處理?) 三:再加下劃線分隔符 四: 方法名:naMain 五:對於過載的本地方法,加上兩個下劃線(“__”), 後跟mangled引數簽名。 我試了把naMain按這個規劃測試,沒測試通,不知道是什麼原因! 反正給我感覺這方法比較麻煩。

回撥JAVA函式

 JNI傳入 JNIEnv的目的就是能夠用C的程式碼呼叫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);
}