Android JNI 函數(shù)注冊的兩種方式(靜態(tài)注冊/動(dòng)態(tài)注冊)

JNI/NDK

在Android開發(fā)中,由于種種原因我們需要調(diào)用C/C++代碼, 這個(gè)時(shí)候就要用到Android開發(fā)者都聽說過的JNI(Java Native Interface)了, 在調(diào)用JNI相關(guān)方法之前, 要對(duì)java中native關(guān)鍵字定義的方法進(jìn)行注冊, 注冊方式有兩種: 靜態(tài)注冊和動(dòng)態(tài)注冊, 兩者優(yōu)缺點(diǎn)如下:

  • 靜態(tài)注冊
    優(yōu)點(diǎn): 理解和使用方式簡單, 屬于傻瓜式操作, 使用相關(guān)工具按流程操作就行, 出錯(cuò)率低
    缺點(diǎn): 當(dāng)需要更改類名,包名或者方法時(shí), 需要按照之前方法重新生成頭文件, 靈活性不高
  • 動(dòng)態(tài)注冊
    優(yōu)點(diǎn): 靈活性高, 更改類名,包名或方法時(shí), 只需對(duì)更改模塊進(jìn)行少量修改, 效率高
    缺點(diǎn): 對(duì)新手來說稍微有點(diǎn)難理解, 同時(shí)會(huì)由于搞錯(cuò)簽名, 方法, 導(dǎo)致注冊失敗

靜態(tài)注冊

此注冊方法是初學(xué)者經(jīng)常用到的, 比較常見, 這里簡單說下流程,
1.編寫一個(gè)java類,在里面加載對(duì)應(yīng)的so庫并且通過native關(guān)鍵字定義需要調(diào)用的函數(shù)

package com.example.wenzhe.myjni;
/**
 * Created by wenzhe on 16-1-27.
 */
public class JniTest {
public native int getRandomNum();
public native String getNativeString();

static {
    System.loadLibrary("HelloJni");
    }
}

2.在命令行下輸入 javac JniTest.java 生成JniTest.class文件
然后在src目錄下通過 javah com.example.wenzhe.myjni.JniTest 生成 com_example_wenzhe_myjni_JniTest.h 頭文件
3.將頭文件拷貝到j(luò)ni目錄下(eclipse在src同級(jí)目錄建立文件夾,Android studio 在java同級(jí)目錄建立文件夾)
4.編寫C/C++源代碼 并把剛拷貝的頭文件包含進(jìn)去 ,復(fù)制頭文件中函數(shù)的定義部分,并實(shí)現(xiàn)其中的你想要的功能

然后編寫Android.mk Application.mk(Application.mk主要用來定義適應(yīng)的平臺(tái),x86 arm等)

Android.mk如下:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := HelloJni
LOCAL_SRC_FILES := HelloJni.cpp

include $(BUILD_SHARED_LIBRARY)

Application.mk如下:

#支持標(biāo)準(zhǔn)C++特性
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
#支持的CPU架構(gòu)
APP_ABI := armeabi-v7a
#Android 版本
APP_PLATFORM := android-22

include $(BUILD_SHARED_LIBRARY)

其中LOCAL_MODULE定義的名字就是生成的so庫名字,so庫前面都會(huì)有個(gè)lib前綴,上面生產(chǎn)的so應(yīng)該為 libHelloJni.so

5.在命令行中進(jìn)入jni目錄,輸入ndk-build 即可生產(chǎn)對(duì)應(yīng)so庫,會(huì)自動(dòng)放在libs文件夾下 至此就可以運(yùn)行程序了

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

動(dòng)態(tài)注冊基本思想是在JNI_Onload()函數(shù)中通過JNI中提供的RegisterNatives()方法來將C/C++方法和java方法對(duì)應(yīng)起來(注冊), 我們在調(diào)用 System.loadLibrary的時(shí)候,會(huì)在C/C++文件中回調(diào)一個(gè)名為 JNI_OnLoad ()的函數(shù),在這個(gè)函數(shù)中一般是做一些初始化相關(guān)操作, 我們可以在這個(gè)方法里面注冊函數(shù), 注冊整體流程如下:

  1. 編寫Java端的相關(guān)native方法
  2. 編寫C/C++代碼, 實(shí)現(xiàn)JNI_Onload()方法
  3. 將Java 方法和 C/C++方法通過簽名信息一一對(duì)應(yīng)起來
  4. 通過JavaVM獲取JNIEnv, JNIEnv主要用于獲取Java類和調(diào)用一些JNI提供的方法
  5. 使用類名和對(duì)應(yīng)起來的方法作為參數(shù), 調(diào)用JNI提供的函數(shù)RegisterNatives()注冊方法

示例代碼如下:

// jni頭文件 
#include <jni.h>
 
#include <cassert>
#include <cstdlib>
#include <iostream>
using namespace std;
 
 
//native 方法實(shí)現(xiàn)
jint get_random_num(){
    return rand();
}
/*需要注冊的函數(shù)列表,放在JNINativeMethod 類型的數(shù)組中,
以后如果需要增加函數(shù),只需在這里添加就行了
參數(shù):
1.java中用native關(guān)鍵字聲明的函數(shù)名
2.簽名(傳進(jìn)來參數(shù)類型和返回值類型的說明) 
3.C/C++中對(duì)應(yīng)函數(shù)的函數(shù)名(地址)
*/
static JNINativeMethod getMethods[] = {
        {"getRandomNum","()I",(void*)get_random_num},
};
//此函數(shù)通過調(diào)用RegisterNatives方法來注冊我們的函數(shù)
static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* getMethods,int methodsNum){
    jclass clazz;
    //找到聲明native方法的類
    clazz = env->FindClass(className);
    if(clazz == NULL){
        return JNI_FALSE;
    }
   //注冊函數(shù) 參數(shù):java類 所要注冊的函數(shù)數(shù)組 注冊函數(shù)的個(gè)數(shù)
    if(env->RegisterNatives(clazz,getMethods,methodsNum) < 0){
        return JNI_FALSE;
    }
    return JNI_TRUE;
}
 
static int registerNatives(JNIEnv* env){
    //指定類的路徑,通過FindClass 方法來找到對(duì)應(yīng)的類
    const char* className  = "com/example/wenzhe/myjni/JniTest";
    return registerNativeMethods(env,className,getMethods, sizeof(getMethods)/ sizeof(getMethods[0]));
}
//回調(diào)函數(shù)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
   //獲取JNIEnv
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);
    //注冊函數(shù) registerNatives ->registerNativeMethods ->env->RegisterNatives
    if(!registerNatives(env)){
        return -1;
    }
    //返回jni 的版本 
    return JNI_VERSION_1_6;
}

上面的代碼就能實(shí)現(xiàn)動(dòng)態(tài)注冊JNI了 以后要增加函數(shù)只需在java文件中聲明native方法,在C/C++文件中實(shí)現(xiàn),
并在getMethods數(shù)組添加一個(gè)元素并指明對(duì)應(yīng)關(guān)系,通過ndk-build 生成so庫就可以運(yùn)行了
其中JNI版本可以在jni.h頭文件中去查看支持哪些版本,一般定義在文件最后幾行

JNI 簽名

動(dòng)態(tài)注冊中 JNINativeMethod 結(jié)構(gòu)體中第二個(gè)參數(shù)需注意
括號(hào)內(nèi)代表傳入?yún)?shù)的簽名符號(hào),為空可以不寫,括號(hào)外代表返回參數(shù)的簽名符號(hào),為空填寫 V,對(duì)應(yīng)關(guān)系入下表

簽名符號(hào) C/C++ java
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
[Z jbooleanArray boolean[]
[I jintArray int[]
[J jlongArray long[]
[D jdoubleArray double[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
L完整包名加類名; jobject class

舉個(gè)例子:

傳入的java參數(shù)有兩個(gè) 分別是 int 和 long[] 函數(shù)返回值為 String 即函數(shù)的定義為:String getString(int a ,long[] b)
簽名就應(yīng)該是 :"(I[J)Ljava/lang/String;"(不要漏掉英文分號(hào))
如果有內(nèi)部類 則用 $ 來分隔 如:Landroid/os/FileUtils$FileStatus;

總結(jié)

當(dāng)熟悉動(dòng)態(tài)注冊后, 動(dòng)態(tài)注冊無疑是注冊函數(shù)的更好方式, 唯一要注意的是注冊函數(shù)時(shí), 需要額外小心, 別把類名,函數(shù)名和簽名寫錯(cuò)了, 不然loadLibraries時(shí)會(huì)導(dǎo)致應(yīng)用Crash, 關(guān)于JNI部分知識(shí), 我還會(huì)寫一篇關(guān)于如何高效傳遞數(shù)據(jù)以及JNI開發(fā)過程中的一些坑的總結(jié).

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容