Android NDK/JNI從入門到從新入門

1.1 JNI(Java Native Interface)

提供一種Java字節碼調用C/C++的解決方案,JNI描述的是一種技術 調用順序java

1.2 NDK(Native Development Kit)

Android NDK 是一組容許您將 C 或 C++(「原生代碼」)嵌入到 Android 應用中的工具,NDK描述的是工具集。 可以在 Android 應用中使用原生代碼對於想執行如下一項或多項操做的開發者特別有用:android

  • 在平臺之間移植其應用。
  • 重複使用現有庫,或者提供其本身的庫供重複使用。
  • 在某些狀況下提升性能,特別是像遊戲這種計算密集型應用。

1.3 JNI註冊

1.3.1 環境配置

這裏已有現成的文章就引用一下別人的了 ,講的很仔細 關於JNI環境配置c++

1.3.2 靜態註冊

當Java層調用navtie函數時,會在JNI庫中根據函數名查找對應的JNI函數。若是沒找到,會報錯。若是找到了,則會在native函數與JNI函數之間創建關聯關係,其實就是保存JNI函數的函數指針。下次再調用native函數,就能夠直接使用這個函數指針。git

JNI函數名格式(包名裏面的」.」須要改成」_」): Java_ + 包名(com_example_auto_jnitest)+ 類名(_MainActivity) + 函數名(_stringFromJNI)github

靜態註冊缺點:數組

  • 要求JNI函數的名字必須遵循JNI規範的命名格式;
  • 名字冗長,容易出錯;
  • 初次調用會根據函數名去搜索JNI中對應的函數,會影響執行效率;
  • 須要編譯全部聲明瞭native函數的Java類,每一個所生成的class文件都要用javah工具生成一個頭文件;

靜態註冊例子markdown

類 JNITest 包名:com.hqk.jnitestone架構

package com.hqk.jnitestone;

public class JNITest {

static {
    System.loadLibrary("native-lib");
}

    public static native String sayHello();
}

複製代碼

對應c++代碼 ,cpp 類名爲:native-lib.cpp函數

#include <jni.h>
#include <string>


extern "C"
JNIEXPORT jstring JNICALL Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
複製代碼

注:工具

  • cpp 類名爲:native-lib.cpp,則對應java交互類 須要加入
static {
        System.loadLibrary("native-lib");
    }
複製代碼
  • java 對應交互類的包名:com.hqk.jnitestone,則cpp對應方法須要將對應[.]轉化成[_];(參照上面格式)

1.3.2 動態註冊

經過提供一個函數映射表,註冊給JVM虛擬機,這樣JVM就能夠用函數映射表來調用相應的函數,就沒必要經過函數名來查找須要調用的函數。

Java與JNI經過JNINativeMethod的結構來創建函數映射表,它在jni.h頭文件中定義,其結構內容以下:

typedef struct {
    const char* name; // 對應交互java 類對應的方法名
    const char* signature;	//對應交互方法的函數簽名 (參考本文1.4.3)
    void*       fnPtr;	//對應交互cpp方法的 指針函數 (指向對應函數)
} JNINativeMethod;
複製代碼

一、建立映射表後,調用RegisterNatives函數將映射表註冊給JVM; 二、當Java層經過System.loadLibrary加載JNI庫時,會在庫中查JNI_OnLoad函數。可將JNI_OnLoad視爲JNI庫的入口函數,須要在這裏完成全部函數映射和動態註冊工做,及其餘一些初始化工做。

概念說完了,實際操做一下

JAVA類 JNITest 包名:com.hqk.jnitestone

package com.hqk.jnitestone;

public class JNITest {
    static {
        System.loadLibrary("native-lib");
    }

    public static native String sayHello();

    public static native String sayHello2();
}

複製代碼

注意:這裏多了一個 sayHello2 方法,方法返回值爲: String

CPP類名 native-lib.cpp 對應代碼

#include <jni.h>
#include <string>
#include <android/log.h>

extern "C"
JNIEXPORT jstring JNICALL Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

//cpp 交互方法
jstring sayHello2(JNIEnv *env, jobject thiz) {
    std::string hello = "Hello,我是動態註冊成功的!";
    return env->NewStringUTF(hello.c_str());
}

// 動態註冊 函數 結構數組
static const JNINativeMethod gMethods[] = { 
        {"sayHello2",	//對應java交互類的方法名
        "()Ljava/lang/String;", //對應方法名的函數簽名 (參考本文1.4.3)
         (jstring *) sayHello2 //對應 cpp交互類的指針函數
         } 
};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    __android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) //從JavaVM獲取JNIEnv,通常使用1.4的版本
        return -1;
        //注意:這裏 FindClass 必需要和交互類的 包名對應上,並換成[/]符號
    jclass clazz = env->FindClass("com/hqk/jnitestone/JNITest");
    if (!clazz) {
        __android_log_print(ANDROID_LOG_INFO, "native",
                            "cannot get class: com/hqk/jnitestone/JNITest");
        return -1;
    }
    if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
        __android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");
        return -1;
    }
    return JNI_VERSION_1_4;
}
複製代碼

從上面代碼能夠看出

一、CPP類裏面 建立了 JNI_OnLoad 方法--》 該方法是在 初始化的時候執行,相似於 activity 的onCreate, 核心代碼,就是經過Find找到對應Java類,最後經過RegisterNatives 實現動態註冊 二、CPP類裏面 新增了JNINativeMethod 類型的 結構數組 ; 三、CPP類裏面 新增了 sayHello2 交互方法 四、Java類裏面通用新增了 sayHello2方法

1.4 數據類型轉換

1.4.1 基本數據轉換

基本數據轉換

1.4.2 引用數據類型轉換

除了Class、String、Throwable和基本數據類型的數組外,其他全部Java對象的數據類型在JNI中都用jobject表示。Java中的String也是引用類型,可是因爲使用頻率較高,因此在JNI中單首創建了一個jstring類型。

引用數據類型轉換

  • 引用類型不能直接在 Native 層使用,須要根據 JNI 函數進行類型的轉化後,才能使用;
    • 多維數組(含二維數組)都是引用類型,須要使用 jobjectArray 類型存取其值;

例如,二維整型數組就是指向一位數組的數組,其聲明使用方式以下:

//得到一維數組的類引用,即jintArray類型 
    jclass intArrayClass = env->FindClass("[I");   
    //構造一個指向jintArray類一維數組的對象數組,該對象數組初始大小爲length,類型爲 jsize
    jobjectArray obejctIntArray  =  env->NewObjectArray(length ,intArrayClass , NULL);
複製代碼

1.4.3 JNI函數簽名信息

因爲Java支持函數重載,所以僅僅根據函數名是無法找到對應的JNI函數。爲了解決這個問題,JNI將參數類型和返回值類型做爲函數的簽名信息。

  • JNI規範定義的函數簽名信息格式:

(參數1類型字符…)返回值類型字符

  • 函數簽名例子:

函數簽名例子 3.JNI經常使用的數據類型及對應字符: JNI經常使用的數據類型及對應字符

1.4.4 JNIEnv介紹

  • JNIEnv概念 : JNIEnv是一個線程相關的結構體, 該結構體表明瞭 Java 在本線程的運行環境。經過JNIEnv能夠調用到一系列JNI系統函數。
  • JNIEnv線程相關性: 每一個線程中都有一個 JNIEnv 指針。JNIEnv只在其所在線程有效, 它不能在線程之間進行傳遞。

注意: 在C++建立的子線程中獲取JNIEnv,要經過調用JavaVM的AttachCurrentThread函數得到。在子線程退出時,要調用JavaVM的DetachCurrentThread函數來釋放對應的資源,不然會出錯。

JNIEnv 做用:

  • 訪問Java成員變量和成員方法;
  • 調用Java構造方法建立Java對象等。

1.5 JNI編譯

1.5.1 Cmake編譯

CMake 則是一個跨平臺的編譯工具,它並不會直接編譯出對象,而是根據自定義的語言規則(CMakeLists.txt)生成 對應 makefile 或 project 文件,而後再調用底層的編譯, 在Android Studio 2.2 以後支持Cmake編譯。

cmake_minimum_required(VERSION 3.4.1)

add_library(
        native-lib
        SHARED
        native-lib.cpp)
find_library(
        log-lib
        log)
target_link_libraries(
        native-lib
        ${log-lib})
複製代碼

1.5.1.1 add_library 指令

語法:add_library(libname SHARED | STATIC | MODULE [source])

將一組源文件 source 編譯出一個庫文件,並保存爲 libname.so (lib 前綴是生成文件時 CMake自動添加上去的)。其中有三種庫文件類型,不寫的話,默認爲 STATIC;

  • SHARED: 表示動態庫,能夠在(Java)代碼中使用 System.loadLibrary(name) 動態調用;
  • STATIC: 表示靜態庫,集成到代碼中會在編譯時調用;
  • MODULE: 只有在使用 dyId 的系統有效,若是不支持 dyId,則被看成 SHARED 對待;
  • EXCLUDE_FROM_ALL: 表示這個庫不被默認構建,除非其餘組件依賴或手工構建;
#將compress.c 編譯成 libcompress.so 的共享庫
add_library(compress SHARED compress.c)
複製代碼

1.5.1.2 target_link_libraries 指令

語法:target_link_libraries(target library <debug | optimized> library2…)

這個指令能夠用來爲 target 添加須要的連接的共享庫,一樣也能夠用於爲本身編寫的共享庫添加共享庫連接。如:

#指定 compress 工程須要用到 libjpeg 庫和 log 庫
target_link_libraries(compress libjpeg ${log-lib})
複製代碼

1.5.1.3 find_library 指令

語法:find_library( name1 path1 path2 ...)

VAR 變量表示找到的庫全路徑,包含庫文件名 。例如:

find_library(libX  X11 /usr/lib)
find_library(log-lib log)  #路徑爲空,應該是查找系統環境變量路徑
複製代碼

1.5.1.4 更多詳細使用

參考文獻:更多關於Cmake的詳細使用

1.5.2 Abi架構

ABI(Application binary interface)應用程序二進制接口。不一樣的CPU 與指令集的每種組合都有定義的 ABI (應用程序二進制接口),一段程序只有遵循這個接口規範才能在該 CPU 上運行,因此一樣的程序代碼爲了兼容多個不一樣的CPU,須要爲不一樣的 ABI 構建不一樣的庫文件。固然對於CPU來講,不一樣的架構並不意味着必定互不兼容。

  • armeabi設備只兼容armeabi;
  • armeabi-v7a設備兼容armeabi-v7a、armeabi;
  • arm64-v8a設備兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86設備兼容X8六、armeabi;
  • X86_64設備兼容X86_6四、X8六、armeabi;
  • mips64設備兼容mips6四、mips;
  • mips只兼容mips;

根據以上的兼容總結,咱們還能夠獲得一些規律:

  • armeabi的SO文件基本上能夠說是萬金油,它能運行在除了mips和mips64的設備上,但在非armeabi設備上運行性能仍是有所損耗;
  • 64位的CPU架構總能向下兼容其對應的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips;

1.6 項目地址

gitHub地址:點擊下載

相關文章
相關標籤/搜索