1. 程式人生 > >Android平臺的jni---註冊native函式

Android平臺的jni---註冊native函式

Android呼叫JNI的方式

Android JNI 的實現包括兩種實現方法:靜態和動態。兩種方法的區別如下:

靜態:先由Java得到本地方法的宣告“System.loadLibrary(“hello_jni”);”,然後再通過JNI實現該宣告方法。

根據函式名找到對應的JNI函式:Java層呼叫函式時,會從對應的JNI中尋找該函式,如果沒有就會報錯,如果存在則會建立一個關聯聯絡,以後在呼叫時會直接使用這個函式,這部分的操作由虛擬機器完成。
靜態方法就是根據函式名來遍歷java和jni函式之間的關聯,而且要求jni層函式的名字必須遵循
特定的格式,其缺點在於:
1)javah生成的jni層函式特別長;
2)初次呼叫native函式時要根據名字搜尋對應的jni層函式來建立關聯聯絡,這樣影響效率。

靜態方法(固定函式名對映方式)實現流程:

1.先在Java中載入so庫

static{
        System.loadLibrary("strRcgMain");
        Log.i("MyView", "loadLibrary");
    }

2.Java層呼叫函式:

public native String recgnt(String filePath,String dicPath);

3.根據Java層的類名函式名,生成jni介面,可直接用ndk生成:
①定位到src位置(JDK1.7及以後)

E:>cd /d E:\Android\work\數字串識別1
E:\Android\work\數字串識別1>cd src

②用javah生成c相關標頭檔案

E:\Android\work\數字串識別1\src>javah -jni -encoding UTF-8 com.ppl.number.MyView
這裡寫圖片描述

③選中專案點選F5重新整理後生成相關的.h檔案
這裡寫圖片描述

.h檔案 中存在Java層需要呼叫的c中的函式。
這裡寫圖片描述

④將該.h檔案剪下至jni資料夾中,實現標頭檔案中宣告的函式。同時將其他所需的cpp h檔案copy至jni資料夾下,同時更改Android.mk檔案。

strRcgMain.cpp的作用就是實現com_ppl_number_MyView.h中宣告的函式。

#include <jni.h>
#include "NumRecognView.h" #include <vector> #include "LogPrint.h" using namespace std; #include <android/log.h> #include "com_ppl_number_MyView.h" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define DLL_EXP __attribute__ ((visibility("default"))) #define DLL_LOCAL __attribute__ ((visibility("hidden"))) extern __mallocfunc void* malloc(size_t); extern void* memcpy(void *, const void *, size_t); JNIEXPORT jstring JNICALL Java_com_ppl_number_MyView_recgnt (JNIEnv * env, jobject clazz, jstring filePath, jstring dicPath){ //功能實現區 }

這裡寫圖片描述
⑤編譯生成庫檔案
生成庫檔案所在目錄:libs/armeabi/libstrRcgMain.so

⑥直接執行程式,我第一個交叉編譯的專案是將研究生期間的空中手寫字串識別演算法,通過SurfaceView跟蹤手指軌跡,結果如下:

這裡寫圖片描述

動態方法(註冊函式對映表方式)

JNI 允許提供一個函式對映表,註冊給Jave虛擬機器,這樣Jvm就可以用函式對映表來呼叫相應的函式,就可以不必通過函式名來查詢需要呼叫的函數了。

Java與JNI通過JNINativeMethod的結構來建立聯絡,它在jni.h中被定義。

結構內容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一個變數name是Java中函式的名字。
第二個變數signature,用字串是描述了函式的引數和返回值
第三個變數fnPtr是函式指標,指向C函式。

當java通過System.loadLibrary載入完JNI動態庫後,緊接著會查詢一個JNI_OnLoad的函式,如果有,就呼叫它,
而動態註冊的工作就是在這裡完成的。

1)JNI_OnLoad()函式
JNI_OnLoad()函式在VM執行System.loadLibrary(xxx)函式時被呼叫,它有兩個重要的作用:
指定JNI版本:告訴VM該元件使用那一個JNI版本(若未提供JNI_OnLoad()函式,VM會預設該使用最老的JNI 1.1版),如果要使用新版本的JNI,
例如JNI 1.4版,則必須由JNI_OnLoad()函式返回常量JNI_VERSION_1_4(該常量定義在jni.h中) 來告知VM。
初始化設定,當VM執行到System.loadLibrary()函式時,會立即先呼叫JNI_OnLoad()方法,因此在該方法中進行各種資源的初始化操作最為恰當,
2)RegisterNatives
RegisterNatives在AndroidRunTime裡定義
syntax:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

實現流程:

①先在Java中載入so庫

static{
        System.loadLibrary("strRcgMain");
        Log.i("MyView", "loadLibrary");
    }

②Java層呼叫函式:

public native String recgnt(String filePath,String dicPath);

③在jni目錄下新建newRec.cpp:

  • 實現識別演算法
//實現recgnt方法
jstring recgnt_number(JNIEnv * env, jobject clazz, jstring filePath, jstring dicPath)
{
    //功能實現區
}
  • 需要註冊的函式列表,放在JNINativeMethod 型別的陣列中
/*需要註冊的函式列表,放在JNINativeMethod 型別的陣列中, 
以後如果需要增加函式,只需在這裡新增就行了 
引數: 
1.java程式碼中用native關鍵字宣告的函式名字串 
2.簽名(傳進來引數型別和返回值型別的說明)  
3.C/C++中對應函式的函式名(地址) 
*/ 
static JNINativeMethod getMethods[] = {  
    {
        "recgnt", 
        "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", 
        (void *)recgnt_number
    },  
}; 
  • 註冊回撥函式
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){  
    JNIEnv* env = NULL;  
   //判斷虛擬機器狀態是否有問題  
    if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!= JNI_OK){  
        return -1;  
    }  
    assert(env != NULL);  
    //開始註冊函式 registerNatives -》registerNativeMethods -》env->RegisterNatives  
    //指定類的路徑,通過FindClass 方法來找到對應的類  

    const char* className  = "com/ppl/number/MyView"; 
    jclass clazz;  
    //找到宣告native方法的類  
    clazz = env->FindClass(className); 
    if(clazz == NULL){  
        return JNI_FALSE;  
    } 

    //註冊函式 引數:java類 所要註冊的函式陣列 註冊函式的個數  
    if(env->RegisterNatives(clazz,getMethods,sizeof(getMethods)/ sizeof(getMethods[0])) < 0){  
        return JNI_FALSE;  
    } 

    //返回jni 的版本   
    return JNI_VERSION_1_6;  
}  
JNINativeMethod的定義如下:

typedef struct {

    constchar*name;      // Java中申明的Native函式名稱

    constchar* signature; // 描述了函式的引數和返回值

    void* fnPtr;           // 函式指標,指向C函式

} JNINativeMethod;

通過method_table,就將本地的recgnt_number()函式和註冊到Java中的recgnt()繫結起來了。當我們在Java中呼叫recgnt()時,實際呼叫的是recgnt_number()。

④. 在jni目錄下新建Android.mk,Android.mk的程式碼如。

⑤ 生成.so庫檔案:
“libs/armeabi/libstrRcgMain.so”庫檔案
⑥ 在eclipse下執行工程,OK。

使用RegisterNatives的好處有三點:

1、C++中函式命名自由,不必像javah自動生成的函式宣告那樣,拘泥特定的命名方式;
2、效率高。傳統方式下,Java類call本地函式時,通常是依靠VM去動態尋找.so中的本地函式(因此它們才需要特定規則的命名格式),而使用RegisterNatives將本地函式向VM進行登記,可以讓其更有效率的找到函式;
3、執行時動態調整本地函式與Java函式值之間的對映關係,只需要多次call RegisterNatives()方法,並傳入不同的對映表引數即可。

jni開發中的常見錯誤

  • java.lang.UnsatisfiedLinkError: Native method not found: 本地方法沒有找到
    • 本地函式名寫錯
    • 忘記載入.so檔案 沒有呼叫System.loadlibrary
  • findLibrary returned null
    • System.loadLibrary(“libhello”); 載入動態連結庫時 動態連結庫名字寫錯
    • 平臺型別錯誤 把只支援arm平臺的.so檔案部署到了 x86cpu的裝置上
      • 在jni目錄下建立 Application.mk 在裡面指定
      • APP_ABI := armeabi
        APP_PLATFORM := android-14