NDK學習-指南(二)

JNI知識總結和使用技巧

JNI定義

JNI(Java Native Interface)意爲JAVA本地調用,它容許Java代碼和其餘語言寫的代碼進行交互。一種在Java虛擬機控制下執行代碼的標準機制。java

從代碼的角度再回顧JNI

組織結構: android

image-20181225135732031

JNI函數表的組成就像C++的虛函數表,虛擬機能夠運行多張函數表。JNI接口指針僅在當前線程中起做用,指針不能從一個線程進入另外一個線程,但能夠在不一樣的線程中調用本地方法。 咱們在來看看JNI的頭文件:c++

image-20181225140011087

JNI類型和數據結構:算法

image-20181225140502085

引用類型:數組

image-20181225140549436

屬性、方法、值類型:安全

jni.h 102struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */

struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */

struct JNIInvokeInterface;

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;
複製代碼

Type Signatures:服務器

image-20181225141323294

觸類旁通數據結構

String類型的域描述符爲 Ljava/lang/String;      
[ + 其類型的域描述符 + ;  
int[ ]     其描述符爲[I  
float[ ]   其描述符爲[F  
String[ ]  其描述符爲[Ljava/lang/String;  
Object[ ]類型的域描述符爲[Ljava/lang/Object;  
int  [ ][ ] 其描述符爲[[I  
float[ ][ ] 其描述符爲[[F 
---------------------------------------------------------------------------------------------
Java層方法                                               JNI函數簽名  
String test ( ) Ljava/lang/String;  
int f (int i, Object object) (ILjava/lang/Object;)I void set (byte[ ] bytes) ([B)V 複製代碼

JNIEnv與JavaVM

JNIEnv 概念 : 是一個線程相關的結構體, 該結構體表明瞭 Java 在本線程的運行環境 ;多線程

JNIEnv 與 JavaVM : 注意區分這兩個概念;app

  • JavaVM : JavaVM 是 Java虛擬機在 JNI 層的表明, JNI 全局只有一個;
  • JNIEnv : JavaVM 在線程中的表明, 每一個線程都有一個, JNI 中可能有不少個JNIEnv;

JNIEnv 做用 :

  • 調用 Java 函數 : JNIEnv 表明 Java 運行環境, 可使用 JNIEnv 調用 Java 中的代碼;
  • 操做 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 須要使用 JNIEnv 來操做這個 Java 對象;

***總結:***JNIEnv只在當前線程中有效。本地方法不能將JNIEnv從一個線程傳遞到另外一個線程中。相同的 Java 線程中對本地方法屢次調用時,傳遞給該本地方法的JNIEnv是相同的。可是,一個本地方法可被不一樣的 Java 線程所調用,所以能夠接受不一樣的 JNIEnv。

JNI調用流程梳理

/packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterApp.java

其實要學習JNI的知識作好是Android的源碼,咱們知道藍牙、MediaPlayer都使用了JNI技術。你們注意在看源碼的時候必定要注意梳理調用流程。

  • Java代碼會 有System.loadLibrary("****")和相關的native函數。
  • 在JNI具體實現的對應的CPP源碼中通常會有以下NativeMethod註冊函數和具體實現Java代碼中對應的native函數。
  • JNI_OnLoad函數中進行方法的動態註冊,方便C++和Java的調用。(其實在JNI_OnLoad中最終都會調用jniRegisterNativeMethods這個方法。)
static JNINativeMethod gMethods[] = {
    ······
    {"native_init",         "()V",                              (void *)android_media_MediaPlayer_native_init},
    // 這邊是 native_setup : 第一個 是java函數名,第二個是簽名,第三個是 jni具體實現方法的指針
    {"native_setup",        "(Ljava/lang/Object;)V",            (void *)android_media_MediaPlayer_native_setup},
    {"native_finalize",     "()V",                              (void *)android_media_MediaPlayer_native_finalize},
    ······
};

// jni具體實現方法的指針
static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this) {
    ALOGV("native_setup");
    sp<MediaPlayer> mp = new MediaPlayer();
    if (mp == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    // create new listener and give it to MediaPlayer
    sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this);
    mp->setListener(listener);

    // Stow our new C++ MediaPlayer in an opaque field in the Java object.
    setMediaPlayer(env, thiz, mp);
}

// This function only registers the native methods
static int register_android_media_MediaPlayer(JNIEnv *env) {
    // gMethods 在這邊被調用,系統能夠拿到AndroidRuntime:,咱們拿不到,只能分析,他註冊的時候作了什麼事情,
    // 分析: env ,"android/media/MediaPlayer" 是MediaPlayer.java的包名+類名
    // gMethods
    // NELEM(gMethods)算這個結構體數組的佔多少個字節,將這個大小放進去(是個宏定義,便於複用)
    // # define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
    // registerNativeMethods 具體實如今AndroidRuntime.cpp 具體見下一段代碼
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}

// 這邊重寫了jni.h聲明的 JNI_OnLoad方法,在JNI_OnLoad中進行註冊(register_android_media_MediaPlayer),在註冊過程當中,聲明瞭一個gMethods的結構體數組,這裏面寫好了方法映射。而JNI_OnLoad的調用處,就是System.loadLibrary 的時候會走到這裏,而後進行動態註冊
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);

    ...
    // register_android_media_MediaPlayer 在這邊被調用
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }
    ...

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}
複製代碼

image-20181225144805278

https://android.googlesource.com/platform/libnativehelper/+/jb-mr1.1-dev-plus-aosp/JNIHelp.cpp#71

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    ALOGV("Registering %s's %d native methods...", className, numMethods);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        char* msg;
        asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
        e->FatalError(msg);
    }
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* msg;
        asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        e->FatalError(msg);
    }
    return 0;
}
複製代碼

JNI函數總結

函數 Java 數組類型 本地類型 說明
GetBooleanArrayElements jbooleanArray jboolean ReleaseBooleanArrayElements 釋放
GetByteArrayElements jbyteArray jbyte ReleaseByteArrayElements 釋放
GetCharArrayElements jcharArray jchar ReleaseShortArrayElements 釋放
GetShortArrayElements jshortArray jshort ReleaseBooleanArrayElements 釋放
GetIntArrayElements jintArray jint ReleaseIntArrayElements 釋放
GetLongArrayElements jlongArray jlong ReleaseLongArrayElements 釋放
GetFloatArrayElements jfloatArray jfloat ReleaseFloatArrayElements 釋放
GetDoubleArrayElements jdoubleArray jdouble ReleaseDoubleArrayElements 釋放
GetObjectArrayElement 自定義對象 object
SetObjectArrayElement 自定義對象 object
GetArrayLength 獲取數組大小
NewArray 建立一個指定長度的原始數據類型的數組
GetPrimitiveArrayCritical 獲得指向原始數據類型內容的指針,該方法可能使垃圾回收不能執行,該方法可能返回數組的拷貝,所以必須釋放此資源。
ReleasePrimitiveArrayCritical 釋放指向原始數據類型內容的指針,該方法可能使垃圾回收不能執行,該方法可能返回數組的拷貝,所以必須釋放此資源。
NewStringUTF jstring類型的方法轉換
GetStringUTFChars jstring類型的方法轉換
DefineClass 從原始類數據的緩衝區中加載類
FindClass 該函數用於加載本地定義的類。它將搜索由CLASSPATH 環境變量爲具備指定名稱的類所指定的目錄和 zip文件
GetObjectClass 經過對象獲取這個類。該函數比較簡單,惟一注意的是對象不能爲NULL,不然獲取的class確定返回也爲NULL
GetSuperclass 獲取父類或者說超類 。 若是 clazz 表明類class而非類 object,則該函數返回由 clazz 所指定的類的超類。 若是 clazz指定類 object 或表明某個接口,則該函數返回NULL
IsAssignableFrom 肯定 clazz1 的對象是否可安全地強制轉換爲clazz2
Throw 拋出 java.lang.Throwable 對象
ThrowNew 利用指定類的消息(由 message 指定)構造異常對象並拋出該異常
ExceptionOccurred 肯定是否某個異常正被拋出。在平臺相關代碼調用 ExceptionClear() 或 Java 代碼處理該異常前,異常將始終保持拋出狀態
ExceptionDescribe 將異常及堆棧的回溯輸出到系統錯誤報告信道(例如 stderr)。該例程可便利調試操做
ExceptionClear 清除當前拋出的任何異常。若是當前無異常,則此例程不產生任何效果
FatalError 拋出致命錯誤而且不但願虛擬機進行修復。該函數無返回值
NewGlobalRef 建立 obj 參數所引用對象的新全局引用。obj 參數既能夠是全局引用,也能夠是局部引用。全局引用經過調用DeleteGlobalRef() 來顯式撤消。
DeleteGlobalRef 刪除 globalRef 所指向的全局引用
DeleteLocalRef 刪除 localRef所指向的局部引用
AllocObject 分配新 Java 對象而不調用該對象的任何構造函數。返回該對象的引用。clazz 參數務必不要引用數組類。
getObjectClass 返回對象的類
IsSameObject 測試兩個引用是否引用同一 Java 對象
NewString 利用 Unicode 字符數組構造新的 java.lang.String 對象
GetStringLength 返回 Java 字符串的長度(Unicode 字符數)
GetStringChars 返回指向字符串的 Unicode 字符數組的指針。該指針在調用 ReleaseStringchars() 前一直有效
ReleaseStringChars 通知虛擬機平臺相關代碼無需再訪問 chars。參數chars 是一個指針,可經過 GetStringChars() 從 string 得到
NewStringUTF 利用 UTF-8 字符數組構造新 java.lang.String 對象
GetStringUTFLength 以字節爲單位返回字符串的 UTF-8 長度
GetStringUTFChars 返回指向字符串的 UTF-8 字符數組的指針。該數組在被ReleaseStringUTFChars() 釋放前將一直有效
ReleaseStringUTFChars 通知虛擬機平臺相關代碼無需再訪問 utf。utf 參數是一個指針,可利用 GetStringUTFChars() 得到
NewObjectArray 構造新的數組,它將保存類 elementClass 中的對象。全部元素初始值均設爲 initialElement
SetArrayRegion 將基本類型數組的某一區域從緩衝區中複製回來的一組函數
GetFieldID 返回類的實例(非靜態)域的屬性 ID。該域由其名稱及簽名指定。訪問器函數的 GetField 及 SetField系列使用域 ID 檢索對象域。GetFieldID() 不能用於獲取數組的長度域。應使用GetArrayLength()。
GetField 該訪問器例程系列返回對象的實例(非靜態)域的值。要訪問的域由經過調用GetFieldID() 而獲得的域 ID 指定。
SetField 該訪問器例程系列設置對象的實例(非靜態)屬性的值。要訪問的屬性由經過調用 SetFieldID() 而獲得的屬性 ID指定。
GetStaticFieldID GetStaticField SetStaticField 同上,只不過是靜態屬性。
GetMethodID 返回類或接口實例(非靜態)方法的方法 ID。方法可在某個 clazz 的超類中定義,也可從 clazz 繼承。該方法由其名稱和簽名決定。 GetMethodID() 可以使未初始化的類初始化。要得到構造函數的方法 ID,應將 做爲方法名,同時將void (V) 做爲返回類型。
CallVoidMethod
CallObjectMethod
CallBooleanMethod
CallByteMethod
CallCharMethod
CallShortMethod
CallIntMethod
CallLongMethod
CallFloatMethod
CallDoubleMethod
GetStaticMethodID 調用靜態方法
CallMethod
RegisterNatives 向 clazz 參數指定的類註冊本地方法。methods 參數將指定 JNINativeMethod 結構的數組,其中包含本地方法的名稱、簽名和函數指針。nMethods 參數將指定數組中的本地方法數。
UnregisterNatives 取消註冊類的本地方法。類將返回到連接或註冊了本地方法函數前的狀態。該函數不該在常規平臺相關代碼中使用。相反,它能夠爲某些程序提供一種從新加載和從新連接本地庫的途徑。

JNI字符串、數組處理總結

咱們來完成一個小的案列來講明知識點吧。咱們在Java層常用AES加密算法,服務器、移動端都須要作一套算法,那咱們就能夠利用JNI來實現一套,給多端調用。閒話很少說,直接上代碼。

下面的是JNIUtils類,主要完成so加載、AES算法初始化等等。關注幾個靜態native函數。

public class JNIUtils {
    public static native String getStringFormC(); //演示從C++獲取字符串
    public static native byte[] getKeyValue();//用於AES加密的密鑰也是從C++獲取字節數組
    public static native byte[] getIv();//使用CBC模式,須要一個向量iv,可增長加密算法的強度

    //-----演示字符串用法-------
    public native static String sayHello(String text);


    private static byte[]keyValue;
    private static byte[]iv;

    private static SecretKey key;
    private static AlgorithmParameterSpec paramSpec;
    private static Cipher ecipher;

    static {
        //加載動態庫
        System.loadLibrary("native-lib");
        //配置AES算法 初始化 這裏能夠在網上查閱下AES相關的資料
        keyValue = getKeyValue();
        iv = getIv();
        if(null != keyValue && null !=iv) {
            KeyGenerator kgen;
            try {
                kgen = KeyGenerator.getInstance("AES");
                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
                random.setSeed(keyValue);
                kgen.init(128,random);
                key =kgen.generateKey();
                paramSpec =new IvParameterSpec(iv);
                ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
                e.printStackTrace();
            }
        }
    }
    /**加密**/
    public static String encode(String msg) {
        String str ="";
        try {
            //用密鑰和一組算法參數初始化此 cipher
            ecipher.init(Cipher.ENCRYPT_MODE,key,paramSpec);
            //加密並轉換成16進制字符串
            str = asHex(ecipher.doFinal(msg.getBytes()));
        } catch (BadPaddingException | InvalidAlgorithmParameterException
                | InvalidKeyException | IllegalBlockSizeException ignored) {
        }
        return str;
    }
    /**解密函數**/
    public static String decode(String value) {
        try {
            ecipher.init(Cipher.DECRYPT_MODE,key,paramSpec);
            return new String(ecipher.doFinal(asBin(value)));
        } catch (BadPaddingException | InvalidKeyException | IllegalBlockSizeException
                | InvalidAlgorithmParameterException ignored) {
        }
        return"";
    }
    /**轉16進制**/
    private static String asHex(byte buf[]) {
        StringBuffer strbuf =new StringBuffer(buf.length * 2);
        int i;
        for (i = 0;i <buf.length;i++) {
            if (((int)buf[i] & 0xff) < 0x10)//小於十前面補零
                strbuf.append("0");
            strbuf.append(Long.toString((int)buf[i] & 0xff, 16));
        }
        return strbuf.toString();
    }
    /**轉2進制**/
    private static byte[] asBin(String src) {
        if (src.length() < 1)
            return null;
        byte[]encrypted =new byte[src.length() / 2];
        for (int i = 0;i <src.length() / 2;i++) {
            int high = Integer.parseInt(src.substring(i * 2, i * 2 + 1), 16);//取高位字節
            int low = Integer.parseInt(src.substring(i * 2 + 1, i * 2 + 2), 16);//取低位字節
            encrypted[i] = (byte) (high * 16 +low);
        }
        return encrypted;
    }
複製代碼

咱們在來看下關鍵的C++核心代碼:

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

#define LOG "ndk-jni" // 這個是自定義的LOG的標識
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定義LOGD類型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定義LOGI類型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定義LOGW類型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定義LOGE類型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定義LOGF類型

const char keyValue[] = {  //祕鑰key
        21, 25, 21, 45, 25, 98, 55, 45, 10, 35, 45, 35,
        26, 5, 25, 65, 78, 99, 85, 45, 5, 10, 0, 11,
        35, 48, 98, 65, 32, 14, 67, 25, 36, 56, 45, 5,
        12, 15, 35, 15, 25, 14, 62, 25, 33, 45, 55, 12, 8
};

const char iv[] =  {    //16bit 加強的iv向量,用於AES算法加強
        33, 32, 25, 25, 35, 27, 55, 12, 15,32,
        23, 45, 26, 32, 5,16
};


extern "C"
JNIEXPORT jstring JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getStringFormC(JNIEnv *env, jclass type) {
	//NewStringUTF
    return env->NewStringUTF("這是來自C++的原始字符串");
}

/*** * 獲取c++裏面的數組jbyte */
extern "C"
JNIEXPORT jbyteArray JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getKeyValue(JNIEnv *env, jclass type) {
    jbyteArray kv = env->NewByteArray(sizeof(keyValue));
    jbyte* bytes = env->GetByteArrayElements(kv,0);

    int i=0;
    for (i;i < sizeof(keyValue); ++i) {
        bytes[i] = keyValue[i];
    }
    env->SetByteArrayRegion(kv,0, sizeof(keyValue),bytes);
    env->ReleaseByteArrayElements(kv,bytes,0);
    return kv;

}

extern "C"
JNIEXPORT jbyteArray JNICALL Java_ndk_jesson_com_ndkdemo_JNIUtils_getIv(JNIEnv *env, jclass type) {

    jbyteArray ivArray = env->NewByteArray(sizeof(iv));
    jbyte *bytes = env->GetByteArrayElements(ivArray, 0);

    int i;
    for (i = 0; i < sizeof(iv); i++){
        bytes[i] = (jbyte)iv[i];
    }

    env->SetByteArrayRegion(ivArray, 0, sizeof(iv), bytes);
    env->ReleaseByteArrayElements(ivArray,bytes,0);
    return ivArray;
}
複製代碼

咱們在來重點看下下面幾個函數:

image-20181227142614253

在jni.h裏面有數組定義,jsize代碼數組的大小(typedef jint jsize;)。那麼下面的幾個函數你必定要熟悉:

jbyteArray (*NewBooleanArray)(JNIEnv*, jsize); //初始化數組
jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);//獲取數組元素
void (*SetCharArrayRegion)(JNIEnv*, jcharArray, jsize, jsize, const jchar*);//設置數組數據
void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray, jbyte*, jint);//釋放數組佔用的內存資源
複製代碼

總結一下:在AES的例子中,實現AES是用的Java SDK API。我演示的C++的代碼主要是爲了演示數組的上面的幾個函數的使用。但願經過這篇文章,讓你真正的瞭解了JNI的一些初步的概念,後續我會繼續演示JNI的相關重點用法,包括字符串、異常、多線程。歡迎你們繼續關注個人NDK開發系列文章。

相關文章
相關標籤/搜索