初識 JNI

JNI 作為 Java/Kotlin(原生端) 同 C/C++ 端交互的工具,是學(xué)習(xí) ffmpeg 的一個(gè)前提,這邊做一個(gè)學(xué)習(xí)過程中的記錄。通過 Android Studio 可以快速創(chuàng)建一個(gè) JNI 項(xiàng)目(創(chuàng)建時(shí)候選擇 Native C++ 即可,會(huì)自動(dòng)配置 CMakeList 等文件),該文基于 AS 3.5

loadLiabry

src 文件夾下相比一般的 AS 項(xiàng)目多了 cpp 文件夾,該文件夾下有一個(gè) .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 中有一個(gè)使用 external 修飾的方法(如果是 java 則使用 native 關(guān)鍵詞修飾)

external fun stringFromJNI(): String

通過該方法,會(huì)去調(diào)用 cpp 層的 native 方法,可以看下 native-lib.cpp 文件,內(nèi)部定義了一個(gè)方法

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 代表這是一個(gè) 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)行拼接,所以就需要封裝一個(gè) 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)行釋放,否則會(huì)內(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 個(gè)字節(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 方法

在很多情況下,都不會(huì)將 JNI 方法直接定義在 Activity,而是封裝到公共方法中,方便調(diào)用,那么在公共方法類調(diào)用除了通過該類的實(shí)例,調(diào)用相應(yīng)方法,還有就是設(shè)置該方法為靜態(tài)方法,那么這種情況和上述有啥區(qū)別呢,其實(shí)區(qū)別不是很大,只需要將 native 端的方法中的參數(shù) jobject 替換成 jclass 即可,但是在 Kotlin 端,除了在半生對象中聲明該 native 方法,還需要增加 JvmStatic 注解才行,例如有如下的一個(gè)方法

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 中有一個(gè) callMe(message: String)call(message:String) 方法,在調(diào)用 call 的時(shí)候,同時(shí)內(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,最后一個(gè)參數(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 方法的時(shí)候,就會(huì)通過 JNI 調(diào)用 callMe 方法,執(zhí)行 callMe 的內(nèi)部邏輯。在上面提到了「簽名」這個(gè)東西,這邊列出簽名的表示方法

類型 簽名
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 有個(gè)私有參數(shù) name,如果外部有個(gè)類需要獲取這個(gè)參數(shù),可以通過 MainActivty 內(nèi)部的共有方法來獲取,假如沒有這個(gè)共有方法該咋辦呢,當(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ù),第三個(gè)參數(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 方法即可獲取到該私有屬性,這個(gè)例子僅為例子而已...

C++ 獲取普通類的參數(shù)信息

假設(shè)我們有一個(gè)類,例如 Student 里面有一些名字,年齡等屬性,然后通過 JNI 將這些屬性轉(zhuǎn)成 String 返回,那么就需要涉及到獲取參數(shù)的字段信息了

// 定義一個(gè)普通類 Student
data class Student(val firstName: String, val lastName: String, val age: Int)

// 在 MAinActivity 定義一個(gè)轉(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 方法的時(shí)候就會(huì)將 student 的屬性打印出來

動(dòng)態(tài)注冊

在前面的 JNI 方法中,每個(gè)方法都需要寫很長的一段類名,非常容易出錯(cuò),那么能不能省略包名呢,當(dāng)然是可以,通過動(dòng)態(tài)注冊就可以讓這個(gè)麻煩的方法名變得簡略

動(dòng)態(tài)注冊,需要指定一個(gè)方法列表,用來存放同個(gè)包名下的方法,存放的方式如下:

{ Kotlin 層方法名, 方法前面, JNI 函數(shù)指針} // 函數(shù)指針固定為 ```(void *) JNI 方法名```

例如我們前面提到的方法,放到一個(gè)列表中

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},
};

接著就是需要注冊這些方法了,封裝一個(gè)通用的方法,注冊成功返回 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)行注冊,該方法會(huì)返回一個(gè)版本號

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 方法的動(dòng)態(tài)注冊,只需要全局定義 className 即可,不需要每次都在方法聲明完整包路徑

內(nèi)存釋放

C++ 中,非常重要的一步就是內(nèi)存釋放,否則就會(huì)造成內(nèi)存泄漏,分分鐘給你炸開

哪些需要手動(dòng)釋放
  • 不需要手動(dòng)釋放(基本類型):jint,jlong 等等
  • 需要手動(dòng)釋放(引用類型,數(shù)組家族):jstring,jobject ,jobjectArray,jintArray ,jclass ,jmethodID
釋放方法(該部分參考自《JNI手動(dòng)釋放內(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);
    

舉個(gè)例子

Android 中,經(jīng)常需要用到 Context 獲取一些相關(guān)的信息,這邊舉個(gè)獲取屏幕信息的例子

#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 ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容