JNI開發之JNI原理

  在上一篇文章中對JNI簡單介紹了,在這篇文章中將對JNI原理進行介紹。本篇文章將以JNI執行環境、JNI數據類型、JNI註冊方式、JNI引用、JNI變量共享以及JNI調用方式來介紹JNI原理。

 一、執行環境(Runtime)

  在計算機中,每種編程語言都有一個執行環境(Runtime),執行環境用來解釋執行語言的語句。在JNI開發中有兩個比較重要與執行環境Runtime相關的變量:JavaVMJNIEnv

  • JavaVM
  Java語言的執行環境是Java虛擬機(JVM),JVM其實是主機環境中的一個進程,每個JVM虛擬機進程在本地環境中都有一個JavaVM結構體,該結構體在創建Java虛擬機的時候被返回的,在JNI中創建JVM函數爲JNI_CreateJavaVM,在jni.h頭文件中有定義。
jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*);
  在jni.h文件中也定義了JavaVM的數據結構,可以看到JavaVM結構中封裝了一些函數指針,這些函數指針主要是對JVM操作的接口,定義如下:
#if defined(__cplusplus)
typedef _JavaVM JavaVM; //C++的JavaVM定義
#else
typedef const struct JNIInvokeInterface* JavaVM; //C的JavaVM定義
#endif

/*
 * JNI invocation interface.
 */
struct JNIInvokeInterface {
    void*       reserved0;//保留
    void*       reserved1;//保留
    void*       reserved2;//保留

    jint        (*DestroyJavaVM)(JavaVM*); // 銷燬Java虛擬機並回收資源
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);//鏈接到當前Java線程
    jint        (*DetachCurrentThread)(JavaVM*); //從當前Java線程中分離
    jint        (*GetEnv)(JavaVM*, void**, jint);// 獲得當前線程的Java運行環境
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*); // 將當前線程作爲守護線程
};

/*
 * C++ version.
 */
struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
  從代碼可以看到,JNIInvokeInterface結構封裝了幾個和JVM相關的函數。另外在C和C++中,JavaVM的定義是不相同的,在C中的JavaVM是JNIInvokeInterface類型指針,而在C++語言中,JavaVM是在JNIInvokeInterface指針上進行了一次封裝,函數調用時少了一個參數。
  JavaVM是JVM在JNI層的代表,在JNI層中有且僅有一個JavaVM。JavaVM是進程相關的,一個進程只有一個JavaVM。

  • JNIEnv
  JNIEnv是當前線程的執行環境,一個JVM對應一個JavaVM結構,而在一個JVM中可能創建多個Java線程,每個線程都有一個JNIEnv結構,這些JNIEnv結構保存在線程的本地存儲中(Thread Local Storage)。JNIEnv是線程相關的,即在每個線程中都有一個JNIEnv指針,每個JNIEnv都是線程專有的,其他線程不能使用本線程中的JNIEnv。
  JNIEnv不能跨線程,只在當前線程中有效。JNIEnv不能在線程之間進行傳遞,在同一個線程中,多次調用JNI層方法,傳入的JNIEnv是相同的。但是一個本地方法可以被不同的Java線程調用,因此本地方法可以接受不同的JNIEnv。
  JNIEnv有兩個作用:一個是調用Java函數,JNIEnv代表當前Java線程的運行環境,通過JNIEnv可以調用Java中的代碼;另一個是操作Java對象,Java對象傳入JNI層就是jobject對象,需要使用JNIEnv來操作這個Java對象。
  JNIEnv的數據結構是在jni.h文件中定義的,可以看到JNIEnv數據結構也是一個函數表,在本地代碼中通過JNIEnv的函數表來操作Java數據或調用Java方法。也就是說,只要在本地代碼中拿到了JNIEnv結構,就可以在本地代碼中調用Java代碼。
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++定義
#else
typedef const struct JNINativeInterface* JNIEnv;//C定義
#endif
/*
 * Table of interface function pointers.
 */
//C JNIEnv定義
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jclass      (*FindClass)(JNIEnv*, const char*);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
    jfieldID    (*GetStaticFieldID)(JNIEnv*, jclass, const char*,const char*);

   .......
    jint        (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);    //註冊本地方法
    jint        (*UnregisterNatives)(JNIEnv*, jclass); //反註冊本地方法
    jint        (*GetJavaVM)(JNIEnv*, JavaVM**); //獲取對應的JavaVM對象
    ......
}

/*
 * C++ object wrapper. 
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
//C++的JNIEnv定義
struct _JNIEnv {
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)
  jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    .......
}

  從代碼可知,JNIEnv和JavaVM類似,也是定義了一些函數指針,通過這些函數可以操作Java對象。JNIEnv在C和C++中定義的方式也不相同,C++對JNINativeInterface指針進行了一次封裝,調用時更加方便。
  總的來說, JNI其實就是定義了Java語言和本地語言之間的一種溝通方式,這種溝通方式依賴於JavaVM和JNIEnv結構中定義的函數表,這些函數將Java中的方法調用轉換爲本地語言的函數調用。
  JNI是JVM實現的一部分,JavaVM是JVM在JNI層的代表,每個Java線程都有一個JNIEnv,代表當前線程的執行環境,他們之間的關係如下圖所示:
   



二、JNI數據類型


  當Java與Native語言相互調用時,肯定會涉及到數據的傳遞。這兩者屬於不同的編程語言,在數據類型上有很多差異的。例如,在C語言中,int類型的長度取決於平臺,char類型爲1個字節長度,而在Java語言中,int類型固定爲4個字節,而char類型爲2個字節。爲了使Java語言數據類型與Native語言數據類型匹配,需要藉助JNI的數據類型來保證它們兩者之間的數據類型和數據空間大小匹配。JNI中定義的一些數據類型有基本數據類型和引用數據類型。

    2.1 JNI基本類型

Java類型
Native類型
JNI類型
描述
boolean
unsigned char
jboolean
無符號8比特
byte
signed char
jbyte
有符號8比特
char
unsigned short
jchar
無符號16比特
short
short
jshort
有符號16比特
int
int
jint
有符號32比特
long
long long
jlong
有符號64比特
float
float
jfloat
32比特
double
double
jdouble
64比特
void
void
void
N/A
  Java類型和JNI類型名稱具有一致性,JNI類型的名稱在只是在Java類型的基礎上添加了一個j,例如,Java的int類型對應的JNI類型爲jint,Java的long類型,對應的JNI類型爲jlong。JNI的基本類型在jni文件中定義了:

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 */
typedef jint            jsize;

  需要注意的是jchar代表的是Java的char類型,對應於C/C++中的卻是unsigned short類型,因爲Java中的char類型是兩個字節,jchar相當於C/C++的寬字符。如果需要在本地方法中定義一個jchar類型的數據,規範的寫法應該是jchar = L'C'。

  實際上,所有帶j的JNI類型,都是JNI對應的Java類型,並且JNI的類型接口與本地代碼在類型的空間大小上是完全匹配的,而在語言層次上卻不一定相同。在本地方法中與JNI接口調用時,要在內部進行轉換,必須小心處理。
  

2.2 JNI引用類型

    
  在本地代碼中爲了訪問Java運行環境中的引用類型,在JNI中也定義了一套對應的引用類型,他們的對應關係如下:
                          
  所有的引用類型對應於jobject,java.lang.Class類型對應於jclass,數組類型對應於jarray,java.lang.Throwable類型對應於jthrowable,Object數組對應於jobjectArray。
  JNI引用類型都是以j開頭的,與Java中所有類的父類都是Object一樣,所有的JNI引用類型都是jobject的子類。JNI的引用類型在jni.h文件中有定義:
#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 {};
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 {};

//在C++中定義的引用類型
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;
#else /* not __cplusplus */

//在C語言中定義的JNI引用類型
/*
 * Reference types, in C.
 */
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;
#endif /* not __cplusplus */

2.3 JNI類型簽名


爲什麼需要JNI類型簽名?
  Java語言是面向對象的語言,支持方法的重載。即允許多個方法調用擁有相同的名字,通過方法的參數和返回值來確定調用的是哪一個函數。爲了讓JNI能調用Java對象中的方法,僅僅通過方法名是不夠的,還需要指定方法的簽名,即:參數列表和返回值類型。
  Java類型對應的JNI類型簽名如下表所示:
Java類型
JNI類型簽名
boolean
Z
btye
B
char
C
short
S
int
I
long
J
float
F
double
D
Class類
L
void
V
數組[]
[
boolean[] [Z
byte[] [B
char[]
[C
short[]
[S
int[]
[I
long[]
[J
float[]
[F
double[]
[D

 基本類型 
 以特定的單個字母表示,例如int用I表示。

 Java類型(L +類名 + ; )
 Java類型以L開頭,以"/"分割包名,在類名的後面加上分號“;”分隔符。例如String的簽名爲:Ljava/lang/String;

 Java數組 ([ + 數據元素簽名)
 Java中數組是引用類型,數組是“[”開頭,後面跟上數組元素的簽名。例如int[]的簽名爲:[I,Object[]的簽名爲:[Ljava/lang/Object;。

  JNI對於方法簽名有特定的格式要求,參數列表簽名在前,返回值簽名在後,如下所示:
      (參數類型簽名列表)返回值類型簽名
 需要注意的是:
  • 在方法簽名中沒有體現方法名;
  • 括號內表示參數列表,參數列表緊密相挨,中間沒有逗號和空格;
  • 返回值出現在括號後面;
  • 如果函數沒有返回值,也要加上V類型。
  例如:
 void setName(String name);對應的JNI方法簽名是(Ljava/lang/String;)V
 char fun(int n,String s,int[] value);對應的JNI方法簽名是(ILjava/lang/String;[I)C
 可以通過javap -s  類名   生成該類中函數方法的類型簽名
 在jni.h文件中,定義了JNI本地方法的數據結構,裏面就有JNI的方法簽名,signature就是JNI方法的簽名。
typedef struct {
    const char* name;//方法名字
    const char* signature;//方法簽名
    void*       fnPtr;//本地方法名字
} JNINativeMethod;

三、JNI方法註冊方式


  JNI方法註冊的方式有兩種:一種是在系統啓動的時候註冊,另外一種是通過System.loadLibrary來加載so庫文件註冊。

3.1系統啓動時註冊

  在Android系統中,所有的應用程序進程以及系統服務進程SystemServer都是由Zygote進程孕育而來(fork)而來。我們知道,Android系統是基於Linux內核的,而在Linux內核中,所有的進程都是由init進程直接或間接fork出來的。Zygote進程也不例外,它是在系統啓動的過程中,由init進程創建的。在啓動Zygote進程的過程中,通過調用AndroidRuntime.cpp的startVm方法創建虛擬機VM,VM創建完成後,緊接着調用startReg註冊JNI方法,最後調用com.android.internal.os.ZygoteInit類main函數來啓動Zygote進程。
  在系統啓動腳本system/core/rootdir/init.rc文件中,可以看到啓動Zygote進程的腳本命令:
  //引用ro.zygote.rc文件
  import /init.${ro.zygote}.rc
  在rootdir目錄下init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc四個文件,代表32位和64位平臺的Zygote腳本,以32位的Zygote腳本爲例,如下:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks
  前面的關鍵字service告訴init進程創建一個名爲“Zygote”的進程,這個Zygote進程需要執行的程序是/system/bin/app_process,後面是要傳給app_process的參數。
  socket關鍵字表示這個Zygote進程需要一個名爲“Zygote”的socket資源,這樣系統啓動後,我們就可以在/dev/socket目錄下看到有一個名爲Zygote的文件。這裏定義的socket的類型是unix domain socket,它是用來作爲本地進程間通信用的。
  最後一系列onrestart關鍵字表示這個Zygote進程重啓時需要執行的命令。
  由前面知道,Zygote進程需要執行app_process程序,app_prcess程序是system/bin/app_process,它的源碼位於frameworks/base/cmds/app_process/app_main.cpp文件中,入口函數是main。
int main(int argc, char* const argv[])
{
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));//創建AppRuntime
    .........
    bool zygote = false;
    bool startSystemServer = false;
    ...........

    ++i;  
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;//啓動Zygote進程
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;//啓動SystemServer進程
        } 
        ........
    }

    Vector<String8> args;
    if (!className.isEmpty()) {
       .........
    } else {
        if (startSystemServer) {
            args.add(String8("start-system-server"));//添加啓動SystemServer的參數
        }
        ............
    }


    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);//啓動Zygote進程
    } 
    ............
}
  
  這個函數的主要作用是創建一個AppRuntime變量,然後調用它的成員函數start來啓動ZygoteInit。AppRuntime類繼承自AndroidRuntime類,同樣定義在app_main.cpp文件中,如下所示: 
class AppRuntime : public AndroidRuntime
{
public:
    AppRuntime(char* argBlockStart, const size_t argBlockLength)
        : AndroidRuntime(argBlockStart, argBlockLength)
        , mClass(NULL)
    { }
} 
     而AndroidRuntime類定義在AndroidRuntime.cpp文件中,代碼路徑在frameworks/base/core/jni/AndroidRuntime.cpp路徑下。
AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) :
        mExitWithoutCleanup(false),
        mArgBlockStart(argBlockStart),
        mArgBlockLength(argBlockLength)
{
    SkGraphics::Init();
    mOptions.setCapacity(20);
    assert(gCurRuntime == NULL);        // one per process
    gCurRuntime = this; //將Runtime保存到一個全局變量中
}

  在創建AppRuntime對象的時候,也會調用其父類AndroidRuntime的構造函數。在AndroidRuntime構造函數中會將this保存到一個全局靜態變量變量gCurRuntime中,可以通過getRuntime()函數來獲取該靜態對象。
static AndroidRuntime* gCurRuntime = NULL;
AndroidRuntime* AndroidRuntime::getRuntime()
{
    return gCurRuntime;
}
  在調用AppRuntime的start方法時,最終調用的是其父類AndroidRuntime的start方法,代碼如下:
frameworks/base/core/jni/AndroidRuntime.cpp
/*
 * Start the Android runtime.  This involves starting the virtual machine
 * and calling the "static void main(String[] args)" method in the class
 * named by "className".
 *
 * Passes the main function two arguments, the class name and the specified
 * options string.
 */
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
     .......
    static const String8 startSystemServer("start-system-server");

    ........
    // 1.啓動虛擬機VM
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    onVmCreated(env);

    //2.註冊JNI函數
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

   //準備啓動Zygote進程的參數
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    for (size_t i = 0; i < options.size(); ++i) {
        jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
        assert(optionsStr != NULL);
        env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }


    // 3.啓動Zygote進程,調用ZygoteInit.main()方法。
    /*
     * Start VM.  This thread becomes the main thread of the VM, and will
     * not return until the VM exits.
     */
    char* slashClassName = toSlashClassName(className);
    //通過JNI調用java方法,具體是通過找到ZygoteInit類,以及ZygoteInit類的靜態方法main,
    //最後通過CallStaticVoidMethod調用ZygoteInit類的main方法。
    // "com.android.internal.os.ZygoteInit"是通過app_main中main方法傳遞過來的參數。
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);//通過JNI調用ZygoteInit的main方法
        }
    }
    free(slashClassName);

    // 虛擬機退出了纔會執行到這裏
    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}
  在AndroidRuntime的start方法中,主要乾了三件事情,一是啓動VM;二是註冊JNI函數,最後一件是啓動ZygoteInit類的main函數。
  startVm函數是啓動Dalvik虛擬機,該方法定義在frameworks/base/core/jni/AndroidRuntime.cpp中實現了,主要是定義一些啓動Dalvik虛擬機參數以及初始化JavaVM和JNIEnv變量。
/*
 * Start the Dalvik Virtual Machine.
 *
 * Various arguments, most determined by system properties, are passed in.
 * The "mOptions" vector is updated.
 *
 * Returns 0 on success.
 */
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    JavaVMInitArgs initArgs;
    //準備創建JVM的參數
    .........
    // 初始化VM,
     /*
     * Initialize the VM.
     *
     * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
     * If this call succeeds, the VM is ready, and we can start issuing
     * JNI calls.
     */
    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        return -1;
    }

    return 0;
}
  當調用JNI_CreateJavaVM成功後,VM也將準備好了可以被使用,可以調用JNI方法。調用JNI_CreateJavaVM後,將初始化一個JavaVM和JNIEnv變量,每個進程都對應一個JavaVM,每個線程對應於一個JNIEnv,即JavaVM是進程相關的,而JNIEnv是線程相關的。
  startReg函數是將JNI方法註冊到VM中,startReg函數也是在AndroidRuntime.cpp文件中實現,代碼如下:
/*
 * Register android native functions with the VM.
 */
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
  // 設置創建線程的方法爲javaCreateThreadEtc,該方法定義在AndroidRuntime.h中
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
    ......
    
    env->PushLocalFrame(200);
   //註冊gRegJNI數組中的函數
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);
    return 0;
}
  可以看到startReg函數最後調用的是register_jni_procs函數將gRegJNI數組中的函數註冊到VM中。gRegJNI是一個包含JNI函數的數組,定義如下:
/*
 * Register android native functions with the VM.
 */
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
  // 設置創建線程的方法爲javaCreateThreadEtc,該方法定義在AndroidRuntime.h中
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
    ......
    
    env->PushLocalFrame(200);
   //註冊gRegJNI數組中的函數
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);
    return 0;
}
    可以看到startReg函數最後調用的是register_jni_procs函數將gRegJNI數組中的函數註冊到VM中。gRegJNI是一個包含JNI函數的數組,定義如下:
//一個包含JNI函數的數組
static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_com_android_internal_os_ZygoteInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    REG_JNI(register_android_util_Log),
    REG_JNI(register_android_util_MemoryIntArray),
    ....
}

#define REG_JNI(name)      { name }
struct RegJNIRec {
        int (*mProc)(JNIEnv*);
 };

 //循環調用gRegJNI數組中的JNI函數,每一個方法都對應於一個類的jni映射。
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
            return -1;
        }
    }
    return 0;
}
  
  在register_jni_procs()函數中,循環調用gRegJNI數組中定義的JNI函數。這樣就完成了JNI函數的註冊。舉個例子,在RegJNIRec數組中有一個register_com_android_internal_os_RuntimeInit函數指針,當調用register_jni_procs()方法時,會調用register_com_android_internal_os_RuntimeInit()方法,將會註冊JNI本地方法。
int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
{
    const JNINativeMethod methods[] = {
        { "nativeFinishInit", "()V",
            (void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
        { "nativeSetExitWithoutCleanup", "(Z)V",
            (void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
    };
    return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
        methods, NELEM(methods));
}
     jniRegisterNativeMethods方法的作用是將本地方法註冊到JNI中,JNINativeMethod是定義本地方法的一個數據結構在jni.h文件中定義。
   typedef struct {
    const char* name;//Java方法名字
    const char* signature;//方法簽名
    void*       fnPtr;//Java方法對應的本地函數指針
} JNINativeMethod; 
  至此,介紹完了JNI方法在系統啓動過程中的註冊流程。其流程如下圖所示:
             



3.2 通過loadLibrary方法註冊

  除了在系統啓動的時候註冊JNI函數,還有一種JNI註冊方式是通過loadLibrary實現的。loadLibrary方法是System.java中的靜態方法,先來看實現:
  在java/lang/System.java類中

/*
     * 加載libname指定的本地庫
     */
    @CallerSensitive
public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
  在 java/lang/Runtime.java類中
 /*
    * 通過給定的ClassLoader搜索和加載指定的共享庫文件
    */
    void loadLibrary(String libraryName, ClassLoader loader) {
        //loader不爲空,進入該分支處理
        if (loader != null) {
            //查找庫所在路徑
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            //加載庫文件
            String error = doLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        //loader爲空,則進入該分支處理
        // 返回平臺相關庫文件名字,在Android中,如果共享庫爲MyLibrary,則返回的共享庫名字爲“libMyLibrary.so”。
        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
         //在/system/lib/和/vendor/lib/下查找指定的filename文件
        for (String directory : mLibPaths) {
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                // 找了對應的庫文件,加載庫文件
                String error = doLoad(candidate, loader);
                if (error == null) {
                    return; // We successfully loaded the library. Job done.
                }
                lastError = error;
            }
        }

        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);//沒有找到滿足條件的,報錯
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

// mLibPaths是保存庫文件路徑,用來查找native庫文件
private final String[] mLibPaths = initLibPaths();
   /* 
     *搜索JNI庫文件的路徑,通過"java.library.path"讀取出來的屬性值爲/vendor/lib:/system/lib/。
     *其中/system/lib/路徑存放的是系統應用使用的so庫文件,/vendor/lib/路徑存放的是第三方應用的so庫文件。*/

 private static String[] initLibPaths() {
        String javaLibraryPath = System.getProperty("java.library.path");
        if (javaLibraryPath == null) {
            return EmptyArray.STRING;
        }
        String[] paths = javaLibraryPath.split(":");
        // Add a '/' to the end of each directory so we don't have to do it every time.
        for (int i = 0; i < paths.length; ++i) {
            if (!paths[i].endsWith("/")) {
                paths[i] += "/";
            }
        }
        return paths;
    }

 private String doLoad(String name, ClassLoader loader) {
        String ldLibraryPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
        }
        synchronized (this) {
            //最後調用nativeLoad方法加載庫文件
            return nativeLoad(name, loader, ldLibraryPath);
        }
    }
  可以看到,System.loadLibrary()方法會先去/system/lib/和/vendor/lib/目錄下去查找指定名字的so庫文件,然後調用nativeLoad方法加載庫文件。nativeLoad()方法在java_lang_Runtime.cc文件中實現,該文件在art/runtime/native/java_lang_Runtime.cc路徑下。這裏再深入下去,就是虛擬機內部的實現了,這裏暫不描述。可以說一下接來下大致的處理流程:
  • 調用dlopen()函數,打開一個so文件並創建一個handle;
  • 調用dlsym()函數,查看相應so文件的JNI_OnLoad()函數指針,並執行相應的函數。

  總之,System.loadLibrary()的作用就是調用相應庫中的JNI_OnLoad()方法。接下來看JNI_OnLoad函數。

3.2.1JNI_OnLoad方法

  本地代碼最終編譯成動態庫,在Java代碼中通過System.loadLibrary()方法來加載本地代碼庫,當本地代碼動態庫被JVM加載時,JVM會自動調用本地代碼中的JNI_OnLoad函數。
  JNI_OnLoad函數的在jni.h文件中定義:
 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
   其中vm參數代表JVM實例,主要是包含一些與JVM相關的操作函數;reversed保留
   JNI_OnLoad函數的工作流程一般如下:
  1. 通過JavaVM獲取JNIEnv,即通過getEnv函數,獲取JNIEnv,JNIEnv代表Java線程執行環境;
  2. 通過RegisterNative函數註冊本地方法;
  3. 返回JNI版本號;
  接下來看一個實例,在framework/media/jni/android_media_MediaPlayer.cpp文件中,實現了JNI_OnLoad()方法:
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;
    // 1.通過JavaVM獲取JNIEnv,指定JNI的版本爲1.4。
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);

    ...
    // 2.註冊JNI方法
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }

    ...
    /* success -- return valid version number */
    // 3.返回JNI版本號
    result = JNI_VERSION_1_4;

bail:
    return result;
}

// 調用registerNativeMethods方法註冊本地方法
static int register_android_media_MediaPlayer(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
static const JNINativeMethod gMethods[] = {
    ...
    {"_prepare",            "()V",                              (void *)android_media_MediaPlayer_prepare},
    {"_start",              "()V",                              (void *)android_media_MediaPlayer_start},
    {"_stop",               "()V",                              (void *)android_media_MediaPlayer_stop},
    {"seekTo",              "(I)V",                             (void *)android_media_MediaPlayer_seekTo},
    {"_pause",              "()V",                              (void *)android_media_MediaPlayer_pause},
    {"isPlaying",           "()Z",                              (void *)android_media_MediaPlayer_isPlaying},
    {"getCurrentPosition",  "()I",                              (void *)android_media_MediaPlayer_getCurrentPosition},
    {"getDuration",         "()I",                              (void *)android_media_MediaPlayer_getDuration},
    {"_release",            "()V",                              (void *)android_media_MediaPlayer_release},
    {"_reset",              "()V",                              (void *)android_media_MediaPlayer_reset},
    {"setParameter",        "(ILandroid/os/Parcel;)Z",          (void *)android_media_MediaPlayer_setParameter},
    {"native_setup",        "(Ljava/lang/Object;)V",            (void *)android_media_MediaPlayer_native_setup},
    ....
};
  在framework/core/jni/AndroidRuntime.cpp文件中實現了registerNativeMethods方法,並最終調用jniRegisterNativeMethods方法。
/*
 * Register native methods using JNI.
 */
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
  registerNativeMethods方法中的className代表的是需要註冊方法的類,JNINativeMethod結構體保存的是映射關係,將Java方法映射到本地函數指針。gMethods是一個JNINativeMethods數組,因爲一個Java類中可能定義多個本地方法,所以需要一個數組來保存這些本地方法的映射關係。
  在/libnativehelper/JNIHelp.cpp文件中實現了jniRegisterNativeMethods方法
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);

    scoped_local_ref<jclass> c(env, findClass(env, className));
    ...
    // 調用RegisterNatives()方法完成註冊
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
       ...
    }
    return 0;
}
  RegisterNatives方法在jni.h文件中定義。
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;
        ...
    jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods){
        return functions->RegisterNatives(this, clazz, methods, nMethods); 
    }
    ....
}
  functions是一個JNINativeInterface的指針,也將調用RegisterNatives()方法,再往下面就是虛擬機的內部實現了,在此不再詳述。
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    ....
    jint  (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
    ....
}
  當不需要這些映射關係時,或者需要更新映射關係時,則需要調用UnregisterNatives函數,來刪除這些映射關係。
jint        (*UnregisterNatives)(JNIEnv*, jclass);
  至此,介紹了通過System.loadLibrary()方法註冊JNI函數的過程。整體的流程如下圖所示:



3.3如何查找native方法


  當查看framework層的代碼時,經常會遇到一些native方法,這些native方法是定義在哪些文件中呢,該如何查找對應的文件呢?接下來說明一些查找native方法的一些途徑。
  首先獲取native方法定義所在的包名和類名,然後得出本地方法名稱。
  [包名]_[類名]_nativemethod()。
  例如:MessageQueue.java中有一個nativeInit()方法,所以得出本地方法名稱爲android_os_MessageQueue_nativeInit()。
  獲取到本地方法名稱後,如果是在系統啓動時候註冊的JNI函數,則可以先到framework/base/core/jni/目錄下查找對應的.cpp和.h文件。cpp和h文件的命名方式一般爲:
  [包名]_[類名].cpp
  [包名]_[類名].h
  MessageQueue.java對應的native文件爲os_android_MessageQueue.cpp文件
  如果是通過System.loadLibrary()註冊的JNI函數,則先找到生成.so庫文件的Android.mk文件,然後在Android.mk文件下的查找包含本地方法名稱的.cpp和.h文件,一般的命名規則和前面一樣:
  [包名]_[類名].cpp
  [包名]_[類名].h
  如果通過上訴兩種方法還未找到,則通過全局搜索包含本地方法名稱的Native文件了。

四、JNI引用

  在JNI提供了三種Reference類型,Global Reference(全局引用)、Local Reference(本地引用)和Weak Global Reference(全局弱引用)。其中Global Reference如果不主動釋放,則一直佔用。
  Java代碼調用本地方法時,涉及到參數的傳遞以及返回值的複製。對於int、char等基本類型,是以傳值的方式進行,可以直接拷貝;而對於Java對象類型,是通過傳遞引用來實現的。JVM保證了所有的Java對象正確的傳遞給本地代碼,並且維持這些引用。如果本地代碼不釋放這些引用,則Java對象不能被Java的垃圾回收器GC回收。
那麼,有什麼辦法可以通知垃圾回收器GC回收這些Java對象?
  JNI將在本地方法引用的對象分爲三大類:局部引用、全局引用和全局弱引用三大類。
  局部引用:只在本地方法中有效,當本地方法返回時,該局部引用自動回收。
  全局引用:只有顯式通知垃圾回收器GC回收,纔會被回收,否則會一直有效。

局部引用
全局引用
作用範圍
本地方法內
全局範圍
回收時機
本地方法返回時,自動回收
顯式通知GC回收
適用範圍
只在當前線程中有效
可以跨越多個線程
創建引用函數
jobject    (*NewLocalRef)(JNIEnv*, jobject)
jobject    (*NewGlobalRef)(JNIEnv*, jobject)
銷燬引用函數 void    (*DeleteLocalRef)(JNIEnv*, jobject)
void    (*DeleteGlobalRef)(JNIEnv*, jobject)

  創建引用和刪除引用的函數在jni.h中定義了:
jobject     (*NewLocalRef)(JNIEnv*, jobject);
void        (*DeleteLocalRef)(JNIEnv*, jobject);

jobject     (*NewGlobalRef)(JNIEnv*, jobject);
void        (*DeleteGlobalRef)(JNIEnv*, jobject);

jweak       (*NewWeakGlobalRef)(JNIEnv*, jobject);
void        (*DeleteWeakGlobalRef)(JNIEnv*, jweak);

4.1局部引用

  局部引用的創建和銷燬函數如下所示:
jobject     (*NewLocalRef)(JNIEnv*, jobject);
void        (*DeleteGlobalRef)(JNIEnv*, jobject);
  默認的,Java代碼傳遞給本地方法的參數引用是局部引用,所有本地方法的返回值引用也是局部引用。
  當本地方法返回時,這些局部引用都會自動被回收
jstring Jstring2CStr(JNIEnv*   env,   jstring   jstr)
{
     ....
     jclass   clsstring   =   (*env)->FindClass(env,"java/lang/String");//局部變量
     jstring native_desc = env->NewStringUTF(" I am Native");//局部變量
     ...
}
  雖然局部引用在本地方法返回時,會自動被回收,但還是存在一些情況需要手動釋放局部引用的情況:
  1. 在本地方法中引用了一個很大的Java對象,在使用完該Java對象後,還需要繼續執行一些耗時操作,如果不主動釋放該局部引用對象的話,則需要等到本地方法執行完成才能釋放該局部引用對象。對於內存比較緊張的情況,可能會由於局部引用對象沒有及時回收而導致內存不足的問題。
  2. 在本地方法中創建了大量的局部對象,可能會導致JNI局部引用表溢出,此時需要手動釋放這些不再使用的局部引用對象。例如,在本地代碼中創建一個很大的對象數組。
  3. 不返回的本地函數。例如:在一個本地函數中循環處理消息,如果不釋放循環中使用的局部引用,則會無限地累積,進而導致內存泄漏。此時在循環體中需要手動釋放局部引用。
  局部引用只在創建它們的線程中有效,不能傳遞到其他的線程。

 4.2全局引用

  當一個本地方法需要被多個線程調用,並且希望本地方法中的引用在多個線程間可以共享使用時,那麼可以使用全局引用。全局引用可以跨越多個線程,全局引用需要手動釋放,顯式通知垃圾回收器GC回收它,否則會一直存在。
  全局引用的使用示例如下:
JNIMediaPlayerListener::JNIMediaPlayerListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{   ....
    jclass clazz = env->GetObjectClass(thiz);//局部引用
    ....
    mClass = (jclass)env->NewGlobalRef(clazz);//創建全局引用並指向局部引用
    ...
    mObject  = env->NewGlobalRef(weak_thiz);//創建全局引用
}

JNIMediaPlayerListener::~JNIMediaPlayerListener()
{
    // remove global references
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->DeleteGlobalRef(mObject);//移除全局引用
    env->DeleteGlobalRef(mClass);//移除全局引用
}
  當本地方法不再需要使用全局引用時,應該通過DeleteGlobalRef方法來釋放全局引用。否則的話,JVM不會回收被全局引用的對象。

  4.3 JNI變量共享

  當需要在本地代碼中共享變量時,首先想到的方法是利用全局引用共享。除此之外,還有一種方法是在Java代碼中,保存需要在本地代碼中共享的對象。如下圖所示,在Native方法1中,生成了一個本地對象m,該對象是局部引用,不能被其他的本地方法訪問。通過將本地對象m保存到Java代碼中的相應對象n,當Native方法2需要利用Native方法1中的本地對象m時,可以通過訪問Java代碼中對象n來獲取本地對象m的值。Java代碼的作用可以理解爲共享區域,一個本地方法往其中寫入值,另一個本地方法從中讀取值出來。
            
 
static void android_media_MediaPlayer_native_init(JNIEnv *env,jobject obj)
{   ....
    jclass clazz;
    clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類
    jfieldID name = env->GetFieldID(clazz,"name","Ljava/lang/String;");//獲取MediaPlayer類的域name的ID
    jstring newName = env->NewStringUTF("media player");//創建一個新的字符串
    env->setObjectField(obj,name,newName);//將MediaPlayer類中的name屬性更新
    ....
}
static void android_media_MediaPlayer_start(JNIEnv *env, jobject obj)
{
    ....
    jclass clazz;
    clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類
    jfieldID name = env->GetFieldID(clazz,"name","Ljava/lang/String;");//獲取MediaPlayer類的域name的ID
    jstring mediaName = (jstring)env->getObjectField(obj,name);//讀取在android_media_MediaPlayer_native_init方法中更新的name對象值
    ...
}

  在上面的例子中,首先在android_media_MediaPlayer_native_init方法中更新name屬性到Java代碼對應的域中,然後在android_media_MediaPlayer_start()方法中,通過讀取Java域中對應的字段的值,這樣就實現了將android_media_MediaPlayer_native_init()方法中的對象,傳遞到android_media_MediaPlayer_start()方法中。

五、調用方式

  在JNI開發中,有兩種調用方式:一種是Java代碼調用本地方法;另一種是本地代碼訪問Java的成員。

 5.1Java訪問本地方法

  在某些情況下,一些功能需要本地代碼來實現,例如對運算速度有要求的代碼需要本地代碼來執行。這時Java代碼需要能訪問本地方法。在訪問本地方法前,首先要保證本地代碼被加載到Java執行環境中,並與Java代碼鏈接到一起了。這樣當Java代碼調用本地方法時,能保證找到對應的本地方法實現。Java訪問本地方法的大致步驟如下:
  1. 在java代碼中,聲明native方法;
  2. 編譯生成本地代碼庫.so文件;
  3. 在java代碼中,加載本地代碼庫.so文件;
  4. 在java代碼中,調用本地native方法;
  例如:
public class JavaToNative{
    static{
        System.loadLibrary(“java_to_native”);  // 通過System.loadLibrary()來加載本地代碼庫
        }
        private static native String hello();    // 聲明native方法
        public static void main(String args[]){
            System.out.println(JavaToNative.hello()); // 調用native方法
        }
}
  需要注意的是加載庫文件的代碼System.loadLibrary()是在靜態代碼塊中,因爲靜態代碼塊在加載Java類的時候被調用,而且只會被調用一次。

 5.2本地方法訪問Java成員

  從Android的系統架構來看,JVM和Native系統庫位於內核之上,構成了Android Runtime。更多的系統功能則是通過在其之上的Framework和Java API提供的。因此,如果希望在Native庫中調用某些系統功能,就需要通過JNI來訪問Framework以及Java API提供的功能。
  Java中的類封裝了屬性和方法,要想訪問Java中的屬性和方法,首先要獲取Java類或Java對象,然後再訪問屬性和方法。在Java中,類成員與對象成員是不同的。類成員是指靜態屬性和靜態方法,而對象成員是指非靜態的屬性和非靜態的方法,他們屬於某一個具體的對象。因此,在本地代碼中對類成員與對象成員的訪問是不相同的。
 本地方法訪問Java成員的大致有以下幾個步驟:
  1. 獲取Java運行環境中的類對象class;
  2. 獲取Java類或Java對象的屬性ID和方法ID;
  3. 通過屬性ID和方法ID訪問Java類或Java對象的屬性和方法;
 獲取類對象class
  在JNI中通過FindClass函數來獲取Java運行環境中的類對象class,FindClass函數的定義如下:
jclass FindClass(const char* name);
  name爲類的全名,包含了類的完整路徑,即包名+類名,以"/"代替"."分割符。例如:
jclass  newStr = env->FindClass("java/lang/String");
  還有一種方法是通過Object對象來獲取其對應的類class對象,所調用的方法爲 GetObjectClass,定義如下:
  jclass GetObjectClass(jobject obj);
  obj代表Java傳遞過來的對象,通過該方法可以獲取該對象的類類型,其功能如同Object.getClass()方法。

 獲取Java屬性ID和方法ID
  爲了在C/C++中表示Java的屬性和方法,JNI在jni.h文件中,定義了jfieldID和jmethodID類型,分別用來代表Java的屬性和方法。當需要訪問Java的屬性和方法,需要首先獲取屬性ID和方法ID,然後通過屬性ID和方法ID來訪問對應的屬性和方法。屬性ID和方法ID定義如下:
struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */
struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */
  在jni.h文件中,定義了獲取屬性ID和方法ID的方法:
//獲取Java對象的屬性ID
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig);
//獲取Java類的靜態屬性ID
jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)

//獲取Java對象的方法ID
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
//獲取Java類的靜態方法ID
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig);
  其中參數class爲類對象,是通過前面的FindClass方法或者getObjectClass方法返回的class對象;
  參數name爲屬性或方法的名字;
  參數sig爲JNI類型簽名,在前面有介紹;
  舉個例子來說:
jclass clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類class
jfieldID name = env->GetFieldID(clazz,"name","Ljava/lang/String;");//獲取MediaPlayer對象的屬性name的ID
jmethodID getHost = env->GetMethodID(clazz, "getHost", "(V)Ljava/lang/String;");//獲取MediaPlayer對象方法getHost()的ID

jfieldID port =  env->GetStaticFieldID(clazz,"port","I");//獲取MediaPlayer類的靜態屬性port的ID
jmethodID getPort = env->GetStaticMethodID(clazz,"getPort","(V)I");//獲取MediaPlayer類的靜態方法getPort()的ID

 JNI操作Java屬性和方法
  •  操作Java屬性
  當通過GetFieldID方法獲取到Java對象的屬性ID和通過GetStaticMethodID方法獲取到Java類的靜態屬性ID後,就可以使用JNIEnv中提供的方法來訪問Java對象的屬性和Java類中的靜態屬性。在jni.h文件中定義了訪問Java對象屬性和Java類的靜態屬性的方法:
jobject GetObjectField(jobject obj, jfieldID fieldID);
jboolean GetBooleanField(jobject obj, jfieldID fieldID);
jint GetIntField(jobject obj, jfieldID fieldID);
...

void SetObjectField(jobject obj, jfieldID fieldID, jobject value);
void SetBooleanField(jobject obj, jfieldID fieldID, jboolean value);
void SetIntField(jobject obj, jfieldID fieldID, jint value);
...
 Java對象獲取屬性的方法和設置屬性的方法爲:
  j<類型> Get<類型>Field(jobject obj,jfieldID fieldID);
  void Set<類型>Field(jobject obj,jfieldID fieldID,j<類型> value);
  其中類型表示Java中的基本類型,obj表示需要獲取的Java對象,fieldID表示需要獲取Java對象的屬性ID,value表示要設置的屬性值;
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID);
jboolean GetStaticBooleanField(jclass clazz, jfieldID fieldID);
jint GetStaticIntField(jclass clazz, jfieldID fieldID);
...
void SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject value);
void SetStaticBooleanField(jclass clazz, jfieldID fieldID, jboolean value);
void SetStaticIntField(jclass clazz, jfieldID fieldID, jint value);
....

  Java類靜態屬性的獲取方法和設置方法爲:
  j<類型> GetStatic<類型>Field(jclass clazz,jfieldID fieldID); 
  void SetStatic<類型>Field(jclass clazz,jfieldID fieldID,j<類型> value);
  其中類型爲Java中的基本類型,clazz表示需要Java類對象,filedID表示類的靜態屬性ID,value表示要設置的靜態屬性值。
  舉個例子來說:
 static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    ...
jclass clazz = env->GetObjectClass(thiz);//獲取MediaPlayer類class
//獲取和設置Java對象的屬性
jfieldID name = env->GetFieldID(clazz,"name","Ljava/lang/String;");//獲取MediaPlayer對象的屬性name的ID
jstring newStr = env->NewStringUTF("local media player");
env->SetObjectField(thiz,name,newStr);//設置MediaPlayer對象的屬性name的值

//獲取和設置類的靜態屬性
jfieldID port =  env->GetStaticFieldID(clazz,"port","I");//獲取MediaPlayer類的靜態屬性port的ID
jint portNum = env->GetStaticIntField(claszz,port);//獲取MediaPlayer類靜態屬性port的值
env->SetStaticIntField(clazz,port,portNum + 100);//設置MediaPlayer類靜態屬性port的值
    ...
}

  • 操作Java方法
   當通過GetMethodID方法獲取到Java對象的方法ID和通過GetStaticMethodID方法獲取到Java類的靜態方法ID後,就可以使用JNIEnv中提供的方法來調用Java對象的方法和Java類中的靜態方法。在jni.h文件中定義了訪問Java對象方法和Java類的靜態方法的方法:

  調用Java對象的成員方法:
  Call<類型>Method(jobject obj,jmethodID methodID,...);
  Call<類型>MethodV(jobject obj, jmethodID methodID, va_list args);
  Call<類型>MethodA(jobject obj, jmethodID methodID, jvalue* args);

  調用Java類的靜態方法:
  CallStatic<類型>Method(jobject obj,jmethodID methodID,...);
  CallStatic<類型>MethodV(jobject obj, jmethodID methodID, va_list args);
  CallStatic<類型>MethodA(jobject obj, jmethodID methodID, jvalue* args);
  其中類型爲Java的基本類型,例如int、char等,代表方法的返回值類型;obj爲成員方法所屬的對象;methodID爲通過GetMethodID方法獲取到的方法ID;最後一項參數表示方法的參數列表,...表示的是變長參數,以“V”結束的方法名錶示以向量形式提供參數列表,以“A”結尾的方法名錶示以jvalue數組形式提供參數列表,這兩種方式調用比較少。
  例如調用返回值爲Void類型的Java對象的成員方法
void CallVoidMethod(jobject obj, jmethodID methodID, ...);
void CallVoidMethodV(jobject obj, jmethodID methodID, va_list args);
void CallVoidMethodA(jobject obj, jmethodID methodID, jvalue* args);

  調用返回值爲Void類型的Java類的靜態方法
void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...);
void CallStaticVoidMethodV(jclass clazz, jmethodID methodID, va_list args);
void CallStaticVoidMethodA(jclass clazz, jmethodID methodID, jvalue* args);
  
  舉個例子來說:
static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    ...
jclass clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類class
jmethodID getHost = env->GetMethodID(clazz, "getHost", "(V)Ljava/lang/String;");//獲取MediaPlayer對象方法getHost()的ID
jmethodID getPort = env->GetStaticMethodID(clazz,"getPort","(V)I");//獲取MediaPlayer類的靜態方法getPort()的ID
env->CallObjectMethod(this,getHost);//調用MediaPlayer的成員方法getHost()
env->CallStaticIntMethod(clazz,getPort);//調用MediaPlayer類的靜態方法getPort()
    ...
}


 在本地代碼中創建Java對象
  • 在本地代碼中創建Java對象
  在JNIEnv的函數表中有幾個方法被用來創建一個Java對象:
jobject NewObject(jclass clazz, jmethodID methodID, ...);
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args);
jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args);
  和Java方法調用一樣,創建對象也有三種形式。其中clazz參數代表需要創建對象的class類;methodID表示創建對象的構造方法ID;
  最後一項參數表示方法的參數列表,...表示的是變長參數,以“V”結束的方法名錶示以向量形式提供參數列表,以“A”結尾的方法名錶示以jvalue數組形式提供參數列表。
  由於構造方法比較特別,與類名一樣,並且沒有返回值,所以通過GetMethodID來獲取構造方法的ID時,第二個參數name固定爲類名或者用"<init>"代替類名,第三個參數sig與構造函數有關,默認的構造函數是沒有參數的。
  例如,調用MediaPlayer的默認構造函數,方法如下:
jclass clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類class
jmethodID ctor = env->GetMethodID(clazz,"<init>","(V)V");//獲取默認構造方法ID
jobject mediaPlayer = env->NewObject(clazz,ctor);//調用構造函數創建MediaPlayer對象

  • 在本地代碼中創建String對象
  在Java中,字符串String對象是Unicode(UTF-16)編碼,每個字符不論是中文還是英文,一個字符都是佔用兩個字節。而在C/C++中一個字符是佔用一個字節的,寬字符是佔用兩個字節的。
  如果在C/C++代碼中需要創建一個Java端的String對象,則需要創建一個寬字符串或者是一個UTF-8編碼的字符串來與Java端的String對象匹配。
  根據傳入的寬字符串創建一個Java String對象的方法如下:
  jstring NewString(const jchar* unicodeChars, jsize len);
  
  根據傳入的UTF-8字符串創建一個Java對象的方法如下:
  jstring NewStringUTF(const char* bytes);
  從Java屬性中獲取的String屬性,在本地方法中對應的都是jstring類型。jstring不是C/C++中的字符串,所以需要通過JNIEnv中的函數來將Java的String類型轉換爲C/C++中的字符串類型。
  將一個jstring對象轉換爲(UTF-8)編碼的字符串(char*);
const char* GetStringUTFChars(jstring string, jboolean* isCopy);
  
  將一個jstring對象轉換爲(UTF-16)編碼的寬字符串(jchar*);
const jchar* GetStringChars(jstring string, jboolean* isCopy);
typedef unsigned short  jchar;          /* unsigned 16 bits */

  這兩個函數中的參數,第一個參數傳入的是一個指向Java String對象的引用;第二個參數傳入的是一個jboolean類型的指針,其值可以爲NULL、JNI_TRUE、JNI_FLASE。如果jboolean爲JNI_TRUE,則表示在本地開闢內存,然後把Java中的String拷貝到這個內存中,然後返回指向這個內存地址的指針。如果jboolean爲JNI_FALSE,則直接返回指向Java中String的指針。Java中String對象和本地方法C/C++字符串將共用同一個內存地址的指針,在本地方法中修改指針指向的內容,將修改Java中String對象的內容,這將破壞String在Java中始終是常量的原則。如果是NULL,則表示不關心是否拷貝字符串。
  通過上述兩個方法獲取Java String對象的字符串,在使用完字符串後,要釋放字符串所佔用的內存,可以使用下列的兩個函數來釋放字符串佔用的內存。

void ReleaseStringChars(jstring string, const jchar* chars);
void ReleaseStringUTFChars(jstring string, const char* utf);
  第一個參數string代表指向的Java中String對象;第二個參數代表需要釋放的本地字符串;
  例如:
const char *tmp = env->GetStringUTFChars(path, NULL);
env->ReleaseStringUTFChars(path, tmp);

  • 在本地代碼中處理Java數組
  在本地代碼中處理Java數組的一個大致流程如下:
  1. 通過GetFieldID方法獲取Java數組變量的ID;
  2. 通過GetObjectField方法獲取Java數組變量的值,保存到jobject中。
  3. 強制將jobject對象轉換爲j<類型>Array類型;
  4. 將j<類型>Array類型轉換爲C/C++中的數組。
  j<類型>Array類型是JNI定義的一個對象類型,並不是C/C++中的數組,例如int[]數組、double[]數組,因此要藉助JNIEnv中定義了一系列方法來把一個j<類型>Array類型轉換爲C/C++數組。
jsize GetArrayLength(jarray array);//獲取數組的長度
  
  //Object數組的操作
jobjectArray NewObjectArray(jsize length, jclass elementClass,jobject initialElement);//創建Object對象數組
  length:代表創建對象數組的長度;
  elementClas:代表對象數組的元素類型;
  initialElement:代表對象數組元素的初始值;

jobject GetObjectArrayElement(jobjectArray array, jsize index);//相當於array[index]
  array:代表要操作的數組
  index:代表要操作數組元素的下標

void SetObjectArrayElement(jobjectArray array, jsize index, jobject value);//相當於arry[index] = value
  array:代表要操作的數組
  參數:代表需要操作數組元素的下標
  參數:代表需要設置的數組元素的值

  注意在JNI中,沒有提供直接將Java的對象數組Object[]直轉換爲C++中的Object[]數組的方法,只能通過GetObjectArrayElement和SetObjectArrayElement方法來對Java的Object數組進行操作。

 基本類型數組的操作
  j<類型>  New<類型>Array(jsize length);//創建基本類型的數組
  類型爲基本類型,例如int、char等;參數length表示需要創建數組的長度;

  j<類型>* Get<類型>ArrayElements(j<類型>Array,jboolean* isCopy);//獲取基本類型的數組
  array:表示需要轉換的Java的基本數組類型;
  isCopy:表示是否需要拷貝到本地。isCopy的取值有JNI_TRUE和JNI_FALSE和NULL。當isCopy爲JNI_TRUE,則表示需要在本地拷貝一份,並返回一個指向本地拷貝的指針;當isCopy爲JNI_FALSE時,則把指向Java數組的指針直接返回給本地代碼,isCopy的取值爲NULL則不關心拷貝情況。
當處理完本地代碼中的數組之後,需要調用Release<類型>ArrayElements方法來釋放數組所佔用的資源。

  void Release<類型>ArrayElements(j<類型>Array array, j<類型>* elems,jint mode);//釋放基本數組所佔用的內存資源
  array:代表Java中基本數組,
  elems:代表需要釋放的C/C++中的基本數組
  mode:mode的取值可以爲0、JNI_COMMIT、JNI_ABORT。當mode爲0時,表示對Java數組進行更新並釋放C/C++數組。當mode爲JNI_COMMIT時,表示對Java數組進行更新,但是不釋放C/C++的數組。當mode爲JNI_ABORT時,表示對Java數組不進行更新,釋放C/C++數組。
上面的這些基本數組的操作,可以完成把Java基本類型的數組轉換到C/C++中的數組。
  例如,類型爲char時,創建、獲取和釋放char的數組方法如下:
jcharArray NewCharArray(jsize length);
jchar* GetCharArrayElements(jcharArray array, jboolean* isCopy);
void ReleaseCharArrayElements(jcharArray array, jchar* elems,jint mode);
  具體的例如如下:
static void android_media_MediaPlayer_release(JNIEnv *env, jobject thiz)
{
     jclass clazz = env->FindClass("android/media/MediaPlayer");//獲取MediaPlayer類class
     jfieldID arrayID = env->GetFieldID(clazz,"arrays", "[I");
     jintArray array = (jintArray)(env->GetObjectField(thiz, arrayID));
     jint*  int_array = env->GetIntArrayElements(arr, NULL);
     jsize  len = env->GetArrayLength(array);
     env->ReleaseIntArrayElements(array, int_array, JNI_ABORT);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章