JNI
作為 Java/Kotlin
(原生端) 同 C/C++
端交互的工具猾蒂,是學(xué)習(xí) ffmpeg
的一個前提奠货,這邊做一個學(xué)習(xí)過程中的記錄。通過 Android Studio
可以快速創(chuàng)建一個 JNI
項(xiàng)目(創(chuàng)建時候選擇 Native C++
即可夏伊,會自動配置 CMakeList
等文件)刑峡,該文基于 AS 3.5
loadLiabry
src
文件夾下相比一般的 AS
項(xiàng)目多了 cpp
文件夾,該文件夾下有一個 .cpp
文件和 CMakeLists.txt
文件阐污,.cpp
文件用來寫 native
端實(shí)現(xiàn)的方法休涤,CMakeLists
用來做些 cpp
的配置,目前可以忽略
main
│ AndroidManifest.xml
├─cpp
│ native-lib.cpp
│ CMakeLists.txt
├─java
│
├─res
接著在 MainActivity
中有這么一行代碼
companion object {
init {
System.loadLibrary("native-lib")
}
}
通過 loadLibrary
方法笛辟,加載編譯的鏈接 so
庫功氨,so
庫的源碼就是前面提到的 native-lib.cpp
文件了
原生調(diào)用 cpp 方法
那么在 Kotlin
中如何調(diào)用 cpp
的方法呢,可以看到 MainActivity
中有一個使用 external
修飾的方法(如果是 java
則使用 native
關(guān)鍵詞修飾)
external fun stringFromJNI(): String
通過該方法手幢,會去調(diào)用 cpp
層的 native
方法捷凄,可以看下 native-lib.cpp
文件,內(nèi)部定義了一個方法
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_stringFromJNI(JNIEnv *env, jobject/*this*/) {
std:string hello = "Hello from c++";
return env->NewStringUTF(hello.c_str());
}
可以看到該方法的命名方式為 Java_包名_類名_方法名
(包名的 .
替換成 _
即可)围来,通過這種命名方式來查找 Kotlin
層的調(diào)用方法跺涤,該方法中 extern "C"
的作用是讓 C++
支持 C
的方法匈睁,JNIEXPORT xxx JNICALL
代表這是一個 JNI
方法,xxx
表示返回的方法類型桶错,在 JNI
中航唆,都有 Kotlin
對應(yīng)的數(shù)據(jù)類型
JNI 數(shù)據(jù)類型
JNI
對應(yīng) Java
的數(shù)據(jù)類型如下,也可以直接查看 jni.h
頭文件
JNI類型 | Java類型 | 類型描述 |
---|---|---|
jboolean | boolean | 無符號8位 |
jbyte | byte | 無符號8位 |
jchar | char | 無符號16位 |
jshort | short | 有符號16位 |
jint | int | 有符號32位 |
jlong | long | 有符號64位 |
jfloat | float | 有符號32位 |
jdouble | double | 有符號64位 |
因?yàn)?String
不屬于基本類型牛曹,所以不定義在這佛点,需要返回 jsrting
類型,只能通過 char *
進(jìn)行相應(yīng)的轉(zhuǎn)換黎比,所以上述的函數(shù)中超营,使用 env->NewStringUTF(hello.c_str())
方法,生成 jstring
并返回阅虫,然后在 Kotlin
層通過調(diào)用 stringFromJNI
方法就可以將 native
層返回的字符串顯示出來演闭,JNI
的基本使用就這么多啦,接著通過一些使用颓帝,熟悉一些方法米碰,比如實(shí)現(xiàn)字符串的拼接
external fun stringCat(a: String, b: String): String
回到 c++
層做具體的實(shí)現(xiàn),前面提到因?yàn)樵?C++
中字符串拼接不能直接通過 jstring
相加實(shí)現(xiàn)购城,需要通過 char *
進(jìn)行拼接吕座,所以就需要封裝一個 jstring2Char
的方法進(jìn)行轉(zhuǎn)換
char *jstring2Char(JNIEnv *env, jstring jstr) {
char *rtn = nullptr;
jclass clazz = env->FindClass("java/lang/String");
jstring strenCode = env->NewStringUTF("UTF-8");
jmethodID mid = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
auto barr = (jbyteArray) (env->CallObjectMethod(jstr, mid, strenCode));
jsize alen = env->GetArrayLength(barr);
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
// malloc(bytes) 方法分配 bytes 字節(jié),并返回這塊內(nèi)存的指針瘪板,
// malloc 分配的內(nèi)存記得使用 free 進(jìn)行釋放吴趴,否則會內(nèi)存泄漏
rtn = static_cast<char *>(malloc(static_cast<size_t>(alen + 1)));
// memcpy(void*dest, const void *src, size_t n)
// 由 src 指向地址為起始地址的連續(xù) n 個字節(jié)的數(shù)據(jù)復(fù)制到以 destin 指向地址為起始地址的空間內(nèi)
memcpy(rtn, ba, static_cast<size_t>(alen));
rtn[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);
return rtn;
}
定義完轉(zhuǎn)換方法,直接調(diào)用即可侮攀,記得釋放內(nèi)存
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_stringCat(JNIEnv *env, jobject, jstring a, jstring b){
char *first = jstring2Char(env, a);
char *second = jstring2Char(env, b);
std::strcat(first, second);
free(first);
free(second);
return env->NewStringUTF(first);
}
靜態(tài) JNI 方法
在很多情況下锣枝,都不會將 JNI
方法直接定義在 Activity
,而是封裝到公共方法中兰英,方便調(diào)用撇叁,那么在公共方法類調(diào)用除了通過該類的實(shí)例,調(diào)用相應(yīng)方法畦贸,還有就是設(shè)置該方法為靜態(tài)方法陨闹,那么這種情況和上述有啥區(qū)別呢,其實(shí)區(qū)別不是很大薄坏,只需要將 native
端的方法中的參數(shù) jobject
替換成 jclass
即可正林,但是在 Kotlin
端,除了在半生對象中聲明該 native
方法颤殴,還需要增加 JvmStatic
注解才行觅廓,例如有如下的一個方法
class JniUtils {
companion object {
@JvmStatic
external fun plus(a: Int, b: Int): Int
}
}
那么在 native
端生成 JNI
方法和前面提到的類似,只需替換參數(shù)類型即可
extern "C" JNIEXPORT jint JNICALL
Java_com_xxx_JniUtils_plus(JNIEnv *env, jclass, jint , jint b){
return a + b;
}
C++ 調(diào)用 Kotlin 方法
前面介紹了如何在 Kotlin
中調(diào)用 native
方法涵但,當(dāng)然杈绸,在 c++
層也可以調(diào)用 Kotlin
層的方法帖蔓。假設(shè)在 MainActivity
中有一個 callMe(message: String)
和 call(message:String)
方法,在調(diào)用 call
的時候瞳脓,同時內(nèi)部調(diào)用 callMe
方法塑娇,當(dāng)然直接調(diào)用很簡單,這邊通過 JNI
來實(shí)現(xiàn)
fun callMe(message: String){
Log.e(TAG, message) // 只做簡單的打印
}
external fun call(message: String)
native
實(shí)現(xiàn) call
方法上面已經(jīng)介紹了劫侧,接下來介紹在 JNI
內(nèi)部調(diào)用 callMe
方法
extern "C" JNIEXPORT void JNICALL
Java_com_xxx_MainActivity_call(JNIEnv *env, jobject instance, jstring msg){
const char *methodName = "callMe"; // 指定需要調(diào)用的方法名
jclass clazz = env->FindClass("com.xxx.MainActivity"); //查找對應(yīng)的類埋酬,指定對應(yīng)的包名和類
// 根據(jù)所在類和方法名查找方法的 ID,最后一個參數(shù)為方法的簽名烧栋,稍后做解釋
jmethodID mid = env->GetMethodId(clazz, methodName, "(Ljava/lang/String;)V");
env->CallVoidMethod(instance, mid, msg); // 根據(jù)返回的類型写妥,調(diào)用方法,傳入相應(yīng)參數(shù)
}
當(dāng) Kotlin
層調(diào)用 call
方法的時候审姓,就會通過 JNI
調(diào)用 callMe
方法珍特,執(zhí)行 callMe
的內(nèi)部邏輯。在上面提到了「簽名」這個東西魔吐,這邊列出簽名的表示方法
類型 | 簽名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
數(shù)組 | [ |
String/Object | Ljava/lang/String; Ljava/lang/Object; |
普通類(com.example.className) | Lcom/example/className; |
嵌套類(com.example.className.Inner) | Lcom/example/className$Inner; |
所以方法的簽名的規(guī)則就是根據(jù)傳入的參數(shù)類型和返回的類型扎筒,替換成相應(yīng)的簽名即可,例如:call(Student s, int a): String
方法的簽名為 (Lcom/xxx/Student;I)Ljava/lang/String;
如果是內(nèi)部類則使用 $ 表示嵌套
C++ 獲取 Kotlin 的內(nèi)部參數(shù)
假設(shè)我們在 MainActivity
有個私有參數(shù) name
酬姆,如果外部有個類需要獲取這個參數(shù)嗜桌,可以通過 MainActivty
內(nèi)部的共有方法來獲取,假如沒有這個共有方法該咋辦呢辞色,當(dāng)然我們可以通過 JNI
來獲取
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_getField(JNIEnv *env, jobjcet instance){
jclass clazz = env->FindClass("com.xxx.MainActivity"); // 根據(jù)類的包名來查找相應(yīng)的類
// 根據(jù)類和參數(shù)名來獲取該參數(shù)骨宠,第三個參數(shù)為參數(shù)的簽名,即類型在 JNI 對應(yīng)的簽名
jfieldID fid = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
// 因?yàn)?String 不是基本類型淫僻,所以只能通過 GetObjectField 進(jìn)行獲取诱篷,然后進(jìn)行強(qiáng)轉(zhuǎn)
// 如果是 int 等基本類型壶唤,提供了 GetIntField 等獲取方法雳灵,auto 為可自行根據(jù)結(jié)果判斷類型
auto name = (jstring)(env->GetObjectField(instance, fid));
return name;
}
當(dāng)在外部通過 getField
方法即可獲取到該私有屬性,這個例子僅為例子而已...
C++ 獲取普通類的參數(shù)信息
假設(shè)我們有一個類闸盔,例如 Student
里面有一些名字悯辙,年齡等屬性,然后通過 JNI
將這些屬性轉(zhuǎn)成 String
返回迎吵,那么就需要涉及到獲取參數(shù)的字段信息了
// 定義一個普通類 Student
data class Student(val firstName: String, val lastName: String, val age: Int)
// 在 MAinActivity 定義一個轉(zhuǎn)換的方法
external fun printStudent(Student student): String
那么在 C++
層就需要將 student
內(nèi)部的信息都獲取出來躲撰,并拼接到字符串,然后返回
extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_printStudent(JNIEnv *env, jobject, jobject student){
jcalss clazz = env->GetObjectClass(student); // 獲取傳入?yún)?shù)對應(yīng)的類
// 通過參數(shù)名和簽名击费,去對應(yīng)的 class 獲取相應(yīng)的 FieldID拢蛋,
// 然后根據(jù) FiedlID 通過 GetObjectField 方法獲取對應(yīng)的屬性
auto firstName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "firstName", "Ljava/lang/String;")));
auto lastName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "lastName", "Ljava/lang/String;")));
// int 為基本類型,可直接通過獲取對應(yīng)類型屬性的方法獲取
auto age = env->GetIntField(student, env->GetFieldID(clazz, "age", "I"));
char *cFirstName = jstring2Char(firstName);
char *cLastName = jstring2Char(lastName);
std::string cAge = std::to_string(age);
strcat(cFirstName, " ");
strcat(cFirstName, cLastName);
strcat(cFirstName, " is ");
strcat(cFirstName, cAge.c_str());
strcat(cFirstName, " years old");
free(cFirstName);
free(cLastName);
return env->NewStringUTF(cFirstName);
}
當(dāng)外部調(diào)用 printStudent
方法的時候就會將 student
的屬性打印出來
動態(tài)注冊
在前面的 JNI
方法中蔫巩,每個方法都需要寫很長的一段類名谆棱,非常容易出錯快压,那么能不能省略包名呢,當(dāng)然是可以垃瞧,通過動態(tài)注冊就可以讓這個麻煩的方法名變得簡略
動態(tài)注冊蔫劣,需要指定一個方法列表,用來存放同個包名下的方法个从,存放的方式如下:
{ Kotlin 層方法名脉幢, 方法前面, JNI 函數(shù)指針} // 函數(shù)指針固定為 ```(void *) JNI 方法名```
例如我們前面提到的方法嗦锐,放到一個列表中
static JNINativeMethod jniMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
{"stringCat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void *) stringCat},
{"call", "(Ljava/lang/String;)V", (void *) call},
{"getField", "()Ljava/lang/String;", (void *) getField},
{"printStudent", "(Lcom/xxx/Student;)Ljava/lang/String;", (void *) printStudent},
};
接著就是需要注冊這些方法了嫌松,封裝一個通用的方法,注冊成功返回 JNI_TRUE
否則 JNI_FALSE
static int registerNativeMethods(JNIEnv *env, const char *className,
JNINativeMethod *getMethods, int sumNum){
jclass clazz = env->FindClass(className); // 根據(jù)類名去查找相應(yīng)類意推,包含 JNINativeMethod 列表所有方法
if (clazz == nullptr) return JNI_FALSE; // 未找到 class 則認(rèn)為注冊失敗
// 根據(jù)所有的方法名和數(shù)量進(jìn)行注冊豆瘫,如果結(jié)果返回小于 0 則認(rèn)為注冊失敗
if (env->RegisterNatives(clazz, getMethods, methodSum) < 0) return JNI_FALSE;
return JNI_TRUE;
}
接著就需要實(shí)現(xiàn) JNI_OnLoad
方法(定義在 jni.h
頭文件中),對上述的方法進(jìn)行注冊菊值,該方法會返回一個版本號
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
JNIEnv *env = nullptr;
// 檢測環(huán)境失敗返回 -1
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
assert(env != nullptr);
// 注冊失敗返回 -1
if (!registerNativeMethods(
env, jniClazz, jniMethods, sizeof(jniMethods) / sizeof(jniMethods[0]))) {
return -1;
}
return JNI_VERSION_1_6;
}
這樣幾步就完成了 JNI
方法的動態(tài)注冊外驱,只需要全局定義 className
即可,不需要每次都在方法聲明完整包路徑
內(nèi)存釋放
在 C++
中腻窒,非常重要的一步就是內(nèi)存釋放昵宇,否則就會造成內(nèi)存泄漏,分分鐘給你炸開
哪些需要手動釋放
- 不需要手動釋放(基本類型):jint儿子,jlong 等等
- 需要手動釋放(引用類型瓦哎,數(shù)組家族):jstring,jobject 柔逼,jobjectArray蒋譬,jintArray ,jclass 愉适,jmethodID
釋放方法(該部分參考自《JNI手動釋放內(nèi)存》)
-
jstring & char *
// 創(chuàng)建 jstring 和 char* jstring jstr = (jstring)(jniEnv->CallObjectMethod(jniEnv, mPerson, getName)); char* cstr = (char*) jniEnv->GetStringUTFChars(jniEnv,jstr, 0); // 釋放 jniEnv->ReleaseStringUTFChars(jniEnv, jstr, cstr); jniEnv->DeleteLocalRef(jniEnv, jstr);jbyteArray audioArray = jnienv->NewByteArray(frameSize); jnienv->DeleteLocalRef(audioArray)
-
jobject犯助,jobjectArray,jclass 维咸,jmethodID 等引用類型
jniEnv->DeleteLocalRef(jniEnv, XXX);
-
jbyteArray
jbyteArray arr = jnienv->NewByteArray(frameSize); jnienv->DeleteLocalRef(arr);
-
GetByteArrayElements
jbyte* array= jniEnv->GetByteArrayElements(env,jarray,&isCopy); jniEnv->ReleaseByteArrayElements(env,jarray,array,0);
-
NewGlobalRef
jobject ref= env->NewGlobalRef(customObj); env->DeleteGlobalRef(customObj);
舉個例子
在 Android
中剂买,經(jīng)常需要用到 Context
獲取一些相關(guān)的信息,這邊舉個獲取屏幕信息的例子
#include <jni.h>
#include <string>
#include <iostream>
#include <android/log.h>
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__)
// 獲取當(dāng)前的 Context
jobject getAndroidApplication(JNIEnv *env) {
jclass activityThreadClazz = env->FindClass("android/app/ActivityThread");
jmethodID jCurrentActivityThread =
env->GetStaticMethodID(activityThreadClazz,
"currentActivityThread", "()Landroid/app/ActivityThread;");
jobject currentActivityThread =
env->CallStaticObjectMethod(activityThreadClazz, jCurrentActivityThread);
jmethodID jGetApplication =
env->GetMethodID(activityThreadClazz, "getApplication", "()Landroid/app/Application;");
return env->CallObjectMethod(currentActivityThread, jGetApplication);
}
extern "C" JNIEXPORT void JNICALL
Java_com_demo_kuky_jniwidth_MainActivity_jniDensity(JNIEnv *env, jobject) {
jobject instance = getAndroidApplication(env);
jclass contextClazz = env->GetObjectClass(instance);
// 獲取 `getResources` 方法
jmethodID getResources = env->GetMethodID(contextClazz, "getResources",
"()Landroid/content/res/Resources;");
jobject resourceInstance = env->CallObjectMethod(instance, getResources);
jclass resourceClazz = env->GetObjectClass(resourceInstance);
// 獲取 Resources 下的 `getDisplayMetrics` 方法
jmethodID getDisplayMetrics = env->GetMethodID(resourceClazz, "getDisplayMetrics",
"()Landroid/util/DisplayMetrics;");
jobject metricsInstance = env->CallObjectMethod(resourceInstance, getDisplayMetrics);
jclass metricsClazz = env->GetObjectClass(metricsInstance);
// 獲取 DisplayMetrics 下的一些參數(shù)
jfieldID densityId = env->GetFieldID(metricsClazz, "density", "F");
jfloat density = env->GetFloatField(metricsInstance, densityId);
jfieldID widthId = env->GetFieldID(metricsClazz, "widthPixels", "I");
jint width = env->GetIntField(metricsInstance, widthId);
jfieldID heightId = env->GetFieldID(metricsClazz, "heightPixels", "I");
jint height = env->GetIntField(metricsInstance, heightId);
LOGE("get density: %f, width: %d, height: %d", density, width, height);
}
目前使用到的就那么多啦癌蓖,后面有更多的方法涉及到瞬哼,再進(jìn)行添加,Enjoy it ~