NDK開發實踐

NDK開發就是先用C/C++開發,而後把C/C++或者彙編代碼編譯成動態連接庫,最後JVM加載庫文件,經過JNI在Java和C/C++之間進行互相調用。通常狀況下,在性能敏感、音視頻和跨平臺等場景,都會涉及NDK開發。本文主要介紹經過Cmake進行NDK開發的一些配置,以及JNI相關知識。html

基於Cmake進行NDK開發

進行NDK開發,須要進行一些簡單配置,首先在local.properties中添加SDK和NDK路徑,其次在Android SDK中的SDK Tools安裝CMake和LLDB,而後在gradle.properties中移除android.useDeprecatedNdk = truejava

ndk.dir=/Users/xxx/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/xxx/Library/Android/sdk
cmake.dir=/Users/xxx/Library/Android/sdk/cmake/3.6.4111459
複製代碼

在模塊級build.gradle中添加Cmake配置,以下所示:android

android {
    ......
    defaultConfig {
        ......
        externalNativeBuild {
            cmake {
                // 設置C++編譯器參數
                cppFlags "-std=c++11"
                // 設置C編譯器參數
                cFlags ""
                // 設置Cmake參數,在CMakeLists.txt中能夠直接訪問參數
                arguments "-DParam=true"
            }
        }

        ndk {
            // 指定編譯輸出的庫文件ABI架構
            abiFilters "armeabi-v7a"
        }
    }
    
    externalNativeBuild {
        cmake {
            // 設置Cmake編譯文件的路徑
            path "CMakeLists.txt"
            // 設置Cmake版本號
            version "3.6.4111459"
        }
    }
}
複製代碼

下面咱們看一下一個典型的CMakeLists.txt的內容:ios

# 設置Cmake的最低版本號
cmake_minimum_required(VERSION 3.4.1)

# 日誌輸出
MESSAGE(STATUS "Param = ${Param}")
# 指定頭文件搜索路徑
include_directories("......")

# 基於源文件添加Library
add_library( # Sets the name of the library.
        avpractice

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        src/main/cpp/_onload.cpp)
     
# 基於靜態庫添加Library
add_library(
        libavcodec-lib
        STATIC
        IMPORTED)
# 設置libavcodec-lib的靜態庫路徑
set_target_properties( # Specifies the target library.
                       libavcodec-lib

                       # Specifies the parameter you want to define.
                       PROPERTIES IMPORTED_LOCATION

                       # Provides the path to the library you want to import.
                       ${FFMPEG_PATH}/lib/${ANDROID_ABI}/libavcodec.a)     

# 尋找NDK提供的庫文件,這裏是EGL
find_library( # Sets the name of the path variable.
              egl-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              EGL )    

# 指定連接庫,這裏會生層一個libavpractice.so 
target_link_libraries( # Specifies the target library.
        avpractice
        
        libavcodec-lib
        # Links the target library to the log library
        # included in the NDK.
        ${egl-lib})              
複製代碼

經過上述的add_librarytarget_link_libraries,咱們能夠同時生成多個動態庫文件。c++

JNI簡介

JNI全稱是:Java Native Interface,即鏈接JVM和Native代碼的接口,它容許Java和Native代碼之間互相調用。在Android平臺,Native代碼是指使用C/C++或彙編語言編寫的代碼,編譯後將以動態連接庫(.so)的形式供Java虛擬機加載,並遵守JNI規範互相調用。本質來講,JNI只是Java和C/C++之間的中間層,在組織代碼結構時,通常也是把Java、JNI和跨平臺的C/C++代碼放在不一樣目錄。下面咱們看一些JNI中比較重要的知識點。git

Java和Native的互相調用

Java調用Native

創建Java和Native方法的關聯關係主要有兩種方式:github

  • 靜態關聯:根據Java方法和Native方法的命名規範進行綁定,通常根據Java層Native方法名經過javah生成對應的Native方法名。
  • 動態關聯:在JNI_OnLoad中註冊JNI函數表。

靜態關聯

假設Java層的Native方法以下所示:shell

package com.leon;

public class LeonJNI {
    static {
        // 加載so
        System.loadLibrary("leon");
    }
    // Native Method
    public native String hello();
    // Static Native Method
    public static native void nihao(String str);
}
複製代碼

那麼經過javah生成頭文件的命令以下所示(當前目錄是包名路徑的上一級,即com目錄的父目錄):編程

javah -jni com.leon.LeonJNI
複製代碼

生成頭文件中的核心Native方法以下所示:數組

/* * 對應LeonJNI.hello實例方法 * Class: com_leon_LeonJNI * Method: hello * Signature: ()Ljava/lang/String; */
JNIEXPORT jstring JNICALL Java_com_leon_LeonJNI_hello (JNIEnv *, jobject);

/* * 對應LeonJNI.nihao靜態方法 * Class: com_leon_LeonJNI * Method: nihao * Signature: (Ljava/lang/String;)V */
JNIEXPORT void JNICALL Java_com_leon_LeonJNI_nihao (JNIEnv *, jclass, jstring);
複製代碼

動態關聯

當Java層加載動態連接庫時(System.loadLibrary("leon")),Native層jint JNI_OnLoad(JavaVM *vm, void *reserved)全局方法首先會被調用,因此這裏是註冊JNI函數表的最佳場所。

假設Java層實現不變,對應的Native層代碼以下所示:

#define PACKAGE_NAME "com/leon/LeonJNI"
#define ARRAY_ELEMENTS_NUM(p) ((int) sizeof(p) / sizeof(p[0]))

//全局引用
jclass g_clazz = nullptr;

// 對應LeonJNI.nihao靜態方法
jstring JNICALL nativeHello(JNIEnv *env, jobject obj) {
    ......
}

// 對應LeonJNI.nihao靜態方法
void JNICALL nativeNihao(JNIEnv * env , jclass clazz, jstring jstr){
    ......
}

// 方法映射表
static JNINativeMethod methods[] = {
    {"hello", "()Ljava/lang/String;", (void *) nativeHello},
    {"nihao", "(Ljava/lang/String;)V", (void *) nativeNihao},
};

// 註冊函數表
static int register_native_methods(JNIEnv *env) {
    if (env->RegisterNatives(g_clazz, methods, ARRAY_ELEMENTS_NUM(methods)) < 0){
        return JNI_ERR;
    }
    return JNI_OK;
}

// JVM加載動態庫時,被調用
jint JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_EVERSION;
    }
    
    jclass clazz = env->FindClass(PACKAGE_NAME);
    if (clazz == nullptr) {
        return JNI_EINVAL;
    }
    g_clazz = (jclass) env->NewGlobalRef(clazz);
    env->DeleteLocalRef(clazz);

    int result = register_native_methods(env);
    if (result != JNI_OK) {
        LOGE("native methods register failed");
    }

    return JNI_VERSION_1_6;
}

// JVM卸載動態庫時,被調用
void JNI_OnUnload(JavaVM* vm, void* reserved){
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return ;
    }
    
    if(g_clazz != nullptr){
        env->DeleteGlobalRef(g_clazz);
    }
    
    // 其餘清理工做
    ......
}
複製代碼

JNI_OnLoad是全局函數,一個動態連接庫只能有一個實現。

Native調用Java

從Native調用Java,與Java的反射調用相似,首先要獲取Java類的jclass對象,而後獲取屬性或者方法的jfieldID或者jmethodID。針對成員屬性,經過JNIEnv->Set(Static)XXField設置屬性值,經過JNIEnv->Get(Static)XXField獲取屬性值,其中XX表示成員屬性的類型。針對成員方法,經過JNIEnv->Call(Static)YYMethod調用方法,其中YY表示成員方法的返回值類型。下面咱們來看一個簡單示例。 在上面LeonJNI類中新增了兩個從Native層調用的方法:

package com.leon;

public class LeonJNI {
    static {
        // 加載so
        System.loadLibrary("leon");
    }
    // Native Method
    public native String hello();
    // Static Native Method
    public static native void nihao(String str);
    
    // 從Native調用的實例方法,必須進行反混淆
    public String strToNative(){
        return "Test";
    }
    // 從Native調用的靜態方法,必須進行反混淆
    public static int intToNative(){
        return 100;
    }
}
複製代碼

而後,從Native調用Java層方法的示例以下所示(簡化後的代碼):

//全局引用,com.leon.LeonJNI對應的jclass,從Native層調用Java層靜態方法時,做爲參數使用
jclass g_clazz = nullptr;
// com.leon.LeonJNI對應的對象,從Native層調用Java層實例方法時,表示具體調用哪一個類對象的實例方法
jobject g_obj = nullptr;

// LeonJNI.strToNative對應的jmethodID
jmethodID strMethod = env->GetMethodID(g_clazz, "strToNative", "()Ljava/lang/String;");
// LeonJNI.intToNative對應的jmethodID
jmethodID intMethod = env->GetStaticMethodID(g_clazz, "intToNative", "()I");

// 調用實例方法:LeonJNI.strToNative
jstring strResult = (jstring)env->CallObjectMethod(g_obj,strMethod);
// 調用靜態方法:LeonJNI.intToNative
jint intResult = env->CallStaticIntMethod(g_clazz,intMethod);
複製代碼

上述代碼雖然簡單,但確是從Native調用Java方法的基本流程,關於Java和Native之間的參數傳遞以及處理,接下來會進行更詳細的介紹。

獲取JNIEnv指針

上述從Native層調用Java方法,前提是Native持有JNIEnv指針。在Java線程中,JNIEnv實例保存在線程本地存儲 TLS(Thread Local Storage)中,所以不能在線程間共享JNIEnv指針,若是當前線程的TLS中存有JNIEnv實例,只是沒有指向該實例的指針,能夠經過JavaVM->GetEnv((JavaVM*, void**, jint))獲取指向當前線程持有的JNIEnv實例的指針。JavaVM是全進程惟一的,能夠被全部線程共享。

還有一種更特殊的狀況:即線程自己沒有JNIEnv實例(例如:經過pthread_create()建立的Native線程),這種狀況下須要調用JavaVM->AttachCurrentThread()將線程依附於JavaVM以得到JNIEnv實例(Attach到JVM後就被視爲Java線程)。當Native線程退出時,必須配對調用JavaVM->DetachCurrentThread()以釋放JVM資源,例如:局部引用。

爲了不DetachCurrentThread沒有配對調用,能夠經過 int pthread_key_create(pthread_key_t* key, void (*destructor)(void*))建立一個 TLS的pthread_key_t:key,並註冊一個destructor回調函數,它會在線程退出前被調用,所以很適合用於執行相似DetachCurrentThread的清理工做。此外,還能夠調用pthread_setspecific函數把JNIEnv指針保存到TLS中,這樣不只能夠隨用隨取,並且當destructor函數被調用時,JNIEnv指針也會做爲參數傳入,方便調用Java層的一些清理方法。示例代碼以下所示:

// 全進程惟一的JavaVM
JavaVM * javaVM;
// TLS key
pthread_key_t threadKey;

// 線程退出時的清理函數
void JNI_ThreadDestroyed(void *value) {
    JNIEnv *env = (JNIEnv *) value;
    if (env != nullptr) {
        javaVM->DetachCurrentThread();
        pthread_setspecific(threadKey, nullptr);
    }
}

// 獲取JNIEnv指針
JNIEnv* getJNIEnv() {
    // 首先嚐試從TLS Key中獲取JNIEnv指針
    JNIEnv *env = (JNIEnv *) pthread_getspecific(threadKey); 
    if (env == nullptr) {
        // 而後嘗試從TLS中獲取指向JNIEnv實例的指針
        if (JNI_OK != javaVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
            // 最後只能attach到JVM,才能獲取到JNIEnv指針
            if (JNI_OK == javaVM->AttachCurrentThread(&env, nullptr)) {
                // 把JNIEnv指針保存到TLS中
                pthread_setspecific(threadKey, env); 
            }
        }
    }
    
    return env;
}

jint JNI_OnLoad(JavaVM *vm, void *) { 
    javaVM = vm;
    // 建立TLS Key,並註冊線程銷燬函數
    pthread_key_create(&threadKey, JNI_ThreadDestroyed);
    return JNI_VERSION_1_6;
}

複製代碼

JNI中Java類型的簡寫

在JNI中,當咱們使用GetFieldID、GetMethodID等函數操做Java對象時,須要表示成員屬性的類型,或者成員函數的方法簽名,JNI以簡寫的形式組織這些類型。

對於成員屬性,直接以Java類型的簡寫表示便可。 例如:

  • "I"表示該成員變量是Int類型;
  • "Ljava/lang/String;"表示該成員變量是String類型。

示例:

jfieldID name = (*env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
jfieldID age = (*env)->GetFieldID(objectClass,"age","I");
複製代碼

對於成員函數,以(*)+形式表示函數的方法簽名。()中的字符串表示函數參數,括號外則表示返回值。 例如:

  • ()V 表示void method();
  • (II)V 表示 void method(int, int);
  • (Ljava/lang/String;Ljava/lang/String;)I表示 int method(String,String)

示例:

jmethodID ageId = (*env)->GetMethodID(env, objectClass,"getAge","(Ljava/lang/String;Ljava/lang/String;)I");
複製代碼

JNI中的類型簡寫以下所示:

Java類型 類型簡寫
Boolean Z
Char C
Byte B
Short S
Int I
Long J
Float F
Double D
Void V
Object對象 L開頭,以;結尾,中間用/分割的包名和類名。
數組對象 [開頭,加上數組類型的簡寫。例如:[I表示 int [];

JNI中的參數傳遞和操做

在JNI的調用中,共涉及到Java層類型、JNI層類型和C/C++層類型(其實,JNI類型是基於C/C++類型經過typedef定義的別名,這裏拆分出來是爲了更加清晰,便於理解)。那麼這幾種類型之間是如何映射的,其實jni.h裏面給出了JNI層類型的定義。 總體的類型映射以下表所示:

Java類型 JNI類型 C/C++類型
boolean jboolean unsigned char (8 bits)
char jchar unsigned short (16 bits)
byte jbyte signed char (8 bits)
short jshort signed short (16 bits)
int jint signed int (32 bits)
long jlong signed long long(64 bits)
float jfloat float (32 bits)
double jdouble double (32 bits)
Object jobject void*(C)或者 _jobject指針(C++)
Class jclass jobject的別名(C)或者 _jclass指針(C++)
String jstring jobject的別名(C)或者 _jstring指針(C++)
Object[] jobjectArray jarray的別名(C)或者 _jobjectArray指針(C++)
boolean[] jbooleanArray jarray的別名(C)或者 _jbooleanArray指針(C++)
char[] jcharArray jarray的別名(C)或者 _jcharArray指針(C++)
byte[] jbyteArray jarray的別名(C)或者 _jbyteArray指針(C++)
short[] jshortArray jarray的別名(C)或者 _jshortArray指(C++)
int[] jintArray jarray的別名(C)或者 _jintArray指針(C++)
long[] jlongArray jarray的別名(C)或者 _jlongArray指針(C++)
float[] jfloatArray jarray的別名(C)或者 _jfloatArray指(C++)
double[] jdoubleArray jarray的別名(C)或者_jdoubleArray指針(C++)

衆所周知,Java包括2種數據類型:基本類型和引用類型,JNI對基本類型的處理比較簡單:Java層的基本類型和C/C++層的基本類型是一一對應,能夠直接相互轉換,jni.h中的定義以下所示:

typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;
複製代碼

而對於引用類型,若是JNI是用C語言編寫的,那麼其定義以下所示,即全部引用類型都是jobject類型:

typedef void*           jobject;
typedef  jobject        jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;
複製代碼

若是JNI是用C++語言編寫的,那麼其定義以下所示:

class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;
複製代碼

JNI利用C++的特性,創建了一個引用類型集合,集合中全部類型都是jobject的子類,這些子類和Java中的引用類型相對應。例如:jstring表示字符串、jclass表示class字節碼對象、jarray表示數組,另外jarray派生了9個子類,分別對應Java中的8種基本數據類型(jintArray、jbooleanArray、jcharArray等)和對象類型(jobjectArray)。 因此,JNI整個引用類型的繼承關係以下圖所示:

JNI引用類型的繼承關係

總的來講,Java層類型映射到JNI層的類型是固定的,可是JNI層類型在C和C++平臺具備不一樣的解釋。

上面介紹了Java層類型、JNI層類型和C/C++層類型三種類型之間的映射關係。下面咱們看下Java層的基本類型和引用類型,在Native層的具體操做。

基本類型

對於基本類型,不論是Java->Native,仍是Native->Java,均可以在Java和C/C++之間直接轉換,須要注意的是Java層的long是8字節,對應到C/C++是long long類型。

字符串類型

Java的String和C++的string是不對等的,因此必須進行轉換處理。

//把UTF-8編碼格式的char*轉換爲jstring
jstring (*NewStringUTF)(JNIEnv*, const char*);
//獲取jstring的長度
size (*GetStringUTFLength)(JNIEnv*, jstring);
//把jstring轉換成爲UTF-8格式的char*
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
//釋放指向UTF-8格式的char*的指針
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);

//示例:
#include <iostream>
JNIEXPORT jstring JNICALL Java_Main_getStr(JNIEnv *env, jobject obj, jstring arg) {
    const char* str;
    //把jstring轉換爲UTF-8格式的char *
    str = (*env)->GetStringUTFChars(arg, false);
    if(str == NULL) {
        return NULL; 
    }
    std::cout << str << std::endl;
    //顯示釋放jstring
    (*env)->ReleaseStringUTFChars(arg, str);
    //建立jstring,返回到java層
    jstring rtstr = (*env)->NewStringUTF("Hello String");
    return rtstr;
}
複製代碼

在使用完轉換後的char * 以後,須要顯示調用 ReleaseStringUTFChars方法,讓JVM釋放轉換成UTF-8的string的對象空間,若是不顯示調用,JVM會一直保存該對象,不會被GC回收,所以會致使內存泄漏。

對象引用類型

在JNI中,除了String以外(jstring),其餘的對象類型都映射爲jobject。JNI提供了在Native層操做Java層對象的能力: 1.首先經過FindClass或者GetObjectClass得到對應的jclass對象。

//根據類名獲取對應的jclass對象
jclass  (*FindClass)(JNIEnv*, const char*);
//根據已有的jobject對象獲取對應的jclass對象
jclass  (*GetObjectClass)(JNIEnv*, jobject);

//示例:
//獲取User對應的jclass對象
jclass clazz = (*env)->FindClass("com.leon.User") ;
//獲取User對應的jclass對象,jobject_user標識jobject對象
jclass clazz = (*env)->GetObjectClass (env , jobject_user);
複製代碼

2.而後經過GetFieldID/GetStaticFieldID得到成員屬性IDjfieldID,或者經過GetMethodID/GetStaticMethodID得到成員函數IDjmethodID

//得到Java類的實例成員屬性
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
//獲取Java類的靜態成員屬性
jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*,
                        const char*);
//獲取Java類的實例成員函數 
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//獲取Java類的靜態成員函數 
jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
//第一個參數固定是JNIENV,第二個參數jclass表示在哪一個類上操做,第三個參數表示對應的成員屬性或者成員函數的名字,第四個參數表示對應的成員屬性的類型或者成員函數的方法簽名。

//示例:
jmethodID getAgeId = (*env)->GetMethodID(env, jclass,"getAge","()I");
複製代碼

3.最後對獲取的jfieldIDjmethodID進行操做。針對成員屬性,主要是獲取和設置屬性值,而屬性又可分爲實例屬性和靜態屬性,所以操做成員屬性的函數原型以下所示:

//獲取實例屬性的值
// 實例屬性是基本類型
JNIType   (*Get<PrimitiveType>Field)(JNIEnv*, jobject, jfieldID)
// 實例屬性是對象類型
jobject   (*GetObjectField)(JNIEnv*, jobject, jfieldID);

//設置實例屬性的值
// 實例屬性是基本類型
void   (*Set<PrimitiveType>Field)(JNIEnv*, jobject, jfieldID, JNIType)
// 實例屬性是對象類型
void   (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);

//獲取靜態屬性的值
// 靜態屬性是基本類型
JNIType   (*GetStatic<PrimitiveType>Field)(JNIEnv*, jclass, jfieldID)
// 靜態屬性是對象類型
jobject   (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);

//設置靜態屬性的值
// 靜態屬性是基本類型
void   (*SetStatic<PrimitiveType>Field)(JNIEnv*, jclass, jfieldID, JNIType)
// 靜態屬性是對象類型
void   (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject);
複製代碼

其中,PrimitiveType表示Java基本類型,JNIType表示對應的JNI基本類型。 針對成員方法,主要是調用成員方法,而成員方法又分爲實例方法和靜態方法。所以操做成員方法的函數原型以下所示:

// 調用實例方法
// 實例方法的返回值是對象類型
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
// 實例方法無返回值
void  (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
// 實例方法的返回值是基本類型
JNIType (*Call<PrimitiveType>Method)(JNIEnv*, jobject, jmethodID, ...);

// 調用靜態方法
// 靜態方法的返回值是對象類型
jobject CallStaticObjectMethod (jclass cl0, jmethodID meth1, ...) // 靜態方法無返回值 void CallStaticVoidMethod (jclass cl0, jmethodID meth1, ...) // 靜態方法的返回值是基本類型 JNIType (*CallStatic<PrimitiveType>Method)(JNIEnv*, jobject, jmethodID, ...);
複製代碼

在JNI中,也能夠建立一個Java對象,主要經過如下方法:

//jclass表示要建立的類,jmethodID表示用哪一個構造函數建立該類的實例,後面的則爲構造函數的參數
jobject  (*NewObject)(JNIEnv*, jclass, jmethodID, ...);

//示例
jclass strClass = (*env)->FindClass(env,"Ljava/lang/String;");
jmethodID ctorID = (*env)->GetMethodID(env,strClass, "<init>", "(Ljava/lang/String;)V");
jobject str = (*env)->NewObject(env,strClass,ctorID,"name");
複製代碼

數組引用類型

經過上面的類型介紹可知,JNI共有9種數組類型:jobjectArray和8種基本類型數組,簡單表示爲j<PrimitiveType>Array。對於jobjectArray,JNI只提供了GetObjectArrayElementSetObjectArrayElement方法容許每次操做數組中的一個對象。對於基本類型數組j<PrimitiveType>Array,JNI提供了2種訪問方式。

把基本類型的Java數組映射爲C數組

JNI提供了以下原型的方法,把Java數組映射爲C數組

JNIType *Get<PrimitiveType>ArrayElements(JNIEnv *env, JNIArrayType array, jboolean *isCopy)
複製代碼

其中,JNIType表示jint、jlong等基本類型,JNIArrayType表示jintArray、jlongArray等對應的JNI數組類型。

上述方法會返回指向Java數組的堆地址或新申請副本的地址(能夠傳遞非NULL的isCopy 指針來確認返回值是否爲副本),若是指針指向Java數組的堆地址而非副本,在 Release<PrimitiveType>ArrayElements以前,此Java數組都沒法被GC回收,因此 Get<PrimitiveType>ArrayElementsRelease<PrimitiveType>ArrayElements必須配對調用以免內存泄漏。另外Get<PrimitiveType>ArrayElements可能因內存不足建立副本失敗而返回NULL,因此應該先對返回值判空後再使用。

Release<PrimitiveType>ArrayElements方法原型以下:

void Release<PrimitiveType>ArrayElements(JNIEnv *env, JNIArrayType array, JNIType *jniArray, jint mode);
複製代碼

最後一個參數mode僅對jniArray爲副本時有效,能夠用於避免一些非必要的副本拷貝,共有如下三種取值:

  1. 0,將jniArray數組內容回寫到Java數組,並釋放jniArray佔用的內存。
  2. JNI_COMMIT,將jniArray數組內容回寫到Java數組,但不釋放jniArray佔用的內存。
  3. JNI_ABORT,不回寫jniArray數組內容到Java數組,僅僅釋放jniArray佔用的內存。

通常來講,mode爲0是最合適的選擇,這樣無論Get<PrimitiveType>ArrayElements返回值是不是副本,都不會發生數據不一致和內存泄漏問題。但也有一些場景爲了性能等因素考慮會使用非零值,好比:對於一個尺寸很大的數組,若是獲取指針 以後經過isCopy確認是副本,且以後沒有修改過內容,那麼徹底可使用JNI_ABORT避免回寫以提升性能;另外一種場景是Native修改數組和Java讀取數組在交替進行(如多線程環境),若是經過isCopy確認獲取的數組是副本,則能夠經過JNI_COMMIT模式,可是JNI_COMMIT不會釋放副本,因此最終還須要使用其餘mode,再調用Release<PrimitiveType>ArrayElements以免副本泄漏。

一種常見的錯誤用法:當isCopy爲false時,沒有調用對應的Release<PrimitiveType>ArrayElements。此時雖然未建立副本,可是Java數組的堆內存被引用後會阻止GC回收,所以也必須配對調用Release方法。

塊拷貝

針對JVM基本類型數組,還能夠進行塊拷貝,包括:從JVM拷貝到Native和從Native拷貝到JVM。

從JVM拷貝到Native的函數原型以下所示:表示把數據從JVM的array數組拷貝到Native層的buf數組。

Get<PrimitiveType>ArrayRegion(JNIEnv *env, JNIArrayType array,jsize start, jsize len, JNIType * buf)
複製代碼

從Native拷貝到JVM的函數原型以下所示:表示把數據從Native層的buf數組拷貝到JVM的array數組。

void Set<PrimitiveType>ArrayRegion(JNIEnv *env, JNIArrayType array, jsize start, jsize len, const JNIType * buf)
複製代碼

其中,JNIType表示jint、jlong等基本類型,JNIArrayType表示jintArray、jlongArray等對應的JNI數組類型。

相比於前一種數組操做方式,塊拷貝有如下優勢:

  1. 只須要一次JNI調用,減小開銷。
  2. 無需建立副本或引用JVM數組內存(即:不影響GC)
  3. 下降編程出錯的風險——不會因忘記調用Release函數而引發內存泄漏。

JNI引用

JNI規範中定義了三種引用:全局引用(Global Reference),局部引用(Local Reference)和弱全局引用(Weak Global Reference)。無論哪一種引用,持有的都是jobject及其子類對象(包括 jclass, jstring, jarray等,但不包括指針類型、jfieldID和jmethodID)。

引用和被引用對象是兩個不一樣的對象,只有先釋放了引用對象才能釋放被引用對象。

局部引用

每一個傳給Native方法的對象參數(jobject及其子類,包括 jclass, jstring, jarray等)和幾乎全部JNI函數返回的對象都是局部引用。這意味着它們只在當前線程的當前Native方法內有效,一旦該方法返回則失效(哪怕被引用的對象仍然存在)。因此正常狀況下,咱們無須手動調用DeleteLocalRef釋放局部引用,除非如下幾種狀況:

  1. Native方法內建立大量的局部引用,例如在循環中反覆建立,由於JVM保存局部引用的空間是有限的 (Android爲512),一旦循環中建立的引用數超出限制就會致使異常:ReferenceTable overflow (max=512);
  2. 經過AttachCurrentThread()依附到JVM的線程內全部局部引用均不會被自動釋放,直到調用DetachCurrentThread()纔會統一釋放,爲避免線程中累積過多局部引用,建議及時手動釋放。
  3. Native方法內,局部引用引用了一個很是大的對象,用完後還要進行較長時間的其它運算才能返回,局部引用會阻止該對象被GC。爲下降OOM風險,用完後應當及時手動釋放。

上述對象是指jobject及其子類,包括jclass、jstring、jarray,不包括GetStringUTFChars和GetByteArrayElements這類函數的原始數據指針返回值,也不包括jfieldID和jmethodID ,在Android下這二者在類加載以後就一直有效。

Native方法內建立的jobject及其子類對象(包括jclass、jstring、jarray等,但不包括指針類型、jfieldID和jmethodID),默認都是局部引用。

全局引用和弱全局引用

全局引用的生存期爲建立(NewGlobalRef)後,直到咱們顯式釋放它(DeleteGlobalRef)。 弱全局引用的生存期爲建立(NewWeakGlobalRef)後,直到咱們顯式釋放(DeleteWeakGlobalRef)它或者JVM認爲應該回收它的時候(好比:內存緊張),進行回收釋放。

(弱)全局引用能夠跨線程跨方法使用,由於經過NewGlobalRef或者NewWeakGlobalRef方法建立後會一直有效,直到調用DeleteGlobalRef或者DeleteWeakGlobalRef方法手動釋放。這個特性經常使用於緩存一些獲取起來較耗時的對象,好比:經過FindClass獲取的jclass,Java層傳下來的jobject等,這些對象均可以經過全局引用緩存起來,供後續使用。

引用比較

比較兩個引用是否指向同一個對象可使用IsSameObject函數

jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2); 
複製代碼

JNI中的NULL指向JVM中的null對象,IsSameObject用於弱全局引用(WeakGlobalRef)與NULL比較時,返回值表示其引用的對象是否已經回收(JNI_TRUE表明已回收,該弱引用已無效)。

JNI把Java中的對象當作一個C指針傳遞到Native方法,這個指針指向JVM中的內部數據結構,而內部數據結構在內存中的存儲方式對外是不可見的。因此,Native方法必須經過在JNIEnv中選擇適當的JNI函數來操做JVM中的對象。

經過JNIEnv建立的對象都受JVM管理,雖然這些對象在在Native層建立(經過Jni接口),可是能夠經過返回值等多種方式引入到Java層,這也間接說明了這些對象分配在Java Heap中。

遇到的問題

NDK開發中總會遇到一些奇奇怪怪的問題,這裏列舉一些典型問題。

Native線程FindClass失敗

假如遇到FindClass失敗問題,首先要排除一些簡單緣由:

  1. 檢查包名、類名是否拼寫錯誤,例如:加載String時,應當是java/lang/String,檢查是否用/分割包名和類名,此時不須要添加L;,若是是內部類,那麼使用$而不是.去標識。
  2. 檢查對應的Java類,是否進行了反混淆,若是你的類/方法/屬性僅僅從Native層訪問,那就八九不離十是這個緣由了。

若是你排除了以上緣由,仍是沒法找到對應類,那可能就是多線程問題了。 通常狀況下,從Java層調用到Native層時,會攜帶棧幀信息(stack frames),其中包含加載當前應用類的ClassLoaderFindClass會依賴該ClassLoader去查找類(此時,通常是負責加載APP類的PathClassLoader)。 可是若是在Native層經過pthread_create建立線程,而且經過AttachCurrentThread關聯到JVM,那麼此時沒有任何關於App的棧幀信息,因此FindClass會依賴系統類加載器去查找類(此時,通常是負責加載系統類的BootClassLoader)。所以,加載全部的APP類都會失敗,可是能夠加載系統類,例如:android/graphics/Bitmap

有如下幾種解決方案:

  1. JNI_OnLoad(Java層調用System.loadLibrary時,會被觸發)中,經過FindClass找出全部須要的jclass,而後經過全局引用緩存起來,後面須要時直接使用便可。
  2. 在Native層緩存App類加載器對象和loadClass的MethodID,而後經過調用PathClassLoader.loadClass方法直接加載指定類。
  3. 把須要的Class實例經過參數傳遞到Native層函數。

下面分別看一下方案1和方案2的簡單示例:

方案1:緩存jclass

jclass cacheClazz = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *pEnv = nullptr;
    if(vm->GetEnv((void **) &pEnv, JNI_VERSION_1_6) != JNI_OK){
        return JNI_ERR;
    }
    jclass clazz = env->FindClass("com/leon/BitmapParam");
    if (clazz == nullptr) {
        return JNI_ERR;
    }
    // 建立並緩存全局引用
    cacheClazz = (jclass) env->NewGlobalRef(clazz);
    // 刪除局部引用
    env->DeleteLocalRef(clazz);
    return JNI_VERSION_1_6;
}
複製代碼

而後能夠在任何Native線程,經過上述緩存的cacheClazz,去獲取jmethodIDjfieldID,而後實現對Java對象的訪問。

方案2:緩存ClassLoader

// 緩存的classloader
jobject jobject_classLoader = nullptr
// 緩存的loadClass的methodID
jmethodID loadClass_methodID = nullptr
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *pEnv = nullptr;
    if(vm->GetEnv((void **) &pEnv, JNI_VERSION_1_6) != JNI_OK){
        return JNI_ERR;
    }
    // jclass point to Test.java,這裏能夠是App的任意類
    jclass jclass_test = env->FindClass("com/ltlovezh/avpractice/render/Test");
    // jclass point to Class.java
    jclass jclass_class = env->GetObjectClass(jclass_test);

    jmethodID getClassLoader_methodID = env->GetMethodID(jclass_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
    jobject local_jobject_classLoader = env->CallObjectMethod(jclass_test, getClassLoader_methodID);
    // 建立全局引用
    jobject_classLoader = env->NewGlobalRef(local_jobject_classLoader);

    jclass jclass_classLoader = env->FindClass("java/lang/ClassLoader");
    loadClass_methodID = env->GetMethodID(jclass_classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    // 刪除局部引用
    env->DeleteLocalRef(jclass_test);
    env->DeleteLocalRef(jclass_class);
    env->DeleteLocalRef(local_jobject_classLoader);
    env->DeleteLocalRef(jclass_classLoader);
  
  return JNI_VERSION_1_6;  
}

// 經過緩存的ClassLoader直接Find Class
jclass findClass(JNIEnv *pEnv, const char* name) {
    return static_cast<jclass>(pEnv->CallObjectMethod(jobject_classLoader, loadClass_methodID, pEnv->NewStringUTF(name)));
}
複製代碼

上述在JNI_OnLoad中緩存了ClassLoader和loadClass的jmethodID,在須要時能夠直接加載指定類,獲取對應的jclass。

C++代碼沒法關聯

曾經遇到過使用cmake3.10,致使C++代碼沒法關聯跳轉的問題,後來對cmake降級處理就OK了。具體步驟以下:

local.properties中指定cmake路徑:

cmake.dir=/Users/xxx/Library/Android/sdk/cmake/3.6.4111459
複製代碼

在模塊級build.gradle中指定cmake版本:

externalNativeBuild {
    cmake {
        path "CMakeLists.txt"
        version "3.6.4111459"
    }
}
複製代碼

參考文檔

  1. Android NDK 開發教程
  2. JNI FindClass Error in Native Thread
  3. JNI官方規範
  4. Google JNI tips
相關文章
相關標籤/搜索