国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

JNI學習筆記

2019-11-08 00:21:19
字體:
來源:轉載
供稿:網友

1為什么使用JNI?

JNI 的強大特性使我們在使用 java 平臺的同時,還可以重用原來的本地代碼。作為虛擬機 實現的一部分,JNI 允許 JAVA 和本地代碼間的雙向交互。 請記住,一旦使用 JNI,JAVA 程序就喪失了 JAVA 平臺的兩個優點: 1、 程序不再跨平臺。要想跨平臺,必須在不同的系統環境下重新編譯本地語言部分。 2、 程序不再是絕對安全的,本地代碼的不當使用可能導致整個程序崩潰。 一個通用規則是,你應該讓本地方法集中在少數幾個類當中。這樣就降低了 JAVA 和 C 之間的耦合性。

當你開始著手準備一個使用 JNI 的項目時,請確認是否還有替代方案。像上一節所提到的, 應用程序使用 JNI 會帶來一些副作用。下面給出幾個方案,可以避免使用 JNI 的時候,達到 與本地代碼進行交互的效果: 1、JAVA 程序和本地程序使用 TCP/ip 或者 IPC 進行交互。 2、 當用 JAVA 程序連接本地數據庫時,使用 JDBC 提供的 API。 3、JAVA 程序可以使用分布式對象技術,如 JAVAIDLAPI。

這些方案的共同點是,JAVA 和 C 處于不同的線程,或者不同的機器上。這樣,當本地程序 崩潰時,不會影響到 JAVA 程序。 下面這些場合中,同一進程內 JNI 的使用無法避免: 1、 程序當中用到了 JAVA API 不提供的特殊系統環境才會有的特征。而跨進程操作又不現 實。 2、 你可能想訪問一些己有的本地庫,但又不想付出跨進程調用時的代價,如效率,內存, 數據傳遞方面。 3、JAVA 程序當中的一部分代碼對效率要求非常高,如算法計算,圖形渲染等。 總之,只有當你必須在同一進程中調用本地代碼時,再使用 JNI。 Android應用框架層JNI部分源碼主要位于frameworks/base/目錄下。按照模塊組織,不同的模塊將被編譯為不同的共享庫,分別為上層提供不同的服務。這些共享庫最終會被放置在目標系統的/system/lib目錄下。

注意:NDK與JNI的區別: NDK是為便于開發基于JNI的應用而提供的一套開發和編譯工具集;而JNI則是一套編程接口,可以運用在應用層,也可以運用在應用框架層,以實現Java代碼與本地代碼的互操作。

2.JNI步驟

JNI編程模型的結構十分清晰,可以概括為以下三個步驟:

步驟1 Java層聲明Native方法。

步驟2 JNI層實現Java層聲明的Native方法,在JNI層可以調用底層庫或者回調Java層方法。這部分將被編譯為動態庫(SO文件)供系統加載。

步驟3 加載JNI層代碼編譯后生成的共享庫。

如何創建一個支持JNI的項目:https://developer.android.com/studio/PRojects/add-native-code.html

創建后的目錄如下: 這里寫圖片描述

3.CMake

一款外部構建工具,可與 Gradle 搭配使用來構建原生庫。簡單來說用來將.cpp文件或.c等文件編譯生成.so文件的工具,其配置文件就是上述目錄圖中的CMakeLists.txt。以前用的是ndk-build,但是已經棄用,其配置文件是Android.mk。

CMakeLists.txt的基本配置: 1. cmake_minimum_required(參數):設置cmake的版本以決定你將使用到cmake的feature。 2. add_library(so_file_name [STATIC | SHARED | MODULE] sources):第一個參數是創建的so文件的名字,第二個參數是配置so文件的用途,第三個參數是該so文件包含的c/c++源代碼文件。舉個例子:

add_library(native_lib SHARED src/main/cpp/test.cpp src/main/cpp/test2.cpp)

這個配置的意思是,CMake會創建一個名字叫libnative_lib.so文件,so的命名 = lib + 名字 + .so。但是在java層調用System.loadLibrary的時候,還是傳入第一個參數即可,在這個例子中只用傳入”native_lib”第二個參數的意思是代表該so文件的類型,static代表靜態庫,shared代表動態庫,module在使用dyid的系統有效,若不支持dyid,等同于shared。 后面的參數都代表加入到so文件的c/c++源代碼,例如這個例子中test.cpp和test2.cpp都會編譯到libnative_lib.so這個文件中,依據需求加入你需要的源代碼。 3.find_library:定位NDK的某個庫,并將其路徑存在某個變量,供其他部分引用。 其他CMake Commands內容,點擊這里

4.javah, javap

在java層聲明好native方法之后,按照一般的習慣是要生成對應的jni層方法,網上最一般的方法也是通過javah來生成。 1. 在jdk1.6及以下,使用對應java文件生成的class文件來生成.h文件 進入到對應的/build/intermediates/classes/debug目錄下,打開命令行輸入以下命令:

javah -jni com.netesae.jnisample.Prompt

后面一定要輸入類的全名,包括包名。然后就會生成.h文件了。 2. 但是在jdk1.7及以上,可以直接使用java文件生成.h文件。 進入/src/main/java目錄下,打開命令行,敲入和上面一樣的命令,就可以了。

兩種方式生成的.h文件名很長,當然你可以改。 在main/下創建cpp文件夾,將該.h文件加入,并創建新的cpp文件或者c文件,include .h文件,實現.h文件的方法即可,這一部分是c++/c的用法就不解釋了。

當然如果你是采用Android官網上的方法創建一個支持c++的項目,它會自動幫你生成一個jni的模板,并且發現他其實只有一個cpp文件,并不需要什么.h文件,當然這也是可以的。 那為什么還要javah呢?這是因為jni方法規范,java層的方法要對應的native層的方法,為了保證每個函數的唯一性,所以jni層的方法命名比較長,規則如下:

Java_包名_函數名字

并且包名之間的.號也要用_來代替。因為名字比較長,為了防止程序員寫錯而導致找不到對應的方法,就用javah。

那么javap是干嘛的,就是用來生成函數簽名的,那什么是函數簽名呢,后面再解釋,現在先看命令。看例子:

public class Prompt { static { System.loadLibrary("prompt-lib"); } native String getLine(String prompt); native String show();}

進入到Prompt.java所在的目錄,敲入以下命令:

D:/git/JNISample/app/src/main/java/com/example/jnisample>javap -s -p -classpath . Prompt警告: 二進制文件Prompt包含com.example.jnisample.PromptCompiled from "Prompt.java"public class com.example.jnisample.Prompt { public com.example.jnisample.Prompt(); descriptor: ()V native java.lang.String getLine(java.lang.String); descriptor: (Ljava/lang/String;)Ljava/lang/String; native java.lang.String show(); descriptor: ()Ljava/lang/String; static {}; descriptor: ()V}

看prompt中的getLine函數,他的參數是String,返回的是String,所以他的函數簽名就是(Ljava/lang/String;)Ljava/lang/String;,括號中的是參數簽名,括號外面的就是方法返回值簽名。這個后面會用到,先記著吧。

4.JNIEnv在c和c++中的區別

一開始看些資料的話,大家可能會有些疑惑,例如我們要調用JNIEnv的同一個函數,會看到有以下兩個版本:

(*env)->FindClass(env,"com/example/jnisample/Prompt");env->FindClass("com/example/jnisample/Prompt");

那這兩個有什么區別么?區別就是一個是c++用法,一個是c中的用法,首先先讓我們看下JNIEnv是啥(其實現是在jni.h中)。

#if defined(__cplusplus)typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv;typedef const struct JNIInvokeInterface* JavaVM;#endif

這段話的意思是在c++中,定義_JNIEnv是JNIEnv,其他情況(c)下,定義const struct JNINativeInterface*是JNIEnv。那么_JNIEnv和JNINativeInterface又是什么呢?

struct JNINativeInterface { ......很多方法 jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,};struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions;#if defined(__cplusplus) jint GetVersion() { return functions->GetVersion(this); }.....其他方法};

可以看到JNINativeInterface 其實定義了很多方法,都是對Java的數據進行操作,而_JNIEnv則封裝了一個JNINativeInterface的指針,并且聲明與JNINativeInterface中一模一樣的方法,并且都是通過JNINativeInterface的指針來調方法,其實就是對JNINativeInterface做了一層封裝,那么為什么這么做呢? 我的猜想是c++是面向對象的語言,不用在用指針方式來調用,并且_JNIEnv中的每個方法都比JNINativeInterface少一個參數,就是JNIEnv。具體可以自己看jni.h中的實現。

5.extern “C” vs JNIExport JNICall

.cpp文件是c++的語法,.c是c的語法,文件的類型決定了JNIEnv的語法,在上面一小節也提到JNIEnv在c++和c的區別。 網上的資料中,native方法除了要遵守JNI函數規范,還要加上JNIExport和JNICall,這樣才能保證這個native函數是可以注冊在函數列表中,但我后來試了下,在使用cmake的情況下,并不需要JNIExport和JNICall。 1.c語言情況下,并不需要JNIExport和JNICall。 2.c++語言情況下,也不需要JNIExport和JNICall,但是需要加上extern “C”{},native函數需要放在這個括號中才可以。

解釋下extern “C”的意思。extern代表聲明的方法和變量為全局變量,和java的static一樣,但是和c++的static不一樣(有關c++語法自行查找)。”c”則代表{}內的內容以c語言方式編譯和連接。 至于c語言下為什么不用JNIExport和JNICall還不是很清楚,尚未找到原因。但猜想可能是在cmake編譯so文件的時候,做了什么手腳。

6.框架層vs應用層

以下內容資料來自JNI在Android系統中所處的位置,可自行往下閱讀。

應用框架層:Android定義了一套JNI編程模型,使用函數注冊方式彌補了標準JNI編程模型的不足。Android應用框架層JNI部分源碼主要位于frameworks/base/目錄下。按照模塊組織,不同的模塊將被編譯為不同的共享庫,分別為上層提供不同的服務。這些共享庫最終會被放置在目標系統的/system/lib目錄下。 在Android應用程序開發中,一般是調用應用框架層的android.util.Log.java提供的Java接口來使用日志系統。比如我們會寫如下代碼輸出日志: Log.d(TAG,"debug log"); 這個Java接口其實是通過JNI調用系統運行庫(即本地庫)并最終調用內核驅動程序Logger把Log寫到內核空間中的。在Android中, Log系統十分通用,而且其JNI結構非常簡潔,很適合作為JNI入門的例子。所涉及的文件包括: frameworks/base/core/jni/android_util_Log.cpp(JNI層實現代碼)

frameworks/base/core/java/android/util/Log.java(Java層代碼)

libnativehelper/include/nativehelper/jni.h(JNI規范的頭文件)

libnativehelper/include/nativehelper/JNIHelp.h

libnativehelper/JNIHelp.cpp

frameworks/base/core/jni/AndroidRuntime.cpp

package android.util; public final class Log { …… public static int d(String tag, String msg) { //使用Native方法打印日志。LOG_ID_MAIN表示日志ID,有4種:main、radio、events、system return println_native(LOG_ID_MAIN, DEBUG, tag, msg); } …… //聲明Native方法isLoggable public static native boolean isLoggable(String tag, int level); …… /** @hide */ public static final int LOG_ID_MAIN = 0; /** @hide */ public static final int LOG_ID_RADIO = 1; /** @hide */ public static final int LOG_ID_EVENTS = 2; /** @hide */ public static final int LOG_ID_SYSTEM = 3; //聲明Native方法println_native /** @hide */ public static native int println_native(int bufID, int priority, String tag, String msg); }

native的實現:

#include "jni.h" //符合JNI規范的頭文件,必須包含進來 #include "JNIHelp.h" //Android為更好地支持JNI提供的頭文件 #include "utils/misc.h" #include "android_runtime/AndroidRuntime.h" /*這里便是Java層聲明的isLoggable方法的實現代碼。 *JNI方法增加了JNIEnv和jobject兩個參數,其余參數和返回值只是將Java參數映射成JNI *的數據類型,然后通過調用本地庫和JNIEnv提供的JNI函數處理數據,最后返回給Java層*/ static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level) { …… //這里調用了JNI函數 const char* chars = env->GetStringUTFChars(tag, NULL); jboolean result = false; if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) { …… } else { //這里調用了本地庫函數 result = isLoggable(chars, level); } env->ReleaseStringUTFChars(tag, chars);//調用JNI函數 return result; } //以下是Java層聲明的println_Native方法的實現代碼 static jint android_util_Log_println_native(JNIEnv* env, jobject clazz, jint bufID, jint priority, jstring tagObj, jstring msgObj) { const char* tag = NULL; const char* msg = NULL; ……//省略異常處理代碼 if (tagObj != NULL) tag = env->GetStringUTFChars(tagObj, NULL);//調用JNI函數 msg = env->GetStringUTFChars(msgObj, NULL); //調用本地庫提供的方法 int res = __android_log_buf_write(bufID,(android_LogPriority)priority, tag, msg); if (tag != NULL) env->ReleaseStringUTFChars(tagObj, tag);//調用JNI函數釋放資源 env->ReleaseStringUTFChars(msgObj, msg);//調用JNI函數釋放資源 return res;

JNI層已經實現了Java層聲明的Native方法。可這兩個方法又是如何聯系在一起的呢?我們接著分析android_util_Log.cpp的源碼。定位到以下部分:

static JNINativeMethod gMethods[] = { { "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable }, { "println_native", "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native }, };

這里定義了一個數組gMethods,用來存儲JNINativeMethod類型的數據。

可以在jni.h文件中找到JNINativeMethod的定義:

typedef struct { const char* name; //Java層聲明的Native函數的函數名 const char* signature; //Java函數的簽名,依據JNI的簽名規則 void* fnPtr; //函數指針,指向JNI層的實現方法 } JNINativeMethod;

可見,JNINativeMethod是一個結構體類型,保存了聲明函數和實現函數的一一對應關系。

下面分析gMethods[0]中存儲的對應信息:

{ "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable } Java層聲明的Native函數名為isLoggable。 Java層聲明的Native函數的簽名為(Ljava/lang/String;I)Z。 JNI層實現方法的指針為(void*) android_util_Log_isLoggable。

這里就可以用到剛剛說到的javap工具,用來生成函數簽名。 至此,我們給出了Java層方法和JNI層方法的對應關系。可如何告訴虛擬機這種對應關系呢?

繼續分析android_util_Log.cpp源碼。定位到以下部分:

int register_android_util_Log(JNIEnv* env) { jclass clazz = env->FindClass("android/util/Log"); levels.debug = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "DEBUG", "I")); …… return AndroidRuntime::registerNativeMethods(env, "android/util/Log", gMethods, NELEM(gMethods));

具體細看AndroidRuntime::registerNativeMethods,發現最終調用的是JNIEnv的RegisterNatives方法,其作用是向clazz參數指定的類注冊本地方法。這樣,虛擬機就得到了Java層和JNI層之間的對應關系,就可以實現Java和C/C++代碼的互操作了。 register_android_util_Log函數是在哪里調用的? 這個問題涉及JNI部分代碼在系統啟動過程中是如何加載的,這已經超出了本章的知識范圍,我們將在啟動篇詳細介紹這個過程。在這里,讀者只需要知道這個函數是在系統啟動過程中通過AndroidRuntime.cpp的register_jni_procs方法執行的,進而調用到register_android_util_Log將這種函數映射關系注冊給Dalvik虛擬機的。 注意 使用JNI有兩種方式:一種是遵守JNI規范的函數命名規范,建立聲明函數和實現函數之間的對應關系;另一種是就是Log系統中采用的函數注冊方式。應用層多采用第一種方式,應用框架層多采用第二種方式。

那么應用層可以使用上述函數注冊方式來么,不用遵守JNI函數規范?答案是可以的。 看以下的例子:

package com.example.jnisample;public class Prompt { static { System.loadLibrary("prompt-lib"); } native String getLine(String prompt);}#include <jni.h>#include <stdio.h>jstringPrompt_getLine(JNIEnv *env, jobject thiz, jstring params) { return params;}static JNINativeMethod gMethods[] = { {"getLine", "(Ljava/lang/String;)Ljava/lang/String;", (void *) Prompt_getLine},};/*虛擬機執行System.loadLibrary("native-lib")后,進入libnative-lib.so后 *會首先執行這個方法,所以我們在這里做注冊的動作*/ jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jint result = -1; if (vm->GetEnv((void **) &env, JNI_VERSION_1_4)) { return result; } jclass clazz = env->FindClass("com/example/jnisample/Prompt"); if (clazz == NULL) { return result; } if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) >= 0) { result = JNI_VERSION_1_4; } return result;}

上述例子是仿自android_media_MediaPlayer.cpp。

6.獲取當前線程的JNIEnv

不論進程中有多少個線程,JavaVM只有一份,所以在任何地方都可以使用它。可以通過調用JavaVM的attachCurrentThread來得到這個線程的JNIEnv,注意要調用detachCurrentThread來釋放對應的資源。

參考資料: http://book.51cto.com/art/201305/395846.htm http://androidxref.com/4.2_r1/xref/frameworks/base/media/jni/ https://developer.android.com/studio/projects/add-native-code.html https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 霍邱县| 滁州市| 姚安县| 仁怀市| 乐亭县| 旬邑县| 金华市| 柳林县| 镇赉县| 许昌市| 个旧市| 呼伦贝尔市| 临湘市| 大英县| 日照市| 固原市| 景洪市| 蓬莱市| 正镶白旗| 灵寿县| 乐都县| 乌兰县| 绵竹市| 涟水县| 平安县| 东城区| 诸暨市| 濉溪县| 来凤县| 通城县| 谷城县| 麻栗坡县| 综艺| 阿瓦提县| 金溪县| 和顺县| 若尔盖县| 邢台市| 肃宁县| 河北省| 高淳县|