1. 程式人生 > >Android JNI 函式註冊的兩種方式(靜態註冊/動態註冊)

Android JNI 函式註冊的兩種方式(靜態註冊/動態註冊)

JNI/NDK

在Android開發中,由於種種原因我們需要呼叫C/C++程式碼, 這個時候就要用到Android開發者都聽說過的JNI(Java Native Interface)了, 在呼叫JNI相關方法之前, 要對java中native關鍵字定義的方法進行註冊, 註冊方式有兩種: 靜態註冊和動態註冊, 兩者優缺點如下:

  • 靜態註冊
    優點: 理解和使用方式簡單, 屬於傻瓜式操作, 使用相關工具按流程操作就行, 出錯率低
    缺點: 當需要更改類名,包名或者方法時, 需要按照之前方法重新生成標頭檔案, 靈活性不高
  • 動態註冊
    優點: 靈活性高, 更改類名,包名或方法時, 只需對更改模組進行少量修改, 效率高
    缺點: 對新手來說稍微有點難理解, 同時會由於搞錯簽名, 方法, 導致註冊失敗

靜態註冊

此註冊方法是初學者經常用到的, 比較常見, 這裡簡單說下流程,
1.編寫一個java類,在裡面載入對應的so庫並且通過native關鍵字定義需要呼叫的函式

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.將標頭檔案拷貝到jni目錄下(eclipse在src同級目錄建立資料夾,Android studio 在java同級目錄建立資料夾)
4.編寫C/C++原始碼 並把剛拷貝的標頭檔案包含進去 ,複製標頭檔案中函式的定義部分,並實現其中的你想要的功能

然後編寫Android.mk Application.mk(Application.mk主要用來定義適應的平臺,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如下:

#支援標準C++特性
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
#支援的CPU架構
APP_ABI := armeabi-v7a
#Android 版本
APP_PLATFORM := android-22

include $(BUILD_SHARED_LIBRARY)

其中LOCAL_MODULE定義的名字就是生成的so庫名字,so庫前面都會有個lib字首,上面生產的so應該為 libHelloJni.so

5.在命令列中進入jni目錄,輸入ndk-build 即可生產對應so庫,會自動放在libs資料夾下 至此就可以執行程式了

動態註冊

動態註冊基本思想是在JNI_Onload()函式中通過JNI中提供的RegisterNatives()方法來將C/C++方法和java方法對應起來(註冊), 我們在呼叫 System.loadLibrary的時候,會在C/C++檔案中回撥一個名為 JNI_OnLoad ()的函式,在這個函式中一般是做一些初始化相關操作, 我們可以在這個方法裡面註冊函式, 註冊整體流程如下:

  1. 編寫Java端的相關native方法
  2. 編寫C/C++程式碼, 實現JNI_Onload()方法
  3. 將Java 方法和 C/C++方法通過簽名信息一一對應起來
  4. 通過JavaVM獲取JNIEnv, JNIEnv主要用於獲取Java類和呼叫一些JNI提供的方法
  5. 使用類名和對應起來的方法作為引數, 呼叫JNI提供的函式RegisterNatives()註冊方法

示例程式碼如下:

// jni標頭檔案 
#include <jni.h>
 
#include <cassert>
#include <cstdlib>
#include <iostream>
using namespace std;
 
 
//native 方法實現
jint get_random_num(){
    return rand();
}
/*需要註冊的函式列表,放在JNINativeMethod 型別的陣列中,
以後如果需要增加函式,只需在這裡新增就行了
引數:
1.java中用native關鍵字宣告的函式名
2.簽名(傳進來引數型別和返回值型別的說明) 
3.C/C++中對應函式的函式名(地址)
*/
static JNINativeMethod getMethods[] = {
        {"getRandomNum","()I",(void*)get_random_num},
};
//此函式通過呼叫RegisterNatives方法來註冊我們的函式
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;
    }
   //註冊函式 引數:java類 所要註冊的函式陣列 註冊函式的個數
    if(env->RegisterNatives(clazz,getMethods,methodsNum) < 0){
        return JNI_FALSE;
    }
    return JNI_TRUE;
}
 
static int registerNatives(JNIEnv* env){
    //指定類的路徑,通過FindClass 方法來找到對應的類
    const char* className  = "com/example/wenzhe/myjni/JniTest";
    return registerNativeMethods(env,className,getMethods, sizeof(getMethods)/ sizeof(getMethods[0]));
}
//回撥函式
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);
    //註冊函式 registerNatives ->registerNativeMethods ->env->RegisterNatives
    if(!registerNatives(env)){
        return -1;
    }
    //返回jni 的版本 
    return JNI_VERSION_1_6;
}

上面的程式碼就能實現動態註冊JNI了 以後要增加函式只需在java檔案中宣告native方法,在C/C++檔案中實現,
並在getMethods陣列新增一個元素並指明對應關係,通過ndk-build 生成so庫就可以運行了
其中JNI版本可以在jni.h標頭檔案中去檢視支援哪些版本,一般定義在檔案最後幾行

JNI 簽名

動態註冊中 JNINativeMethod 結構體中第二個引數需注意
括號內代表傳入引數的簽名符號,為空可以不寫,括號外代表返回引數的簽名符號,為空填寫 V,對應關係入下表

簽名符號 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

舉個例子:

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

總結

當熟悉動態註冊後, 動態註冊無疑是註冊函式的更好方式, 唯一要注意的是註冊函式時, 需要額外小心, 別把類名,函式名和簽名寫錯了, 不然loadLibraries時會導致應用Crash, 關於JNI部分知識, 我還會寫一篇關於如何高效傳遞資料以及JNI開發過程中的一些坑的總結.



作者:smewise
連結:https://www.jianshu.com/p/1d6ec5068d05
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授