JNI 的強大特性使咱們在使用 JAVA 平臺的同一時候,還可以重用原來的本地代碼。做爲虛擬機 實現的一部分,JNI 贊成 JAVA 和本地代碼間的雙向交互。
請記住,一旦使用 JNI,JAVA 程序就喪失了 JAVA 平臺的兩個長處:
一、 程序再也不跨平臺。要想跨平臺。必須在不一樣的系統環境下又一次編譯本地語言部分。
二、 程序再也不是絕對安全的,本地代碼的不當使用可能致使整個程序崩潰。javascript
一個通用規則是,你應該讓本地方法集中在少數幾個類其中。html
這樣就減小了 JAVA 和 C 之間的耦合性。java
當你開始着手準備一個使用 JNI 的項目時,請確認是否還有替代方案。android
像上一節所提到的, 應用程序使用 JNI 會帶來一些反作用。下面給出幾個方案,可以避免使用 JNI 的時候,達到 與本地代碼進行交互的效果:
一、JAVA 程序和本地程序使用 TCP/IP 或者 IPC 進行交互。
二、 當用 JAVA 程序鏈接本地數據庫時,使用 JDBC 提供的 API。
三、JAVA 程序可以使用分佈式對象技術,如 JAVAIDLAPI。c++
這些方案的共同點是,JAVA 和 C 處於不一樣的線程。或者不一樣的機器上。這樣,當本地程序 崩潰時,不會影響到 JAVA 程序。 下面這些場合中,同一進程內 JNI 的使用沒法避免:
一、 程序其中用到了 JAVA API 不提供的特殊系統環境纔會有的特徵。而跨進程操做又不現 實。git
二、 你可能想訪問一些己有的本地庫,但又不想付出跨進程調用時的代價,如效率,內存, 數據傳遞方面。
三、JAVA 程序其中的一部分代碼對效率要求很是高。如算法計算。圖形渲染等。
總之,僅僅有當你必須在同一進程中調用本地代碼時,再使用 JNI。
Android應用框架層JNI部分源代碼主要位於frameworks/base/文件夾下。依照模塊組織,不一樣的模塊將被編譯爲不一樣的共享庫。分別爲上層提供不一樣的服務。這些共享庫終於會被放置在目標系統的/system/lib文件夾下。算法
注意:NDK與JNI的差異: NDK是爲便於開發基於JNI的應用而提供的一套開發和編譯工具集;而JNI則是一套編程接口,可以運用在應用層,也可以運用在應用框架層,以實現Java代碼與本地代碼的互操做。數據庫
JNI編程模型的結構十分清晰,可以歸納爲下面三個步驟:編程
步驟1 Java層聲明Native方法。數組
步驟2 JNI層實現Java層聲明的Native方法,在JNI層可以調用底層庫或者回調Java層方法。這部分將被編譯爲動態庫(SO文件)供系統載入。
步驟3 載入JNI層代碼編譯後生成的共享庫。
怎樣建立一個支持JNI的項目:https://developer.android.com/studio/projects/add-native-code.html
建立後的文件夾例如如下:
一款外部構建工具。可與 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內容。點擊這裏
在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.Prompt
Compiled 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;,括號裏的是參數簽名,括號外面的就是方法返回值簽名。這個後面會用到,先記着吧。
一開始看些資料的話。你們可能會有些疑惑,好比咱們要調用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;
#else
typedef 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中的實現。
.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文件的時候。作了什麼手腳。
下面內容資料來自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>
jstring
Prompt_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。
不論進程中有多少個線程,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