API實現了Java和其餘語言的通訊(主要是C&C++)。從Java1.1開始,JNI標準成爲java平臺的一部分,它容許Java代碼和其餘語言寫的代碼進行交互。JNI標準至少要保證本地代碼能工做在任何Java虛擬機環境。html
Android NDK官方原文檔:developer.android.google.cn/ndk/java
官方的Android體系架構圖android
能夠看到Android上層的Application和ApplicationFramework都是使用Java編寫,底層包括系統和使用衆多的Libraries都是C/C++編寫的,因此上層Java要調用底層的C/C++函數庫必須經過Java的JNI來實現c++
1.1 JNI 與 NDK 區別git
1.2 JNI 做用github
1.3 JNI在Android中做用: JNI能夠調用本地代碼庫(即C/C++代碼),並經過 Dalvik 虛擬機與應用層和應用框架層進行交互,Android中JNI代碼主要位於應用層和應用框架層;算法
應用層: 該層是由JNI開發,主要使用標準JNI編程模型; 應用框架層: 使用的是Android中自定義的一套JNI編程模型,該自定義的JNI編程模型彌補了標準JNI編程模型的不足;編程
Android Studio版本:3.0.1 NDK下載和CMake下載數組
在Android Studio2.2之後,AS開始支持使用Cmake編譯JNI的C++代碼,使用LLDB調試程序。在此以前編譯JNI代碼使用ndk-build編譯工具。架構
在Android Studio 3.0.1中配置jni須要在SDK Tools中下載支持JNI開發的配置,以下圖 在SDK Manager->SDK tool中下載下列四項:
在File->Project Structure中添加下載好的Ndk路徑,通常下載好了NDK後,下面有個"Select Default"的按鈕: 在local.properties的文件中加入ndk的路徑:接下來在Android studio3.0中正式開發JNI ,Android studio已經支持建立C/C++的開發,並使用CMake的模式構建NDK開發。
C++ Standard:
使用下拉列表選擇您但願使用哪一種 C++ 標準。選擇 Toolchain Default 會使用默認的 CMake 設置;Exceptions Support:
若是您但願啓用對 C++ 異常處理的支持,請選中此複選框。若是啓用此複選框,Android Studio 會將 -fexceptions 標誌添加到模塊級 build.gradle 文件的 cppFlags 中,Gradle 會將其傳遞到 CMake;Runtime Type Information Support:
若是您但願支持 RTTI,請選中此複選框。若是啓用此複選框,Android Studio 會將 -frtti 標誌添加到模塊級 build.gradle 文件的 cppFlags 中,Gradle 會將其傳遞到 CMake。點擊Finish等待項目建立完成。
和之前惟一不一樣的就是多出了cpp目錄以及External Build Files兩個目錄,那麼這兩個都有什麼用呢?
cpp 目錄
存放全部 native code 的地方,包括源碼,頭文件,預編譯項目等。對於新項目,Android Studio 建立了一個 C++ 模板文件:native-lib.cpp,而且將該文件放到了你的 app 模塊的 src/main/cpp/ 目錄下。這份模板代碼提供了一個簡答的 C++ 函數:stringFromJNI(),該函數返回一個字符串:」Hello from C++」External Build Files 目錄
存放 CMake 或 ndk-build 構建腳本的地方。有點相似於 build.gradle 文件告訴 Gradle 如何編譯你的APP 同樣,CMake 和 ndk-build 也須要一個腳原本告知如何編譯你的 native library。對於一個新的項目,Android Studio 建立了一個 CMake腳本:CMakeLists.txt,而且將其放到了你的 module 的根目錄下native-lib.cpp
文件內容:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL Java_com_jni_demo_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
複製代碼
簡單能夠看到,首先定義hello變量,以後return回該字符
MainActivity.java
文件的內容
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
//應用啓動時加載
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */
//native方法
public native String stringFromJNI();
}
複製代碼
Android Studio2.2起開始使用Cmake編譯C++代碼,固然也兼容以前的ndk-build的方式。
配置Cmake 查看官網文檔:developer.android.com/studio/proj…
CMakeLists.txt
,簡單的翻譯下
# 有關使用CMake在Android Studio的更多信息,請閱讀文檔:https://d.android.com/studio/projects/add-native-code.html
# 設置CMake的最低版本構建本機所需庫
cmake_minimum_required(VERSION 3.4.1)
# 建立並命名庫,將其設置爲靜態的
# 或共享,並提供其源代碼的相對路徑。
# 你能夠定義多個library庫,並使用CMake來構建。
# Gradle會自動將包共享庫關聯到你的apk程序。
add_library( # 設置庫的名稱
native-lib
# 將庫設置爲共享庫。
SHARED
# 爲源文件提供一個相對路徑。
src/main/cpp/native-lib.cpp )
# 搜索指定預先構建的庫和存儲路徑變量。由於CMake包括系統庫搜索路徑中默認狀況下,只須要指定想添加公共NDK庫的名稱,在CMake驗證庫以前存在完成構建
find_library( # 設置path變量的名稱
log-lib
# 在CMake定位前指定的NDK庫名稱
log )
# 指定庫CMake應該連接到目標庫中,能夠連接多個庫,好比定義庫,構建腳本,預先構建的第三方庫或者系統庫
target_link_libraries( # 指定目標庫
native-lib
# 目標庫到日誌庫的連接 包含在NDK
${log-lib} )
複製代碼
build.gradle
文件
能夠看出,AS幫咱們配置cmake時自動幫咱們添加了,上述兩塊代碼。可是在咱們本身配置cmake工具時,須要本身手動填寫,拷貝。
咱們Build編譯一下,在編譯輸出文件夾 能夠看到:
。編譯到運行示例 APP 的流程過程:
libnative-lib.so
,而後 Gradle 將其打包到 APK 中;注意:Instant Run 並不兼容使用了 native code 的項目。Android Studio 會自動禁止 Instant Run 功能。
.so庫在apk裏面:
在原項目基礎上增長本身的代碼
//在MainActivity.java中增長一個native方法
public native String getHelloJni();
複製代碼
你會發現getHelloJni( )
方法是紅色的。不要急,按住Alt+Enter回車後,系統會自動爲你在以前.cpp文件中建立一個getHelloJni( )的C++代碼,是否是很智能……
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL Java_com_jni_demo_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL Java_com_jni_demo_MainActivity_getHelloJni(JNIEnv *env, jobject instance) {
// TODO
return env->NewStringUTF(returnValue);
}
複製代碼
在原項目基礎上修改庫的名字
package com.baidu.jni.demo;
/** * <p/> * 功能 : * <p/> * <p> * <p>Copyright baidu.com 2018 All right reserved</p> * * @author tuke 時間 2018/6/1 * @email tuke@baidu.com * <p> * 最後修改人 無 */
public class JniTest {
static {
System.loadLibrary("sayhello");
}
public native String getHello(String string, int[] ints);
public native String getSayBaibai(int a ,float b,boolean c);
}
複製代碼
而後build 項目,在build文件夾找到是否生成JniTest.class字節碼文件.只有生成了.class字節碼文件才能下一步
第二步:須要使用javah 命令生成.h頭文件。
cd app/build/intermediates/classes/debug
複製代碼
而後經過命令行:
javah -jni com.baidu.jni.demo.JniTest
複製代碼
JDK 10之後移除了javah命令,JDK十、JDK十一、JDK12新特性,使用javac -h . xxxx.java 代替
其中com.baidu.jni.demo.是包名,JniTest是java代碼。 而後會在當前目錄下生成:com_baidu_jni_demo_JniTest.h
頭文件 而後在src->main 新建jni文件夾,新建xxx.cpp文件,而且把剛纔生成的com_baidu_jni_demo_JniTest.h頭文件,拷貝到jni文件夾,編寫jnitest.c以下:
//
// Created by Tu,Ke on 2018/6/1.
//
#include "com_baidu_jni_demo_JniTest.h"
//extern "C"
JNIEXPORT jstring JNICALL Java_com_baidu_jni_demo_JniTest_getHello (JNIEnv * env, jclass, jstring, jintArray) {
return env->NewStringUTF("helloworld");
}
//extern "C"
JNIEXPORT jstring JNICALL Java_com_baidu_jni_demo_JniTest_getSayBaibai(JNIEnv *env, jobject instance, jint a, jfloat b, jboolean c) {
// TODO
return env->NewStringUTF("helloworld");
}
複製代碼
在"Program"是JDK的javah命令的路徑,個人Mac結尾沒有.exe,有的資料是javah.exe多是Windows這樣配置。
在JniTest.java文件右鍵,選擇External Tool ->javah -jni 而後會自動在src->main下新建一個jni文件夾,而且自動生成com_baidu_jni_demo_JniTest.h頭文件,和第一種方法如出一轍。
接着新建C文件,include進來就OK。
在JniTest.java文件中新建一個native方法,開始是紅色的,使用上面的External Tool->javah -jni 生成以後以下圖:
此時.h頭文件已經被更新:
而後在C文件裏繼續實現就好。
添加CmakeLists.txt文件,在app的目錄下:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
sayhello
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/jni/hello.cpp )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
sayhello
# Links the target library to the log library
# included in the NDK.
${log-lib} )
複製代碼
build.gradle添加,下面兩項:
這樣整個JNI過程就完成了,下面就是靠本身學習編寫JNI代碼實現so邏輯了。
#ifdef HAVE_INTTYPES_H
# include <inttypes.h> /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
/* "cardinal indices and sizes" */
typedef jint jsize;
#ifdef __cplusplus
/*
* Reference types, in C++
*/
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
//……
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
//……
#else /* not __cplusplus */
/*
* Reference types, in C.
*/
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
//……
#endif
複製代碼
當是C++環境時,jobject, jclass, jstring, jarray
等都是繼承自_jobject類,而在 C 語言環境是,則它的本質都是空類型指針typedef void* jobject
;
下圖是Java基本數據類型和本地類型的映射關係,這些基本數據類型都是能夠直接在 Native 層直接使用的:
須要注意的是,
1)引用類型不能直接在 Native 層使用,須要根據 JNI 函數進行類型的轉化後,才能使用; 2)多維數組(含二維數組)都是引用類型,須要使用 jobjectArray 類型存取其值;
一樣不能直接在 Native 層使用。當 Native 層須要調用 Java 的某個方法時,須要經過 JNI 函數獲取它的 ID,根據 ID 調用 JNI 函數獲取該方法;變量的獲取也是相似。ID 的結構體以下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID* jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */
複製代碼
基本類型描述符 下面是基本的數據類型的描述符,除了 boolean 和 long 類型分別是 Z 和 J 外,其餘的描述符對應的都是Java類型名的大寫首字母。另外,void 的描述符爲 V
引用類型描述符 通常引用類型描述符的規則以下,注意不要丟掉「;」:L + 類描述符 + ;
如,String 類型的域描述符爲:Ljava/lang/String;
數組的域描述符特殊一點,以下,其中有多少級數組就有多少個「[」,數組的類型爲類時,則有分號,爲基本類型時沒有分號 [ + 其類型的域描述符
例如:
int[] 描述符爲 [I
double[] 描述符爲 [D
String[] 描述符爲 [Ljava/lang/String;
Object[] 描述符爲 [Ljava/lang/Object;
int[][] 描述符爲 [[I
double[][] 描述符爲 [[D
複製代碼
對應在 jni.h 獲取 Java 的字段的 native 函數以下,name爲 Java 的字段名字,sig 爲域描述符
//C
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
//C++
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) { return functions->GetFieldID(this, clazz, name, sig); }
jobject GetObjectField(jobject obj, jfieldID fieldID) { return functions->GetObjectField(this, obj, fieldID); }
複製代碼
類描述符是類的完整名稱:包名+類名,java 中包名用 . 分割,jni 中改成用 / 分割 如,Java 中 java.lang.String 類的描述符爲 java/lang/String native 層獲取 Java 的類對象,須要經過 FindClass() 函數獲取
, jni.h 的函數定義以下:
//C
jclass (*FindClass)(JNIEnv*, const char*);
//C++
jclass FindClass(const char* name) { return functions->FindClass(this, name); }
複製代碼
name 就是類的引用類型描述符,如 Java 對象 cn.cfanr.jni.JniTest,對應字符串爲Lcn/cfanr/jni/JniTest; 以下:
jclass jclazz = env->FindClass("Lcn/cfanr/jni/JniTest;");
複製代碼
方法描述符須要將全部參數類型的域描述符按照聲明順序放入括號
,而後再加上返回值類型的域描述符
,其中沒有參數時,不須要括號,以下規則:
(參數……)返回類型
複製代碼
例如:
Java 層方法 ——> JNI 函數簽名
String getString() ——> Ljava/lang/String;
int sum(int a, int b) ——> (II)I void main(String[] args) ——> ([Ljava/lang/String;)V 複製代碼
另外,對應在 jni.h 獲取 Java 方法的 native 函數
以下,其中 jclass 是獲取到的類對象,name 是 Java 對應的方法名字,sig 就是上面說的方法描述符: 全部參數就是 類的對象,函數名,方法描述符(其實就包含參數列表和返回值類型了)
//C
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//C++
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) { return functions->GetMethodID(this, clazz, name, sig); }
複製代碼
不過在實際編程中,若是使用 javah 工具來生成對應的 native 代碼,就不須要手動編寫對應的類型轉換了。
JNIEnv 是 jni.h 文件最重要的部分,它的本質是指向函數表指針的指針(JavaVM也是),函數表裏面定義了不少 JNI 函數,同時它也是區分 C 和 C++環境的(由上面介紹描述符時也能夠看到),在 C 語言環境中,JNIEnv 是strut JNINativeInterface*的指針別名。
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; //C++中的 JNIEnv 類型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; //C語言的 JNIEnv 類型
typedef const struct JNIInvokeInterface* JavaVM;
#endif
複製代碼
JNIEnv的特色
C++的 JNIEnv
由typedef _JNIEnv JNIEnv;
可知,C++的 JNIEnv 是 _JNIEnv 結構體,而 _JNIEnv 結構體定義了 JNINativeInterface 的結構體指針,內部定義的函數其實是調用 JNINativeInterface 的函數,因此C++的 env 是一級指針,調用時不須要加 env 做爲函數的參數,例如:env->NewStringUTF(env, "hello")
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); }
jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
jmethodID FromReflectedMethod(jobject method)
{ return functions->FromReflectedMethod(this, method); }
jfieldID FromReflectedField(jobject field)
{ return functions->FromReflectedField(this, field); }
jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic)
{ return functions->ToReflectedMethod(this, cls, methodID, isStatic); }
jclass GetSuperclass(jclass clazz)
{ return functions->GetSuperclass(this, clazz); }
//……
}
複製代碼
Java 的 native 方法是如何連接 C/C++中的函數的呢?能夠經過靜態和動態的方式註冊JNI。
靜態註冊的方式有兩個重要的關鍵詞 JNIEXPORT
和 JNICALL
,這兩個關鍵詞是宏定義
,主要是註明該函數式 JNI 函數,當虛擬機加載 so 庫時,若是發現函數含有這兩個宏定義時,就會連接到對應的 Java 層的 native 方法
。
由前面生成頭文件的方法,從新建立一個cn.cfanr.test_jni.Jni_Test.java的類
public class Jni_Test {
private static native int swap();
private static native void swap(int a, int b);
private static native void swap(String a, String b);
private native void swap(int[] arr, int a, int b);
private static native void swap_0(int a, int b);
}
複製代碼
用 javah 工具生成如下頭文件:
#include <jni.h>
/* Header for class cn_cfanr_test_jni_Jni_Test */
#ifndef _Included_cn_cfanr_test_jni_Jni_Test
#define _Included_cn_cfanr_test_jni_Jni_Test
#ifdef __cplusplus
extern "C" {
#endif
/* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: ()I */
JNIEXPORT jint JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__ (JNIEnv *, jclass); // 凡是重載的方法,方法後面都會多一個下劃線
/* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: (II)V */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__II (JNIEnv *, jclass, jint, jint);
/* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: (Ljava/lang/String;Ljava/lang/String;)V */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap__Ljava_lang_String_2Ljava_lang_String_2 (JNIEnv *, jclass, jstring, jstring);
/* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap * Signature: ([III)V */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap___3III (JNIEnv *, jobject, jintArray, jint, jint); // 非 static 的爲 jobject
/* * Class: cn_cfanr_test_jni_Jni_Test * Method: swap_0 * Signature: (II)V */
JNIEXPORT void JNICALL Java_cn_cfanr_test_1jni_Jni_1Test_swap_10 (JNIEnv *, jclass, jint, jint); // 不知道爲何後面沒有 II
#ifdef __cplusplus
}
#endif
#endif
複製代碼
能夠看出 JNI 的調用函數的定義是按照必定規則命名的: JNIEXPORT 返回值 JNICALL Java_全路徑類名_方法名_參數簽名(JNIEnv* , jclass, 其它參數);
其中 Java_ 是爲了標識該函數來源於Java。 經檢驗(不必定正確),若是是重載的方法,則有「參數簽名」,不然沒有
;另外若是使用的是 C++,在函數前面加上 extern 「C」(表示按照 C 的方式編譯),函數命名後面就不須要加上「參數簽名」。
另外還須要注意幾點特殊規則:
_
,在c++裏要用_1
鏈接;__
鏈接;參數簽名
的斜槓 「/」
改成下劃線 _
鏈接,分號 ;
改成 _2
鏈接,左方括號 [
改成 _3
鏈接;static 的爲 jclass
,非 static 的 爲 jobject
;JNI 函數中是沒有修飾符的。優勢: 實現比較簡單,能夠經過 javah 工具將 Java代碼的 native 方法直接轉化爲對應的native層代碼的函數; 缺點:
動態註冊 原理:直接告訴 native 方法其在JNI 中對應函數的指針
。經過使用 JNINativeMethod
結構來保存 Java native 方法和 JNI 函數關聯關係
,步驟:
先編寫 Java 的 native 方法;
編寫 JNI 函數的實現(函數名能夠隨便命名
);
利用結構體 JNINativeMethod 保存Java native方法和 JNI函數的對應關係
;
利用registerNatives(JNIEnv* env)
註冊類的全部本地方法;
在 JNI_OnLoad
方法中調用註冊方法;
在Java中經過System.loadLibrary加載完JNI動態庫以後,會調用JNI_OnLoad函數,完成動態註冊
;
jni.h中的
//JNINativeMethod結構體
typedef struct {
const char* name; //Java中native方法的名字
const char* signature; //Java中native方法的描述符
void* fnPtr; //對應JNI函數的指針
} JNINativeMethod;
/** * @param clazz java類名,經過 FindClass 獲取 * @param methods JNINativeMethod 結構體指針 * @param nMethods 方法個數 */
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) //JNI_OnLoad JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
複製代碼
參考連接: