Preface
在用安卓處理音視頻開發時,往往需要我們調用已有的成熟的用C/C++語言編寫的庫,比如FFmpeg,LAME等.這就牽涉到如何用Java調用C語言編寫的庫.
本文處理的是最簡單的情形,簡單調用一個C文件里的函數.
想寫這篇文章很久了,但是之前遇到了一個坑,今天才萬幸走出來.所以趕緊記錄一下.
1 Java調用C代碼的整體流程
Java調用C代碼是通過JNI(JavaNativeInterface)這個手段來實現的,具體流程見下圖:
接下來我們用一個實例來說明如何實現Java調用C代碼
2 準備工作
- 確認開發環境
小編這里使用的是
MacOS 11.0.1
Android Stuidio 4.1.2
Javac 的版本是14.0.1,這個很重要,影響到了第2步生成jni文件.之前參考帖子上用的都是javah命令,但是在小編這里就是不成功,可能就是版本問題.
- 明確兩個重要工具的目錄
ndk-build程序目錄:
/Users/gikkiares/Library/Android/sdk/ndk/20.0.5594570/ndk-build
javac目錄
/usr/bin/javac
如果在使用命令時,提示command not found:
就說明要么工具沒有安裝,要么安裝了,但是沒有加入到環境變量.
如果是前者,重新安裝對應工具.
如果是后者,將工具的目錄加入到環境變量,或者使用命令的全路徑.
- 創建一個新的Android項目
我們在該項目中,實現用java調用c代碼.
3 Java調用C代碼示例
3.1 編寫java橋接類
為了簡單起見,我們新建一個CManager.java
CManager類負責兩個事情:
1,對java層,提供了java形式的接口
2,實現的方式為c語言.
該文件內容為:
package com.tinywind.gajdemo.module.cmanager;
public class CManager {
public static CManager sharedInstance = new CManager();
public native String getMessageFromC();
public native int sum(int a, int b);
}
我們注意到以下幾點:
- 我們使用了單例模式.
- 定義了兩個用native關鍵字修飾的方法.
1.使用native關鍵字,表示我們實現該方法的語言不是java而是c/c++.
2,getMessageFromC,簡單地用c語言返回一個字符串
3,sum函數,用c語言實現兩個數相加.
- 我們只聲明了函數,并沒有實現.
3.2 生成jni風格的頭文件.
小編之前一直卡在這一步.一直在嘗試用javah命令生成,單總是提示找不到類.
估計可能和小編用的javac版本是14.0.1有關.
總之,錯誤的方式成千上萬,正確的方式只有那么一種,我們記住正確的就好了,錯誤的就讓他隨風而去吧~
//切換到CManager.java所在的目錄
cd ${ProjectPath}/app/src/main/java/com/tinywind/gajdemo/module/cmanager
//-h .指定了在當前目錄中輸出jni風格的頭文件
javac CManager.java -h .
這一步完成之后,我們得到了一個.class文件和.h文件
.class文件對我們來說沒用,可以直接刪除.
這個很長的h文件,就是我們的jhi頭文件.
3.3 編寫C文件
新建目錄
src/main/jni
需要將jni的頭文件移動到這個目錄中.
然后新建一個文件CManager.c
,內容為:
#include "com_tinywind_gajdemo_module_cmanager_CManager.h"
//C字符串轉java字符串
jstring cstringToJstring(JNIEnv* env, const char* pStr) {
int strLen = strlen(pStr);
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
// 獲取java String類方法String(byte[],String)的構造器,用于將本地byte[]數組轉換為一個新String
jmethodID methodId = (*env)->GetMethodID(env, cls_string, "<init>", "([BLjava/lang/String;)V");
jbyteArray byteArray = (*env)->NewByteArray(env, strLen);
jstring encode = (*env)->NewStringUTF(env, "utf-8");
(*env)->SetByteArrayRegion(env, byteArray, 0, strLen, (jbyte*)pStr);
return (jstring)(*env)->NewObject(env, cls_string, methodId, byteArray, encode);
}
JNIEXPORT jstring JNICALL Java_com_tinywind_gajdemo_module_cmanager_CManager_getMessageFromC
(JNIEnv * env, jobject obj) {
char * str = "Hello,this is a message from c!";
return cstringToJstring(env, str);
}
JNIEXPORT jint JNICALL Java_com_tinywind_gajdemo_module_cmanager_CManager_sum
(JNIEnv * env, jobject obj, jint a, jint b) {
return a + b;
}
需要注意以下幾點:
- c文件中要引入jni頭文件
#include "com_tinywind_gajdemo_module_cmanager_CManager.h"
- 函數的原型從jni頭文件中拷貝,但是要注意jni頭文件中形參的名稱是省略的,我們需要加上去.
- c語言的字符串和jni的jstring是不同類型的需要轉換.
3.4 生成動態庫
在jni目錄中新建Android.mk文件,目錄看起來是這個樣子:
在Android.mk文件中輸入以下內容:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libcmanager
LOCAL_SRC_FILES := ./CManager.c
include $(BUILD_SHARED_LIBRARY)
- LOCAL_MODULE指定了so庫的名字
要注意的是,寫得是libcmanager,但是后期導入時只需要寫cmanager.
- LOCAL_SRC_FILES指定了so庫的源文件
其他的項目暫時不太清楚
然后執行以下命令:
//然后切換到目錄:
${ProjectPath}/app/src/main
//編譯c文件為so動態鏈接庫
${NdkDir}/ndk-build
編譯成功之后,項目里在src/main目錄下會多出libs目錄,里面對應不同的架構,產生了對應的so庫.
3.5 加載動態鏈接庫
加載動態鏈接庫最簡單的方法,就是在main目錄下創建jniLibs文件夾,然后將libs中目全部加入進去就額可以了.
系統會自動加載jniLibs里的so動態庫.
3.6 調用Native方法
最后,我們調用Native方法,只要把橋接的Java類CManager當做普通的java類去調用就可以了:
String string = CManager.sharedInstance.getMessageFromC();
int sub = CManager.sharedInstance.sum(1,1);
我們斷點查看執行結果
我們可以看到,c的世界向java的世界發來了賀電"Hello,this is a message from c",然后也友好地教導了我們1+1=2.
這就是java調用c代碼的最簡單模型.
4 總結
- 關于Markdown的一個問題
希望markdown有一個沒有級別的標題,現在是要么1級標題,2級標題.
我希望能有一個沒有級別的標題.
- 關于模板
模板,就是套路.不管是在編程界還是社交界,都是需要各種模板.
模板夠多,才能玩的六.
所以本文牽涉的內容很簡單,但是也是一個很重要的模板