Android進階3:Android的NDK開發-JNI基礎

1.JNI-Java Native Interface的縮寫

API實現了Java和其餘語言的通訊(主要是C&C++)。從Java1.1開始,JNI標準成爲java平臺的一部分,它容許Java代碼和其餘語言寫的代碼進行交互。JNI標準至少要保證本地代碼能工做在任何Java虛擬機環境。html

Android NDK官方原文檔:developer.android.google.cn/ndk/java

官方的Android體系架構圖android

android 架構圖

能夠看到Android上層的Application和ApplicationFramework都是使用Java編寫,底層包括系統和使用衆多的Libraries都是C/C++編寫的,因此上層Java要調用底層的C/C++函數庫必須經過Java的JNI來實現c++

1.1 JNI 與 NDK 區別git

  • JNI:JNI是一套編程接口,用來實現Java代碼與本地的C/C++代碼進行交互;
  • NDK: NDK是Google開發的一套開發和編譯工具集,能夠生成動態連接庫,主要用於Android的JNI開發

1.2 JNI 做用github

  • 擴展:JNI擴展了JVM能力,驅動開發,例如開發一個wifi驅動,能夠將手機設置爲無限路由;
  • 高效: 本地代碼效率高,遊戲渲染,音頻視頻處理等方面使用JNI調用本地代碼,C語言能夠靈活操做內存;
  • 複用: 在文件壓縮算法 7zip開源代碼庫,機器視覺,OpenCV開放算法庫等方面能夠複用C平臺上的代碼,沒必要在開發一套完整的Java體系,避免重複發明輪子;
  • 特殊: 產品的核心技術通常也採用JNI開發,不易破解;

1.3 JNI在Android中做用: JNI能夠調用本地代碼庫(即C/C++代碼),並經過 Dalvik 虛擬機與應用層和應用框架層進行交互,Android中JNI代碼主要位於應用層和應用框架層;算法

應用層: 該層是由JNI開發,主要使用標準JNI編程模型; 應用框架層: 使用的是Android中自定義的一套JNI編程模型,該自定義的JNI編程模型彌補了標準JNI編程模型的不足;編程

2. 兩種基本的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中下載下列四項:

SDKtool
在File->Project Structure中添加下載好的Ndk路徑,通常下載好了NDK後,下面有個"Select Default"的按鈕:

Project Structure
在local.properties的文件中加入ndk的路徑:

local.properties

2.1 新建項目開發JNI

接下來在Android studio3.0中正式開發JNI ,Android studio已經支持建立C/C++的開發,並使用CMake的模式構建NDK開發

  • 建立一個支持C/C++的Android項目

新建勾選C++ support 的項目
建立過程一直Next下去,直到最後一步

這裏須要特殊說明:

  • 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等待項目建立完成。

  • 支持C/C++項目的總體結構

項目結構

和之前惟一不一樣的就是多出了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文件

build.gradle

能夠看出,AS幫咱們配置cmake時自動幫咱們添加了,上述兩塊代碼。可是在咱們本身配置cmake工具時,須要本身手動填寫,拷貝。

咱們Build編譯一下,在編譯輸出文件夾 能夠看到:

enter description here

編譯到運行示例 APP 的流程過程:

  1. Gradle 調用外部構建腳本,也就是 CMakeLists.txt;
  2. CMake 會根據構建腳本的指令去編譯一個 C++ 源文件,也就是 native-lib.cpp,並將編譯後的產物扔進共享對象庫中,並將其命名爲 libnative-lib.so,而後 Gradle 將其打包到 APK 中;
  3. 在運行期間,APP 的 MainActivity 會調用 System.loadLibrary() 方法,加載 native library。而這個庫的原生函數,stringFromJNI(),就能夠爲 APP 所用了;
  4. MainActivity.onCreate() 方法會調用 stringFromJNI(),而後返回 「Hello from C++」,並更新 TextView 的顯示; 注意:Instant Run 並不兼容使用了 native code 的項目。Android Studio 會自動禁止 Instant Run 功能。

.so庫在apk裏面:

app-debug.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);
}

複製代碼

在原項目基礎上修改庫的名字

CmakeLists.txt

MainActivity.java

build

2.2 在現有Project上開發JNI

  • 第一步: 在java文件夾中新建JniTest.java,把須要的native方法先定義好。
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字節碼文件才能下一步

JniTest.class

  • 第二步:須要使用javah 命令生成.h頭文件。

    • 1,手動命令生成 在AS下的Terminal 進入當前工程目錄:
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");
}
複製代碼
  • 2,工具生成javah命令 在Android Studio 的Preference 中的External Tool中新建工具:

external tool

在"Program"是JDK的javah命令的路徑,個人Mac結尾沒有.exe,有的資料是javah.exe多是Windows這樣配置。

enter description here

在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 生成以後以下圖:

enter description here

此時.h頭文件已經被更新:

.h

而後在C文件裏繼續實現就好。

  • 第三步:仍是使用Cmake編譯C++代碼,和上面不一樣的是 手動添加CmakeLists.txt文件,並修改build.gradle文件

添加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添加,下面兩項:

build.gradle

這樣整個JNI過程就完成了,下面就是靠本身學習編寫JNI代碼實現so邏輯了。

3.JNI數據類型

  • JNI數據類型映射 由頭文件代碼能夠看到,jni.h有不少類型預編譯的定義,而且區分了 C 和 C++的不一樣環境。
#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 類型存取其值;

  • 方法和變量 ID

一樣不能直接在 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 */

複製代碼

4.JNI 描述符

  • 域描述符
    • 基本類型描述符 下面是基本的數據類型的描述符,除了 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 代碼,就不須要手動編寫對應的類型轉換了。

5.JNIEnv 分析

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的特色

    • JNIEnv 是一個指針,指向一組 JNI 函數,經過這些函數能夠實現 Java 層和 JNI 層的交互,就是說經過 JNIEnv 調用 JNI 函數能夠訪問 Java 虛擬機,操做 Java 對象;
    • 全部本地函數都會接收 JNIEnv 做爲第一個參數;(不過 C++ 的JNI 函數已經對 JNIEnv 參數進行了封裝,不用寫在函數參數上)
    • 用做線程局部存儲,不能在線程間共享一個 JNIEnv 變量,也就是說 JNIEnv 只在建立它的線程有效,不能跨線程傳遞;相同的 Java 線程調用本地方法,所使用的 JNIEnv 是相同的,一個 native 方法不能被不一樣的 Java 線程調用;
  • 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); }
    //……
}

複製代碼

6.JNI 的兩種註冊方式

Java 的 native 方法是如何連接 C/C++中的函數的呢?能夠經過靜態和動態的方式註冊JNI。

  • 靜態註冊:原理:根據函數名創建 Java 方法和 JNI 函數的一一對應關係。流程以下:
    • 先編寫 Java 的 native 方法;
    • 而後用 javah 工具生成對應的頭文件,執行命令 javah packagename.classname能夠生成由包名加類名命名的 jni 層頭文件,或執行命名javah -o custom.h packagename.classname,其中 custom.h 爲自定義的文件名;
    • 實現 JNI 裏面的函數,再在Java中經過System.loadLibrary加載 so 庫便可;

靜態註冊的方式有兩個重要的關鍵詞 JNIEXPORTJNICALL,這兩個關鍵詞是宏定義,主要是註明該函數式 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 的方式編譯),函數命名後面就不須要加上「參數簽名」。

另外還須要注意幾點特殊規則:

  1. java的包名或類名或方法名中含下劃線_,在c++裏要用_1鏈接;
  2. 重載的本地方法命名要用雙下劃線__鏈接;
  3. 參數簽名的斜槓 「/」改成下劃線 _ 鏈接,分號 ;改成 _2鏈接,左方括號 [改成 _3 鏈接;
  4. 對於 Java 的 native 方法,static 和非 static 方法的區別在於第二個參數,static 的爲 jclass非 static 的 爲 jobject;JNI 函數中是沒有修飾符的。

優勢: 實現比較簡單,能夠經過 javah 工具將 Java代碼的 native 方法直接轉化爲對應的native層代碼的函數; 缺點:

  1. javah 生成的 native 層函數名特別長,可讀性不好;
  2. 後期修改文件名、類名或函數名時,頭文件的函數將失效,須要從新生成或手動改,比較麻煩;
  3. 程序運行效率低,首次調用 native 函數時,須要根據函數名在 JNI 層搜索對應的本地函數,創建對應關係,有點耗時;
  • 動態註冊 原理:直接告訴 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);

複製代碼

參考連接:

www.jianshu.com/p/ac00d5999… zhixinliu.com/2015/07/01/…

相關文章
相關標籤/搜索