JNI/NDK
在Android開發(fā)中豁护,由于種種原因我們需要調(diào)用C/C++代碼, 這個時候就要用到Android開發(fā)者都聽說過的JNI(Java Native Interface)了, 在調(diào)用JNI相關(guān)方法之前, 要對java中native關(guān)鍵字定義的方法進(jìn)行注冊, 注冊方式有兩種: 靜態(tài)注冊和動態(tài)注冊, 兩者優(yōu)缺點如下:
- 靜態(tài)注冊
優(yōu)點: 理解和使用方式簡單, 屬于傻瓜式操作, 使用相關(guān)工具按流程操作就行, 出錯率低
缺點: 當(dāng)需要更改類名,包名或者方法時, 需要按照之前方法重新生成頭文件, 靈活性不高 - 動態(tài)注冊
優(yōu)點: 靈活性高, 更改類名,包名或方法時, 只需對更改模塊進(jìn)行少量修改, 效率高
缺點: 對新手來說稍微有點難理解, 同時會由于搞錯簽名, 方法, 導(dǎo)致注冊失敗
靜態(tài)注冊
此注冊方法是初學(xué)者經(jīng)常用到的, 比較常見, 這里簡單說下流程,
1.編寫一個java類,在里面加載對應(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同級目錄建立文件夾酗昼,Android studio 在java同級目錄建立文件夾)
4.編寫C/C++源代碼 并把剛拷貝的頭文件包含進(jìn)去 ,復(fù)制頭文件中函數(shù)的定義部分,并實現(xiàn)其中的你想要的功能
然后編寫Android.mk Application.mk(Application.mk主要用來定義適應(yīng)的平臺综看,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庫前面都會有個lib前綴,上面生產(chǎn)的so應(yīng)該為 libHelloJni.so
5.在命令行中進(jìn)入jni目錄姻灶,輸入ndk-build 即可生產(chǎn)對應(yīng)so庫铛绰,會自動放在libs文件夾下 至此就可以運行程序了
動態(tài)注冊
動態(tài)注冊基本思想是在JNI_Onload()函數(shù)中通過JNI中提供的RegisterNatives()方法來將C/C++方法和java方法對應(yīng)起來(注冊), 我們在調(diào)用 System.loadLibrary的時候,會在C/C++文件中回調(diào)一個名為 JNI_OnLoad ()的函數(shù),在這個函數(shù)中一般是做一些初始化相關(guān)操作, 我們可以在這個方法里面注冊函數(shù), 注冊整體流程如下:
- 編寫Java端的相關(guān)native方法
- 編寫C/C++代碼, 實現(xiàn)JNI_Onload()方法
- 將Java 方法和 C/C++方法通過簽名信息一一對應(yīng)起來
- 通過JavaVM獲取JNIEnv, JNIEnv主要用于獲取Java類和調(diào)用一些JNI提供的方法
- 使用類名和對應(yīng)起來的方法作為參數(shù), 調(diào)用JNI提供的函數(shù)RegisterNatives()注冊方法
示例代碼如下:
// jni頭文件
#include <jni.h>
#include <cassert>
#include <cstdlib>
#include <iostream>
using namespace std;
//native 方法實現(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++中對應(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ù)的個數(shù)
if(env->RegisterNatives(clazz,getMethods,methodsNum) < 0){
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv* env){
//指定類的路徑,通過FindClass 方法來找到對應(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;
}
上面的代碼就能實現(xiàn)動態(tài)注冊JNI了 以后要增加函數(shù)只需在java文件中聲明native方法曾沈,在C/C++文件中實現(xiàn)这嚣,
并在getMethods數(shù)組添加一個元素并指明對應(yīng)關(guān)系,通過ndk-build 生成so庫就可以運行了
其中JNI版本可以在jni.h頭文件中去查看支持哪些版本塞俱,一般定義在文件最后幾行
JNI 簽名
動態(tài)注冊中 JNINativeMethod 結(jié)構(gòu)體中第二個參數(shù)需注意
括號內(nèi)代表傳入?yún)?shù)的簽名符號姐帚,為空可以不寫,括號外代表返回參數(shù)的簽名符號,為空填寫 V障涯,對應(yīng)關(guān)系入下表
簽名符號 | 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參數(shù)有兩個 分別是 int 和 long[] 函數(shù)返回值為 String 即函數(shù)的定義為:String getString(int a ,long[] b)
簽名就應(yīng)該是 :"(I[J)Ljava/lang/String;"(不要漏掉英文分號)
如果有內(nèi)部類 則用 $ 來分隔 如:Landroid/os/FileUtils$FileStatus;
總結(jié)
當(dāng)熟悉動態(tài)注冊后, 動態(tài)注冊無疑是注冊函數(shù)的更好方式, 唯一要注意的是注冊函數(shù)時, 需要額外小心, 別把類名,函數(shù)名和簽名寫錯了, 不然loadLibraries時會導(dǎo)致應(yīng)用Crash, 關(guān)于JNI部分知識, 我還會寫一篇關(guān)于如何高效傳遞數(shù)據(jù)以及JNI開發(fā)過程中的一些坑的總結(jié).