Java JNI接口的詳細描述

當前隨著移動設備、大數據以及人工智能的蓬勃發展,我們設計出的App也好或者其他程序也罷對于CPU性能的要求也是越來越高,因此對于Java開發者而言,現在也難免需要用到更偏向硬件底層的C語言。Java語言發展到現在已經經歷了20多年,其語言框架本身已經非常成熟,而且整個生態都保持得非常好,因而再與底層的C、甚至匯編進行輔助的話,那就能釋放出更強大的威力來。而Java要與本地底層代碼進行交互,則需要通過 JNIJava Native Interface)接口。

Oracle官方的JNI說明文檔在此(基于Java SE 10):https://docs.oracle.com/javase/10/docs/specs/jni/index.html


環境配置

筆者此前已經寫過一篇博文對于JNI的一個初步使用方式,原文為:《 Java JNI的使用基礎》。而本篇博文將基于Android開發環境,對JNI接口做更深入詳細地介紹。如果各位想了解其他平臺如何編譯構建動態庫的話可以參考《C語言編程魔法書》

本博文所基于的開發環境為Android Studio 3.1.4,采用Java 8語言。而底層的C語言部分則使用的是android-ndk-r16b,基于Clang 5.0編譯工具鏈。

我們要在自己所創建的Android項目工程中使用JNI訪問底層C語言代碼,則需要做一些準備工作。比如,我們需要在項目工程中的app文件夾中創建一個名為jni的文件夾,然后在里面需要至少創建三個文件——一個是Android.mk,一個是Application.mk,還有一個則是自己所定制的C源文件。當然,如果需要的話還可以增加其他C源文件或者是匯編源文件等。筆者為了能跟各位清晰地展示代碼demo,這里就創建了一個名為test.c的C源文件。

Android.mk文件類似于一個makefile文件,它對我們當前JNI項目包做一些編譯配置,包括導入哪些其他庫文件,輸出的動態庫文件名叫啥,哪些源文件參與編譯等。該文件內容可參考以下代碼:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni_test

LOCAL_SRC_FILES := test.c

LOCAL_STATIC_LIBRARIES := cpufeatures

LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

$(call import-module, android/cpufeatures)

各位可以看到,這里整個JNI工程將會輸出jni_test這一動態庫模塊,而動態庫文件名則為:libjni_test.so。此外,這里還引入了cpufeatures這個庫,這個各位可以暫時不用管,反正編譯進去也問題不大,這個庫的內容很小。
而Application.mk則是對當前JNI所生成目標的整體配置,包括C語言的整體編譯選項、輸出目標處理器架構等。下面是筆者所寫的Application.mk文件內容,各位可以參考:

# Build all available machine code.
APP_ABI := all
APP_CFLAGS += -std=gnu11 -Os

上述代碼表示構建所有當前NDK所支持的處理器架構目標動態庫。而在C語言編譯選項上則使用最新的GNU11標準,并且使用Os(最小尺寸,速度最快)的優化選項。
接著,我們就可以實現tes.t源文件的內容了。

隨后,我們在Android項目工程中,需要給build.gradle(Module: app)添加上sourceSets配置,否則使用ndk-build完所生成的庫加載不到當前的項目工程中。添加完的內容如下所示:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.test.zenny_chen.test"
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:28.0.0-rc02'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

各位只需關注sourceSets部分即可。


Java與本地代碼的橋接

Java端是如何調用JNI本地代碼的呢?Java是一門完全基于類的編程語言,當某個類中包含調用本地代碼的方法,那么在訪問該類時就需要加載相應的動態庫(在Windows系統中是dll文件;在macOS中是dylib文件;在其他類Unix系統中則是so文件)。然后,對于實現在JNI側完成的方法,需要顯式地使用native關鍵字進行聲明,指明在調用該方法時需要在剛才所加載的動態庫中去找。因此,我們這個demo中,Java側的類如以下代碼所示:

package com.test.zenny_chen.test;

/**
 * 我們定制的JNI測試類
 */
public class MyJNIClass {

    static {
        // 在訪問MyJNIClass時,加載jni_test動態庫
        System.loadLibrary("jni_test");
    }

    /**
     * 聲明一個實例方法,它在JNI中實現
     * @param i 指定一個整數參數
     * @return 某一整數
     */
    public native int nativeInstanceMethod(int i);

    /**
     * 聲明一個類方法,它在JNI中實現
     * @param s 指定一個字符串
     */
    public native static void nativeStaticMethod(String s);

    /**
     * 當前類的實例方法,將指定參數的值加1然后返回
     * @param i 指定的一個整數
     * @return 參數加1后的值
     */
    public int increasedInt(int i) {
        return i + 1;
    }

    /**
     * 當前類的類方法,用于實現打印輸出指定的字符串
     * @param s 指定的字符串內容
     */
    public static void print(String s) {
        System.out.println(s);
    }

    /** 當前類的一個實例屬性 */
    public int mField;

    /** 當前類的一個類屬性 */
    public static int cField;
}

上述代碼完整展示了一個com.test.zenny_chen.test.MyJNIClass類。它在被訪問時就會自動加載jni_test這一動態庫。然后,nativeInstanceMethod 是一個MyJNIClass類的實例方法,其實現在JNI側完成。nativeStaticMethod 則是一個類方法,其實現也是在JNI側完成。

有了Java端的方法聲明,那么當這些JNI方法被調用時,JVM是如何去找這些方法的實現的呢?這就需要本地代碼的符號命名與Java端有一套約定成俗的規則。在JNI側我們需要一套命名法則使得當前函數在動態庫中能被JVM找到。這套規則其實也不復雜,基本遵循以下幾條:

  1. Java_ 打頭,表示這是一個能被JVM識別的在JNI端實現的全局函數。
  2. 必須具體指明當前函數所實現的是具體哪個包里的哪個類中的哪個方法。對于包名之間以及包名與類名之間的分隔符由 . 改為了 _ 單下劃線,因為 . 點符號不是一個有效的標識符字符。而對于包名或類名中已經含有一條下劃線的,則在該下劃線后面加一個數字,即 _1 進行區分。比如,com.test.zenny_chen.test.MyJNIClass類作為C函數名,則可表示為:com_test_zenny_1chen_test_MyJNIClass。
  3. 最后,將上面兩條拼接起來,以形成完整的函數名。
    比如,MyJNIClass類中的nativeInstanceMethod實例方法,在JNI中所對應的全局函數名就應該為:Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod。這樣,函數名就確定了。

由于我們在JNI側最重要生成的是動態庫,因此我們需要遵循各個系統平臺對動態庫輸出符號的聲明規則。比如在Windows平臺,動態庫中允許被外部動態加載的符號需要用__declspec(dllexport)進行聲明,而對于i386架構的處理器,還需要用__stdcall函數調用約定等等。因此在<jni.h>頭文件中為了兼容各個平臺對于動態庫輸出符號的聲明,用了一些宏:

  1. JNIEXPORT:表示需要輸出給外部程序進行動態加載訪問的說明符。
  2. JNICALL:表示可被JVM調用的,遵循JNI調用約定的函數說明符。
    因此整個JNI側的Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函數的聲明如下:
/// 這個函數與com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod這一實例方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param instance 表示對當前調用此實例方法的MyJNIClass對象實例的引用
/// @param i 調用此實例方法時所傳入的整型參數
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)

盡管在安卓系統上,JNIEXPORT、JNICALL這兩個宏可以完全缺省,但出于跨平臺性的考慮,筆者這里加上能給各位一個更完整的認知。

下面先介紹一下參數。env這個參數表示JVM在調用此JNI函數時所傳入的當前JNI環境的句柄handle)。后面對Java層的類、屬性以及方法的訪問都需要借助這個句柄。
instance這個參數表示當前調用nativeInstanceMethod方法的對象引用。
參數i表示在Java端調用nativeInstanceMethod方法時所傳入的參數值。
這個方法返回一個整數值。
對于實例方法,instance參數指向調用當前方法的對象的引用;而對于類方法,也包含此參數,它指向當前類本身,在JNI側就是用一個jclass對象來表示的。因此,無論是類方法還是實例方法,第一個參數總是JNIEnv* env。而對于第二個參數,如果是實例方法,那么對應的是jobject instance;如果是類方法,那么對應的是jclass cls,當然,我們下面會看到jclass其實是屬于jobject的子類型,所以jclass是兼容于jobject的。從后面開始則是Java端該方法自己的參數列表了,如果參數列表為空,則在JNI層就直接這兩個參數。

從上面我們看到Java層映射到JNI層的類型,在Java層原本用int類型的,在JNI層則是用jint來表示。JNI規范約定了以下這些基本類型與Java端相對應。

1.png

以上所表示的都是Java中的基本類型(即值類型)到JNI層的映射,除此之外的類型不是類類型就是對象類型,即都屬于引用類型。類類型用jclass表示;對象類型則是用jobject來表示。為了方便對一些常用的對象類型進行操作,JNI側還約定了以下這些jobject的子類型:

3.png

我們可以看到,其實jclass類型也屬于jobject類型的子類型。而事實也是如此,在Java中Class類的聲明如下:

public final class Class<T> extends Object

另外,我們還看到了JNI層對Java的數組支持得非常完整。盡管Java中的數組是一種比較特殊的表現方式,但就其類型而言仍然是屬于Object的子類類型,并且數組也是一個引用類型,我們可以看以下代碼:

Object obj = new int[]{1, 2, 3};

關于Java數組類型映射到JNI的類型,這里舉些例子進行說明。比如,Java端的int[]類型對應于JNI側的jintArray;Java端的char[]類型對應于JNI側的jcharArray;Java端的String[]類型則對應于JNI側的jobjectArray類型。
有了這些類型之后,我們就可以在JNI側對Java層的類、方法與屬性進行交互訪問了。

有了類型之間的映射關系之后還不夠,因為我們知道Java中的方法是可被重載的overloadable),因此為了要準確描述一個方法,既要獲得該方法的種類(類方法還是實例方法),還要獲得它的名稱(即方法標識符)以及類型(包括參數類型以及返回類型)。在JNI層可以通過后續要描述的接口來指定訪問的是類方法還是實例方法。而要表示方法或屬性的類型,JNI層提供了一套類型簽名type signature)機制,如下圖表示。

2.png

我們下面來舉一些例子。Java端的 long 類型,其簽名為:J;Java端的 String 類型,其簽名為:Ljava/lang/String;,注意這里的前綴大寫字母 L,最后的分號也注意別漏,簽名中包名與類名的分隔符用的是 / 符號。Java端的 short[] 類型,其簽名為:[S;Java中的 String[] 類型,其簽名為:[Ljava/lang/String;
而對于Java的方法類型簽名,則需要其完整的參數類型與返回類型。如果沒有參數列表,則直接用 () 來表示。為了清晰描述方法類型的簽名機制,這里采用類Swift編程語言的類型表達方式,Kotlin也同樣如此。比如,Java端的 () -> void 類型,其簽名為:()V;Java端的 (int) -> void 類型,其簽名為:(I)V;Java端的 (int, boolean) -> String 類型,其簽名為:(IZ)Ljava/lang/String;;Java端的 (int, String, int) -> int[] 類型,其簽名為:(ILjava/lang/String;I)[I

有了上述這些知識之后,我們下面來先寫一個最最簡單的JNI側C代碼的例子。我們可以將下面的代碼粘貼到test.c中去:

#include <jni.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    return i + 100;
}

隨后,我們可以在Activity中添加以下代碼來觀察結果:

        MyJNIClass jniCls = new MyJNIClass();
        int value = jniCls.nativeInstanceMethod(10);
        System.out.println("value = " + value);

這樣,Java端調用JNI層的整個邏輯就完成了。


在JNI層訪問Java類、屬性以及方法

如果我們在JNI層去訪問Java層的屬性或方法,需要進行以下三步驟:

  1. 找到所要訪問的屬性或方法所屬的類
  2. 獲得屬性或方法的ID
  3. 訪問屬性或調用方法

這些步驟所牽涉到的JNI層的接口都需要通過JNIEnv句柄去訪問。

在JNI層獲取Java類對象

要獲得一個指定的類,需要通過FindClass這一接口。這個接口有兩個參數,第一個參數就是env句柄;第二個參數是一個C字符串,用于描述該類的全名。與類型簽名一樣,類的全名在這里也需要將包名與類名寫完整,并且其中的 . 符號需要用 / 符號來代替。FindClass這一接口所返回的對象則是一個jclass類型的對象。
比如,要獲得一個Java層String的類,我們可以這么做:

jclass cls = (*env)->FindClass(env, "java/lang/String;");

那么我們要獲取本例子中的MyJNIClass類,就需要這么做:

jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");


在JNI層獲取屬性ID

Java中有類屬性與實例屬性這兩類,所以要獲取屬性ID的接口也有兩套。下面先介紹一下獲取實例屬性ID的接口:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

這里,clazz參數就是我們之前獲得的jclass對象。name參數指定了實例屬性的名稱。sig參數指定了該實例屬性的類型簽名。該接口返回一個jfieldID的對象,用于標識此特定的實例屬性。
如果我們要獲取MyJNIClass類的mField這一實例屬性,可以這么做:

jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");

而要獲得類屬性的JNI接口是:

jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

我們可以看到,該接口的參數列表以及返回類型與GetFieldID都完全一樣,指示接口名不一樣而已。所以在用法上也完全一樣。各位在獲取屬性的時候一定要注意,該屬性是類屬性還是實例屬性,必須調用針對性的接口,不能用錯。


訪問屬性

訪問屬性有兩種模式,一種是讀屬性,還有一種就是寫屬性,類似于我們在Java中常用的getter方法與setter方法。無論是讀屬性還是寫屬性,根據該屬性是類屬性還是實例屬性,也各分為兩套。下面我們先討論實例屬性的讀寫方法。


訪問對象實例屬性

對象實例屬性的讀方法接口形式如下:

Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);

其中,<type>根據屬性不同的類型而有所不同。obj參數就是在Java端調用此方法的對象引用。fieldID參數就是我們剛才獲取到的此屬性的ID。
下面列出官方給出的實例屬性的讀接口列表:

屏幕快照 2018-09-27 下午5.11.36.png

比如,在本demo中,如果我們要MyJNIClass對象中的mField實例方法,則可用這么用:

int value = (*env)->GetIntField(env, instance, fieldID);

實例屬性的寫方法接口形式如下:

void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

這里,<type>根據屬性不同的類型而有所不同。obj參數就是在Java端調用此方法的對象引用。fieldID參數就是我們剛才獲取到的此屬性的ID。NativeType則是<type>所對應的JNI側的類型。參數value就是要給該屬性所設置的值。
下面列出官方給出的實例屬性的寫接口列表:

屏幕快照 2018-09-27 下午6.22.55.png

比如,在本demo中,如果我們要MyJNIClass對象中的mField實例方法,則可用這么用:

(*env)->SetIntField(env, instance, fieldID, 10);

上述代碼就是將instance所引用對象的mField實例屬性賦值為10。
根據上面所描述的對mField實例屬性的讀寫方法,我們改造一下Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函數,各位可以運行一下看下效果:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField實例屬性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 獲取當前mField實例屬性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后對mField實例屬性進行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);

    return value + 10;
}

輸入完之后,我們用ndk-build命令重新編譯構建。隨后,在Java端的Activity中填寫以下代碼:

        MyJNIClass jniCls = new MyJNIClass();
        int value = jniCls.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + jniCls.mField);

重新運行后我們就能看到新的結果了。


訪問類屬性

下面再來談談JNI訪問Java層的類屬性的接口。首先介紹讀類屬性的接口,其形式如下:

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);

這里,clazz參數是我們之前通過FindClass接口所獲得的該類屬性所屬的類在JNI側的對象。fieldID參數則是我們之前通過GetStaticFieldID接口所獲得的指定類屬性的ID。該接口返回的是一個NativeType類型,它是<type>在JNI側所對應的一個類型。下面列出官方給出的關于此接口的所有函數列表:

屏幕快照 2018-09-27 下午7.54.20.png

比如,在本demo中,如果我們要MyJNIClass類的cField類屬性,可以使用以下代碼:

jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
int value = (*env)->GetStaticIntField(env, cls, fieldID);

上述代碼片段中,cls就是通過FindClass接口所找到的MyJNIClass類在JNI側所對應的類對象。

然后,寫類屬性的接口,其形式如下:

void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);

下面列出官方給出的關于此接口的所有函數列表:

屏幕快照 2018-09-27 下午8.17.35.png

比如,在本demo中,如果我們要MyJNIClass類的cField類屬性,可以使用以下代碼:

(*env)->SetStaticIntField(env, cls, fieldID, 10);

以上代碼片段就是將MyJNIClass類的cField類屬性賦值為10。
這么一來,我們可以再整合一下Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod,把類屬性的讀寫也放進去:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField實例屬性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 獲取當前mField實例屬性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后對mField實例屬性進行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 獲取類屬性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 獲取當前類屬性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);

    return value + 100;
}

此外,在Activity中也可以把MyJNIClass類的cField類屬性的值也進行輸出,便于觀察:

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);


JNI中的方法調用

在Java中,方法與屬性類似也分為兩大類:一類是類方法,另一類是實例方法。與訪問屬性的步驟類似,我們要調用一個方法之前,首先需要獲得該方法的ID,隨后再用該ID去做方法調用。我們下面先討論調用Java對象的實例方法。


調用對象的實例方法

首先我們先看一下如何獲得實例方法的ID,該接口的形式如下:

jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

該接口的參數與獲取實例屬性的一樣,這里不再贅述。這個接口所返回的就是一個方法ID,以標識當前所獲取的方法在JNI中的表示。
在本demo中,如果我們要獲取MyJNIClassincreasedInt實例方法的ID,那么可以用以下方式:

    // 獲取MyJNIClass類的increasedInt實例方法;其類型為:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");

獲取了方法ID之后我們就可以去調用此方法了。調用實例方法的接口如下:

NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);

這其中的<type>與訪問屬性所用的<type一致,各位可以參考上面。這里后面的...是C語言的不定參數列表,表示對應于Java層實例方法的參數。如果Java層的實例方法沒有參數,則不填任何東西,如果有參數,則依次對應填進去即可。該接口的返回類型對應于Java層實例方法的返回類型。
在本demo中,如果我們要調用MyJNIClassincreasedInt實例方法,那么可以用以下形式:

    // 調用this對象的increasedInt實例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);

至此,我可以再把Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函數補充完整:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField實例屬性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 獲取當前mField實例屬性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后對mField實例屬性進行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 獲取類屬性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 獲取當前類屬性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 獲取MyJNIClass類的increasedInt實例方法;其類型為:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 調用this對象的increasedInt實例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);

    return value + 100;
}

各位可以查看整個app的運行結果。

獲取類方法的方法ID的接口如下描述:

jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

此方法的參數以及返回類型跟獲取實例方法ID的接口一樣。
在本例中,我們要獲取MyJNIClass中的print類方法的ID,可以用以下方式:

    // 找到print類方法;其類型為:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");


調用類方法

在JNI側對類方法的調用與對實例方法的調用形式差不多,采用以下接口:

NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

其參數與返回類型與實例方法基本一樣,除了第二個參數。這里第二個參數是調用當前Java類方法的類類型所在JNI側的對象。
在本demo中,我們要調用MyJNIClass類的print方法如下所示:

/// 在JNI側的打印函數
/// @param 指定的要打印輸出的C字符串
static void JNIPrint(JNIEnv* env, const char *s)
{
    if(s == NULL)
        return;
    
    // 將C字符串轉換為Java字符串對象
    jstring jstr = (*env)->NewStringUTF(env, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print類方法;其類型為:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");
    
    // 調用print這一類方法
    (*env)->CallStaticVoidMethod(env, cls, methodID, jstr);
}

我們在這里定義了一個可在JNI側進行控制臺打印輸出的函數JNIPrint。我們可以在Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函數以及Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函數中均可調用此函數。下面我們將Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函數補充完整,順便再給出Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函數的實現。

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod這一成員方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param instance 表示對當前調用此實例方法的MyJNIClass對象實例的引用
/// @param i 調用此實例方法時所傳入的整型參數
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField實例屬性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 獲取當前mField實例屬性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后對mField實例屬性進行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 獲取類屬性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 獲取當前類屬性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 獲取MyJNIClass類的increasedInt實例方法;其類型為:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 調用this對象的increasedInt實例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);
    
    char strBuffer[128];
    sprintf(strBuffer, "native value is: %d\n", value);
    NativePrint(strBuffer);
    
    return value + 100;
}

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.nativeStaticMethod這一類方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param cls 指向在Java層調用此方法類方法的類類型
/// @param js 調用此類方法時所傳入的Java字符串對象
JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    size_t length = strlen(cs);
    
    // 獲取類屬性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
    
    char buffer[128];
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    JNIPrint(env, buffer);
    (*env)->ReleaseStringUTFChars(env, js, cs);
}

下面我們再調整一下Activity中的Java代碼,來觀察修改后的結果輸出:

        MyJNIClass.nativeStaticMethod("Hello, world!");

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);

這樣一來,我們就把基本的JNI屬性與方法的基本操作接口都講解完了,下面我們將再介紹一下JNI側對Java層的字符串操作以及數組操作方式。


字符串操作

眾所周知,Java中所用的字符串編碼格式為Unicode。由于Java誕生地非常早,1995年就發布了第一個版本,那個時候Unicode標準才剛啟動沒多久,所以就其編碼格式名稱而言一直把“Unicode”這個稱呼沿用至今。但是對于現代化的Unicode標準早就引入了若干細分編碼形式,最常用的是UTF-8、UTF-16以及UTF-32,尤其是前兩者幾乎被各大系統以及所有網頁所支持。之前較老的Java版本支持UCS-2編碼格式,現在默認情況下都直接使用了UTF-16編碼,而UCS-2是UTF-16的一個子集,坐落于Unicode碼點表中的基本多語言平面Basic Multilingual Plane)中。下面我們就證明一下當前就Java SE 8而言,默認采用的是UTF-16編碼格式:

        String emojiStr = "??";
        String fs = String.format("First code is: %1$4X, second code is: %2$4X",
                (int)emojiStr.charAt(0), (int)emojiStr.charAt(1));
        // 輸出:First code is: D83D, second code is: DE04
        System.out.println(fs);

筆者用的系統是macOS 10.14 Mojave,Android 9.0系統的模擬器。上述的一個Emoji表情??,其Unicode碼點值為0x1F604,位于增補多語言平面Supplementary Multilingual Plane)之中。它所對應的UTF-16編碼,高16位是0xD83D,低16位是0xDE04。所以各位對于Java所采用的字符串編碼有一定了解之后,再來看JNI側如何對Java層的字符串進行操作就可以有更好地把握了。

我們先介紹一下獲取Java字符串內容。JNI API主要提供了獲取UTF-16與UTF-8這兩種字符編碼的字符串的接口。要獲取UTF-16字符編碼的字符串使用GetStringChars()這個接口,其形式如下:

const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);

此接口第二個參數string就表示Java層傳遞過來的Java字符串對象。第三個參數isCopy是一個暗示性參數,可以為空。如果不空,那么我們可以先定義一個變量,指示當前獲取字符串的形式是使用拷貝方式還是非拷貝方式。如果是JNI_TRUE,則指示使用拷貝方式;JNI_FALSE則指示使用非拷貝方式。但實際是否用拷貝方式,我們在調用完此接口之后還需要通過所傳入的實參去查看。
該接口返回一個指向UTF-16編碼格式的字符串,其中jchar類型在之前類型對照表中也列出來過,表示無符號16位整數類型。

由于此接口所返回的存放字符串的緩存是通過Java虛擬機來分配的,因此當我們使用完這組字符串之后需要調用ReleaseStringChars接口去釋放。該接口聲明如下:

void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);

其中,第二個參數為之前所獲取的Java字符串對象;第三個參數為之前所返回的Java字符串緩存。

如果我們要獲取一個Java字符串的長度,在JNI側提供了GetStringLength這一接口,其聲明如下:

jsize GetStringLength(JNIEnv *env, jstring string);

所以有了這個接口之后,即便我們當前使用的C語言不含Unicode庫的,也能獲取當前的字符串長度。

下面我們就來舉一個綜合性的例子:

JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16編碼形式獲取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 獲取當前字符串的長度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后進行釋放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 輸出結果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
}

我們修改了Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函數。隨后,我們在Activity中將原本傳入的字符串內容改為"??",即可查看到運行結果。這里我們可以看到,一個??Emoji字符占用2個字符。

下面我們介紹一下獲取UTF-8編碼格式字符串的接口。該接口形式如下:

const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

該接口的三個參數與上面所描述的獲得UTF-16字符串接口的三個參數一樣。該接口返回一個存放標準的C字符串的緩存首地址。

同樣,獲取UTF-8字符串的接口也對應有一個釋放字符串的接口,其形式如下:

void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);

我們在使用完GetStringUTFChars所創建的字符串緩存之后需要調用此接口進行釋放。

JNI側也提供了GetStringUTFLength接口用于獲取指定Java字符串以UTF-8編碼格式所表示的字符串的長度。其形式如下:

jsize GetStringUTFLength(JNIEnv *env, jstring string);

當然,如果字符串長度不太長的話,我們用C語言標準庫的strlen函數在性能上會更高些。

下面我們來舉一個綜合性的例子:

JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16編碼形式獲取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 獲取當前字符串的長度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后進行釋放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 輸出結果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
    
    // 獲取UTF-8字符串
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    
    // 獲取當前以UTF-8編碼所表示的字符串的長度
    length = (*env)->GetStringUTFLength(env, js);
    
    // 組織所要打印c輸出的字符串
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    
    // 釋放UTF-8字符串緩存
    (*env)->ReleaseStringUTFChars(env, js, cs);
    
    // 打印輸出:The input string is: ??, and the length is: 4
    JNIPrint(env, buffer);
    
    // 獲取類屬性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
}

我們可以運行一下程序查看結果。


下面我們來談談如何在JNI層將一個UTF-16字符數組來創建一個Java字符串對象。JNI API提供了NewString接口來實現這個操作,其形式如下:

jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);

其中,第二個參數unicodeChars表示該我們所傳入的UTF-16字符數組的緩存首地址;第三個參數len用于指示該緩存中有多少個字符參與構建。

當然,JNI層也提供了接口,用于將一個指定的UTF-8字符數組來創建一個Java字符串對象。其形式如下:

jstring NewStringUTF(JNIEnv *env, const char *bytes);

這個接口沒有第三個參數用于指定字符串的長度,而是用\0字符表示該字符串的結束符。我們可以回顧JNIPrint函數中對此接口的使用。


對Java數組對象的操作

我們首先介紹獲取Java數組對象的元素個數的接口。其形式如下:

jsize GetArrayLength(JNIEnv *env, jarray array);

這個接口非常簡單,我們不做過多介紹。

下面介紹獲取Java數組對象的元素。這里根據Java數組元素的不同類型,其接口形式也有所不同。獲取元素類型為對象類型的接口是GetObjectArrayElement,其形式如下:

jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

這里第二個參數array是Java數組對象。第三個參數index指定所要獲取數組元素對象的索引。

而對于獲取數組元素為基本類型的數組,可以通過兩種接口進行獲取,第一種是Get<PrimitiveType>ArrayElements接口,其形式如下:

NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);

此接口的參數與獲取字符串的參數差不多。下面列出<type>以及相應的NativeType的列表。

屏幕快照 2018-10-02 下午3.32.28.png

該接口所返回的基本類型的元素的緩存首地址也是受JVM管理的。因此,當我們用完此緩存元素之后需要調用Release<PrimitiveType>ArrayElements接口進行釋放。此接口形式如下:

void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);

第二個參數array表示對應的Java數組對象。第三個參數elems是上面Get接口所返回的緩存首地址。第四個參數mode表示釋放模式,它目前有三種值:

  1. 0表示將elems中存放的元素拷貝回array數組對象,然后將所分配的elems緩存釋放掉。
  2. JNI_COMMIT表示將elems中存放的元素拷貝回array數組對象,但不釋放elems緩存。
  3. JNI_ABORT表示不拷貝回當前elems中存放的元素,而直接釋放elems緩存。
    此接口的所有具體接口名列表如下所示:
屏幕快照 2018-10-02 下午3.41.41.png

還有一個接口是Get<PrimitiveType>ArrayRegion,其形式如下:

void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

這個第二個參數array表示Java數組對象。第三個參數start指示從第幾個元素開始獲取。第四個參數len指示獲取多少個元素。第五個參數buf指向存放數組元素的緩存。由于此接口是由程序員自己負責分配存放數組元素的空間,因此JNI API不提供相應的釋放接口。

下面我們介紹一下Java數組元素的設置接口。與獲取數組元素的接口類似,Java數組元素的設置接口也是根據不同的元素類型而有所不同。如果要設置元素類型為Java對象的數組對象,則使用SetObjectArrayElement接口。該接口形式如下:

void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

該接口第二個參數array用于指定所要操作的Java數組對象。第三個參數index用于指定設置元素對象的索引。而第四個參數value就是所要設置的對象。

而要設置數組元素類型為基本類型的接口就只有一種,即Set<PrimitiveType>ArrayRegion。其形式如下:

void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);

其中,第二個參數array為Java層的數組對象。第三個參數start表示要對array數組進行元素設置的起始索引。第四個參數len指定了所要設置元素的個數。第五個參數buf則是在JNI側準備好的要對array進行設置的元素緩存。下面列出所有具體<PrimitiveType>的相關接口:

屏幕快照 2018-10-03 下午2.23.15.png

下面我們將對數組操作舉一個綜合性的例子。大家先在Java端的MyJNIClass類中添加以下方法:

    /**
     * 聲明一個類方法,它在JNI中實現
     * @param array 一個int數組對象
     * @return 一個int數組對象
     */
    public static native int[] arrayOpMethod(int[] array);

隨后在JNI側的test.c中添加以下函數:

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.arrayOpMethod這一類方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param cls 指向在Java層調用此方法類方法的類類型
/// @param array 調用此類方法時所傳入的Java端的int數組對象
JNIEXPORT jintArray JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_arrayOpMethod(JNIEnv* env, jclass cls, jintArray array)
{
    // 獲取Java數組的長度
    const size_t arrayCount = (*env)->GetArrayLength(env, array);
    
    jboolean isCopy = false;
    
    // 獲取Java數組中的所有元素
    int *cArray = (*env)->GetIntArrayElements(env, array, &isCopy);
    
    // 我們將該數組中的所有元素的值做加1操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i]++;
    
    // 將更新后的數組替換原Java數組對象中的元素
    (*env)->SetIntArrayRegion(env, array, 0, arrayCount, cArray);
    
    // 新建一個新的數組作為后面返回的結果數組
    jintArray resultArray = (*env)->NewIntArray(env, arrayCount);
    
    // 我們將之前數組中的所有元素的值再做乘以2的操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i] *= 2;
    
    // 將更新后的數組替換resultArray這一Java數組對象中的元素
    (*env)->SetIntArrayRegion(env, resultArray, 0, arrayCount, cArray);
    
    (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT);
    
    return resultArray;
}

最后,我們在Activity層添加以下代碼即可觀察到運行結果:

        int[] array = {1, 2, 3};
        int[] dstArray = MyJNIClass.arrayOpMethod(array);
        String output = String.format("array [0] = %1$d, [1] = %2$d, [2] = %3$d",
                array[0], array[1], array[2]);
        System.out.println(output);

        output = String.format("dstArray [0] = %1$d, [1] = %2$d, [2] = %3$d",
                dstArray[0], dstArray[1], dstArray[2]);
        System.out.println(output);

OK!下面我們較完整的展示一下我們較完整的項目代碼。

首先列出完整的test.c源文件:

#include <jni.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <cpu-features.h>

/// 全局JNI環境指針變量
JNIEnv* gEnv;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    jint result = -1;
    
    if((*vm)->GetEnv(vm, (void**)&gEnv, JNI_VERSION_1_6) != JNI_OK)
        return -1;
    
    assert(gEnv != NULL);
    
    return JNI_VERSION_1_6;
}

/// 在JNI側的打印函數
/// @param 指定的要打印輸出的C字符串
static void NativePrint(const char *s)
{
    if(s == NULL)
        return;
    
    // 將C字符串轉換為Java字符串對象
    jstring jstr = (*gEnv)->NewStringUTF(gEnv, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*gEnv)->FindClass(gEnv, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print類方法;其類型為:(String) -> void
    jmethodID methodID = (*gEnv)->GetStaticMethodID(gEnv, cls, "print", "(Ljava/lang/String;)V");
    
    // 調用print這一類方法
    (*gEnv)->CallStaticVoidMethod(gEnv, cls, methodID, jstr);
}

/// 在JNI側的打印函數
/// @param 指定的要打印輸出的C字符串
static void JNIPrint(JNIEnv* env, const char *s)
{
    if(s == NULL)
        return;
    
    // 將C字符串轉換為Java字符串對象
    jstring jstr = (*env)->NewStringUTF(env, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print類方法;其類型為:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");
    
    // 調用print這一類方法
    (*env)->CallStaticVoidMethod(env, cls, methodID, jstr);
}

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod這一成員方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param instance 表示對當前調用此實例方法的MyJNIClass對象實例的引用
/// @param i 調用此實例方法時所傳入的整型參數
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass類
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField實例屬性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 獲取當前mField實例屬性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后對mField實例屬性進行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 獲取類屬性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 獲取當前類屬性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 獲取MyJNIClass類的increasedInt實例方法;其類型為:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 調用this對象的increasedInt實例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);
    
    char strBuffer[128];
    sprintf(strBuffer, "native value is: %d\n", value);
    NativePrint(strBuffer);
    
    return value + 100;
}

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.nativeStaticMethod這一類方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param cls 指向在Java層調用此方法類方法的類類型
/// @param js 調用此類方法時所傳入的Java字符串對象
JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16編碼形式獲取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 獲取當前字符串的長度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后進行釋放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 輸出結果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
    
    // 獲取UTF-8字符串
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    
    // 獲取當前以UTF-8編碼所表示的字符串的長度
    length = (*env)->GetStringUTFLength(env, js);
    
    // 組織所要打印c輸出的字符串
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    
    // 釋放UTF-8字符串緩存
    (*env)->ReleaseStringUTFChars(env, js, cs);
    
    // 打印輸出:The input string is: ??, and the length is: 4
    JNIPrint(env, buffer);
    
    // 獲取類屬性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后對類屬性cField的值進行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
}

/// 這個函數與com.test.zenny_chen.test.MyJNIClass.arrayOpMethod這一類方法對應
/// @param env 表示當前JVM所傳入的JNI環境
/// @param cls 指向在Java層調用此方法類方法的類類型
/// @param array 調用此類方法時所傳入的Java端的int數組對象
JNIEXPORT jintArray JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_arrayOpMethod(JNIEnv* env, jclass cls, jintArray array)
{
    // 獲取Java數組的長度
    const size_t arrayCount = (*env)->GetArrayLength(env, array);
    
    jboolean isCopy = false;
    
    // 獲取Java數組中的所有元素
    int *cArray = (*env)->GetIntArrayElements(env, array, &isCopy);
    
    // 我們將該數組中的所有元素的值做加1操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i]++;
    
    // 將更新后的數組替換原Java數組對象中的元素
    (*env)->SetIntArrayRegion(env, array, 0, arrayCount, cArray);
    
    // 新建一個新的數組作為后面返回的結果數組
    jintArray resultArray = (*env)->NewIntArray(env, arrayCount);
    
    // 我們將之前數組中的所有元素的值再做乘以2的操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i] *= 2;
    
    // 將更新后的數組替換resultArray這一Java數組對象中的元素
    (*env)->SetIntArrayRegion(env, resultArray, 0, arrayCount, cArray);
    
    (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT);
    
    return resultArray;
}

隨后列出MyJNIClass.java源文件:

package com.test.zenny_chen.test;

/**
 * 我們定制的JNI測試類
 */
public class MyJNIClass {

    static {
        // 在訪問MyJNIClass時,加載jni_test動態庫
        System.loadLibrary("jni_test");
    }

    /**
     * 聲明一個實例方法,它在JNI中實現
     * @param i 指定一個整數參數
     * @return 某一整數
     */
    public native int nativeInstanceMethod(int i);

    /**
     * 聲明一個類方法,它在JNI中實現
     * @param s 指定一個字符串
     */
    public native static void nativeStaticMethod(String s);

    /**
     * 聲明一個類方法,它在JNI中實現
     * @param array 一個int數組對象
     * @return 一個int數組對象
     */
    public static native int[] arrayOpMethod(int[] array);

    /**
     * 當前類的實例方法,將指定參數的值加1然后返回
     * @param i 指定的一個整數
     * @return 參數加1后的值
     */
    public int increasedInt(int i) {
        return i + 1;
    }

    /**
     * 當前類的類方法,用于實現打印輸出指定的字符串
     * @param s 指定的字符串內容
     */
    public static void print(String s) {
        System.out.println(s);
    }

    /** 當前類的一個實例屬性 */
    public int mField;

    /** 當前類的一個類屬性 */
    public static int cField;
}

最后列出Activity端的測試代碼:

        MyJNIClass.nativeStaticMethod("??");

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);

        int[] array = {1, 2, 3};
        int[] dstArray = MyJNIClass.arrayOpMethod(array);
        String output = String.format("array [0] = %1$d, [1] = %2$d, [2] = %3$d",
                array[0], array[1], array[2]);
        System.out.println(output);

        output = String.format("dstArray [0] = %1$d, [1] = %2$d, [2] = %3$d",
                dstArray[0], dstArray[1], dstArray[2]);
        System.out.println(output);

關于上述代碼,有一個JNI_OnLoad函數我們沒有講解到。這個函數用于保存全局JNI環境所提供的。我們有了JNI全局環境句柄之后就不需要依賴每次通過調用的Java層native方法所傳遞過來的env參數,而可直接使用JNI環境句柄來訪問各個類以及屬性與方法了。這樣對公共庫的構建而言顯然要方便很多。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,767評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,815評論 2 59
  • 從本書中,提煉出獲得幸福的小習慣,希望可以為我們的生活帶來小幸運。 1.每晚睡前10分鐘采集三個幸福時刻,記錄他們...
    懿拾閱讀 324評論 2 0
  • 上午閑來無事便點開聽了前段時間在微信十點讀書欄目訂閱的《聽蔣勛講中國文學》,聽蔣大師用他那帶有磁性渾厚的聲音把悠...
    五月的荷閱讀 315評論 4 12
  • 要按照規定來 做事心里要有個大概的規劃 今天本來排好了教室 因為自己大意疏忽差點出了亂子 心情好看什么都好 心情...
    都被注冊了2閱讀 154評論 2 1