JAVA JNI的基本總結一籮筐

 JNI的基本原理
** 在Java中調用C庫函數

開發流程
------
在Java代碼中通過JNI調用C函數的步驟如下:

第一步: 編寫Java代碼

第二步: 編譯Java代碼

第三步: 生成C語言頭文件

第四步: 編寫C代碼

第五步: 生成C共享庫

第六步: 運行Java程序

*** 第一步 編寫Java代碼

JNI方法是在Java代碼中聲明的。

在Java類中,使用"native"關鍵字,聲明本地方法該方法與用C/C++編寫的JNI本地函數相對應。"native"關鍵字告知Java編譯器,在Java代碼中帶有該關鍵字的方法只是聲明,具體由C/C++等其他語言編寫實現。

如果起吊方法前的native關鍵字,編譯代碼時,Java編譯器就會報錯,拋出編譯錯誤,告知該方法沒有實現。

調用System.loadLibrary()方法加載具體的實現本地方法的C運行庫。System.loadLibrary()方法加載由字符串參數指定的本地庫,在不同操作系統平臺下,加載的C運行庫不同。

*** 第二步 編譯Java代碼

#+BEGIN_SRC java
javac xxx.java
#+END_SRC

生成 xxx.class

*** 第三步 生成C語言頭文件

#+BEGIN_SRC java
javah -classpath path classname
#+END_SRC

生成classname.h

| Java類型 | Java本地類型 |
|----------+--------------|
| /        | <            |
|----------+--------------|
| byte     | jbyte        |
| short    | jshort       |
| int      | jint         |
| long     | jlong        |
| float    | jfloat       |
| double   | jdouble      |
| char     | jchar        |
| boolean  | jboolean     |
| void     | void         |

Java本地類型也提供了另外三種類型

| java引用類型 | java本地類型 |
|--------------+--------------|
| /            | <            |
|--------------+--------------|
| 對象         | Jobject      |
| String       | Jstring      |

*** 第四步 編寫C/C++代碼

編寫xxx.c文件

*** 第五步 生成C共享庫

#+BEGIN_SRC sh
cc -I/usr/lib/jvm/java-6-sun/include/linux
   -I/usr/lib/jvm/java-6-sun/include/
   -fPIC -shared -o libxxx.so xxx.c
#+END_SRC

*** 第六步 運行Java程序

#+BEGIN_SRC java
java -cp path -o java.library.path='path' classname
#+END_SRC

** 小結

(1)在java類中聲明本地方法

(2)使用javah命令,生成包含JNI本地函數原型的頭文件

(3)實現JNI本地函數

(4)生成C共享庫

(5)通過JNI,調用JNI本地函數

* 調用JNI函數

在由C語言編寫的JNI本地函數中如何控制Java端的代碼

- 創建Java對象

- 訪問靜態成員域

- 調用類的靜態方法

- 訪問Java對象的成員變量

- 訪問Java對象的方法

** 調用JNI函數的示例程序結構

** Java層代碼 (JniFuncMain.java)

1.JniFuncMain類
#+BEGIN_SRC java
public class JniFuncMain
{
    print static int staticIntField = 300;

    // 加載本地庫
    static { System.loadLibrary("jnifunc"); }

    // 本地方法聲明

    public static native JniTest createJniObject();

    public static void main(String[] args)
    {
        // 從本地代碼生成JniTest對象
	System.out.println("[Java] createJniObject() 調用本地方法");
	JniTest jniObj = createJniObject();

	// 調用JniTest對象的方法
	jniObj.callTest();
    }
}
#+END_SRC
JniFuncMain.java中的JniFuncMain類

+ 通過java靜態塊,在調用本地方法前,加載jnifunc運行庫

+ 使用static關鍵字聲明本地方法createJniObject()在調研那個此方法時不需要創建對象,直接通過JniFuncMain類調用即可

+ 不使用Java語言的new運算符,調用與createJniObject()本地方法相對應的C函數生成JniTest類的對象,在將對象的引用保存在jniObj引用變量中

+ 調用jniObj對象的callTest()方法

2.JniTest類

#+BEGIN_SRC java
class JniTest 
{
    private int intField;
    //構造方法

    public JniTest(int num)
    {
        intField = num;
	System.out.println("[Java] 調用JniTest對象的構造方法:intField = " + intField);
    }

    // 此方法由JNI本地函數調用
    public int callByNative(int num)
    {
        System.out.println("[Java] JniTest 對象的 callByNative("+ num +")調用");
	return num;
    }

    public void callTest() 
    {
        System.out.println("[Java] JniTest 對象的 callTest() 方法調用:intField="intField");
    }
}
#+END_SRC

** 分析JNI本地函數代碼

**** JniFuncMain.h頭文件

使用javah命令,生成本地方法的函數原型
#+BEGIN_SRC java
javah JniFuncMain
#+END_SRC
JniFuncMain.h
#+BEGIN_SRC c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniFuncMain */

#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniFuncMain
 * Method:    createJniObject
 * Signature: ()LJniTest;
 */
JNIEXPORT jobject JNICALL Java_JniFuncMain_CreateJniObject(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
#+END_SRC

createJniObject()本地方法對應的JNI本地函數原型,形式如下

JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *, jclass)

**** jnifunc.cpp 文件

#+BEGIN_SRC C++
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz)
{
    jclass targetClass;
    jmethodID mid;
    jobject newObject;
    jstring helloStr;
    jfieldID fid;
    jint staticIntField;
    jint result;

    // 獲取JniFuncMain類的staticIntField變量值
    fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
    staticIntField = env->GetStaticIntField(clazz, fid);
    printf("[CPP] 獲取JniFuncMain類的staticIntField值\n");
    printf("         JniFuncMain.staticIntField = %d\n", staticIntField);

    // 查找生成對象的類
    targetClass = new->NewObject(targetClass, mid , 100);

    // 查找構造方法
    mid = env->GetMethodID(targetClass, "<init>", "(I)V");

    // 生成JniTest對象(返回對象的引用)
    printf("[CPP]JniTest對象生成\n");
    newObject = env->NewObject(targetClass, mid, 100);

    // 調用對象的方法
    mid = env->GetMethodID(targetClass,"callByNative", "(I)I");
    result = env->CallIntMethod(newObject, mid , 200);

    //設置JniObject對象的intField值
    fid = env->GetFieldID(targetClass, "intField", "I");
    printf("[CPP] 設置JniTest對象的intField值爲200\n");
    env->SetIntField(newObject, fid, result);

    //返回對象的引用
    return newObject;
}
#+END_SRC

**** 通過JNI,獲取成員變量值

下面代碼用於獲取JniFuncMaind類的staticIntField成員變量的值

#+BEGIN_SRC c
// 1. 查找含有待放文成員變量的JniFuncMain類的jclass值
// 2. 獲取staticField變量的ID值
fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
// 3. 讀取jclass與fieldid指定的成員變量值
staticIntField = env->GetStaticIntField(clazz, fid);
#+END_SRC

程序通過JNI訪問java類/對象的成員變量安如下順序進行:

(1) 查找含待放文的成員變量的Java類的jclass值
(2) 獲取此類成員變量的jfieldID值。若成員變量爲靜態變量,則調用名稱爲GetStaticFieldID()的JNI函數;若待訪問的成員變量是普通對象,則調用名稱爲GetFieldID()的JNI函數。
(3) 使用12中獲得的jclass與jfieldID值,獲取或設置成員變量值。

依據以上順序,待讀取樹脂的staticIntField成員變量在JniFuncMain類被聲明。JniFuncMain類的jclass值被傳遞給JNI本地函數 =java_JniFuncMain_createJniObject()= 的第二個參數中,若想獲取指定類的jclass值,調用JNI函數FindClass()即可。

若想在本地代碼中訪問Java的成員變量,必須獲取相應成員變量的ID值。例子中成員變量的ID保存在jfieldID類型的變量中。由於待讀取數值的staticIntField成員變量時JniFUncMain類的靜態變量,在獲取staticIntField的ID時,影調用名稱爲GetStaticFieldID()的JNI函數。

在例子中的GetStaticFieldID()函數,與下表中的GetStaticFieldID()函數原型有些不同,函數原型中帶有四個參數,而代碼中僅有三個,缺少了env參數,這不是錯誤,而是與所用的編程語言相關。具體請參考後面Tip中關於JNI函數編碼風格的說明。

| JNI函數 - GetStaticFieldID() |                                                                                            |
|------------------------------+--------------------------------------------------------------------------------------------|
| /                            | <                                                                                          |
| 形式                         | jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char*name, const char *signature) |
|------------------------------+--------------------------------------------------------------------------------------------|
| 說明                         | 返回指定類的指定的靜態變量的jfieldID的值                                                   |
|------------------------------+--------------------------------------------------------------------------------------------|
| 參數                         | env-JNI接口指針 clazz-包含成員變量的類的jclass name-成員變量名 signature-成員變量簽名      |

| JNI函數 - GetFieldID() |                                                                                       |
|------------------------+---------------------------------------------------------------------------------------|
| /                      | <                                                                                     |
| 形式                   | jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature) |
|------------------------+---------------------------------------------------------------------------------------|
| 說明                   | 返回對象中指定的成員變量的jfieldID的值                                                |
|------------------------+---------------------------------------------------------------------------------------|
| 參數                   | env-JNI接口指針 clazz-包含成員變量的類的jclass name-成員變量名 signatuer-成員變量簽名 |

以上兩個函數都要去提供成員變量的簽名。成員變量與成員方法都擁有簽名,使用 =<JDK_HOME>/bin= 目錄下的javap命令(java反編譯器),可以獲取成員變量活成員方法簽名。

Tip: 在JNI中獲取成員變量活成員方法簽名

形式: javap =[選項]=  '類名'

選項: -s 輸出java簽名
      -p 輸出所有類及成員

在獲取成員變量所在的類與ID後,根據各個成員變量的類型與存儲區塊(static或non-static),調用相應的JNI函數讀取成員變量值即可。在JNI中有兩種函數用來獲取成員便令的值,分別爲Get<type>Field函數與GetStatic<type> Field函數。<type>指Int, Char, Double等基本數據類型,具體參考JNI文檔。

| JNI函數 GetStatic<type>Field |                                                                                                      |
|------------------------------+------------------------------------------------------------------------------------------------------|
| /                            | <                                                                                                    |
| 形式                         | <jnitype>GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID)                           |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 說明                         | 返回clazz類中ID爲fieldID的靜態變量的值                                                               |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 參數                         | env-JNI接口指針 clazz-包含成員變量的類 fieldID-成員變量的ID                                          |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 參考                         | <type>指Object、Boolean、Byte、Char、Short、Int、Long、Float、Double九種基本類型                     |
|                              | 返回類型<jnitype>指jobject、jboolean、jbyte、jchar、jshort、jint、jlong、jfloat、jdouble九種基本類型 |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 返回值                       | 返回靜態成員變量的值                                                                                 |

| JNI函數 Get<type>Field |                                                                     |
| /                      | <                                                                   |
|------------------------+---------------------------------------------------------------------|
| 形式                   | <jnitype>Get<type>Field(JNIEnv *env, Jobject obj, jfieldID fieldID) |
|------------------------+---------------------------------------------------------------------|
| 說明                   |     返回obj對象中ID爲fieldID的成員變量的值                                       |
|------------------------+---------------------------------------------------------------------|
| 參數                   |      env-JNI接口指針                                                    |
|                        |      obj-包含成員變量的對象                                                  |
|                        |       fieldID-成員變量的ID                                               |
|------------------------+---------------------------------------------------------------------|
| 返回值                    |       返回成員變量的值                                                      |
由於staticIntField是Int類型的靜態成員變量,所以調用GetStaticFieldID()函數即可獲取StaticIntField的值.

生成對象

在JNI本地函數中如何生成Java類對象呢?
-----
// 1. 查找生成對象的類
targetClass = env->FindClass("JniTest");

// 2. 查找類的構造方法
mid = env->GetMethodID(targetClass, "<init>", "(I)V");

// 3. 生成JniTest類對象(返回對象引用)
newObject = env->NewObject(targetClass, mid, 100);
-----

通過JNI函數,生成Java對象的順序如下:
1. 查找指定的類,並將查找到的類賦值給jclass類型的變量。

2. 查找java類構造方法的ID值,類型爲jmethodID。
 
3. 生成java類對象

首先調用JNI函數FindClass(),查找生成對象的類。將類名作爲FindClass()函數參數,查找並獲得jclass值

| JNI函數 FindClass |                                                 |
|-------------------+-------------------------------------------------|
| 形式              | jclass FindClass(JNIEnv *env, const char *name) |
|-------------------+-------------------------------------------------|
| 說明              | 查找name指定的Java類                                  |
|-------------------+-------------------------------------------------------|
| 參數              |  env-JNI接口指針                                    |
|                   | name-待查找的類名                               |
|-------------------+-------------------------------------------------|
| 返回值            | 返回jclass的值                                  |

獲取類的構造方法的ID並保存在jmethodID變量中。在JNI函數中有一個GetMethodID()函數用來獲取指定類的指定方法ID。此函數除了可以用來獲取指定類的構造方法的ID外,還可以獲取類的其他的方法的ID。若指定的是靜態方法,則可以調用JNI函數中的GetStaticMethodID()函數,獲得指定靜態方法的ID。

| JNI函數 GetMethodID |                                                                                                           |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 形式                | jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature)                 |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 說明                | 獲取clazz類對象的指定方法ID。注意,方法名(name)與簽名應當保持一致。若獲取類構造方法的ID,方法名應爲<init> |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 參數                | env: JNI接口指針                                                                                          |
|                     | clazz:Java類                                                                                              |
|                     | name:方法名                                                                                              |
|                     | signature:方法簽名                                                                                       |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 返回值           | 若方法ID錯誤,則返回NULL                                                                                           | 

以類的jclass與構造方法ID爲參數,調用函數NewObject()函數生成JniTest類的對象。JniTest類的構造方法JniTest(int num)帶有一個int類型的參數,在調用NewObject()時,同時傳入100這一int數據。在生成類對象後,將對象的引用保存在jobject變量中。

| JNI函數 NewObject |                                                                       |
|-------------------+-----------------------------------------------------------------------|
| 形式              | jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...) |
|-------------------+-----------------------------------------------------------------------|
| 說明              | 生成指定類的對象。methodID指類的構造方法的ID                                           |
|-------------------+-----------------------------------------------------------------------|
| 參數              |     env:JNI接口指針                                                       |
|                   |    clazz: Java類                                                       |
|                   |    methodID:類的構造方法的ID                                                 |
|                   |    ...:傳遞給類構造方法的參數                                                    |
|-------------------+-----------------------------------------------------------------------|
| 返回值            |   返回類對象的引用。若發生錯誤,返回NULl                                               |

Tip: 局部引用與全局引用

在實現JNI本地函數時,由GetObjectClass()、FindClass()等JNI函數返回的jclass\jobject等引用都是局部引用(Local Reference)

局部引用是JNI默認的,它僅在JNI本地函數內部有效,即當JNI本地函數返回後,其內部的引用就會失效。

在JNI編程中,實現JNI本地函數時,必須準確地理解局部引用的含義。
下面再舉一個例子進一步詳細的說明一下。

#+BEGIN_SRC java
class RefTest
{
    public static int intField;

    public static void setField(int num) {
        int Field = num;
    }
}

public class RefTestMain
{
    // 加載本地庫
    static { System.loadLibrary("reftest"e); }

    // 聲明本地方法
    public static native int getMember();

    public static void main(String[] args) {
        RefTest.setField(100);
	System.out.println("intField = " + getMember());
	RefTest.setField(200);
	System.out.println("intField = " + getMember());
    }
}
#+END_SRC

其中,本地方法getMember()的具體實現在reftest.cpp中。爲了說明局部引用問題,聲明瞭一個靜態jclass變量targetClass,準備保存類的引用。
#+BEGIN_SRC c
static jclass targetClass = 0;
JNIEXPORT jint JNICALL Java_RefTestMain_getMember(JNIEnv *env, jclass clazz)
{
    jfieldID fid;
    jint intField;
    jclass targetClass;

    if(targetClass == 0) {
        targetClass = env->FindClass(RefTest");
    }
    fid = env->GetStaticFieldID(targetClass, "intField", "I");
    intField = env->GetStaticIntFIeld(targetClass, fid);

    return intField;
}
#+END_SRC

運行程序會報錯,原因在於JNI函數中的if (targetClass == 0)的判斷,在java中的兩次調用,第一次調用時targetClass還爲0,第二次就不爲0了。第二次沒有調用FindClass造成出現錯誤。

爲了解決這一問題,JNI提供了一個名爲NewGlobalRef()的JNI函數,用來爲指定的類或對象生成全局引用(Global Reference),以便在JNI本地函數中在全局範圍內使用該引用。

| JNI函數 NewGlobalRef |                                                |
|----------------------+------------------------------------------------|
| 形式                 | jobject NewGlobalRef(JNIEnv *env, jobject obj) |
|----------------------+------------------------------------------------|
| 說明                 | 爲obj指定的類或對象,生成全局引用                             |
|----------------------+------------------------------------------------|
| 參數                 |   env: JNI接口指針                                 |
|                      |   obj: 待生成全局引用的引用值                             |
|----------------------+------------------------------------------------|
| 返回值               | 返回生成的全局引用,所發生錯誤,返回NULL       |

當全局引用使用完後,應當調用名稱爲DeleteGlobalRef()的JNI函數,顯性的將全局引用銷燬。

#+BEGIN_SRC c
#include "RefTestMain.h"

static jclass globalTargetClass = 0;

JNIEXPORT jint JNICALL Java_RefTestMain_getMember (JNIEnv *env, jclass jclazz)
{
    jfieldID fid;
    jint intField;
    jclass targetClass;
    
    if(globalTargetClass == 0) {
        targetClass = env->FindClass("RefTest");
	globalTargetClass = (jclass)env->NewGlobalRef(targetClass);
    }

    fid = env->GetStaticFieldID(globalTargetClass, "initField", "I");
    intField = env->GetStaticIntField(globalTargetClass, fid);

    return intField;
}
#+END_SRC

上面代碼調用了NewGlobalRef()函數,將targetClass中保存的RefTest類的局部引用(由FindClass()函數返回)轉換成全局引用。並且將生成的全局引用保存在globalTargetClass靜態變量中。

局部引用在函數執行完程後即無效。而全局引用除非調用DeleteGlobalRef()明確將其銷燬,不然這個全局引用總是有效的,可以在運行庫的其他函數中使用該引用。

**** 調用Java方法

下面描述瞭如何使用JNI函數調用Java方法,並將返回值保存至JNI本地函數的變量中的過程
-----
// 1. 獲取含待調用方法的Java類的jclass
targetClass = env->GetObjectClass(newObject);

// 2. 獲取待調用方法的ID
mid = env->GetMethodID(targetClass, "callByNative", "(I)I");

// 3. 調用Java方法 保存返回值
result = env->CallIntMethod(newObject, mid, 200);
-----

通過JNI調用Java方法的順序如下
1. 獲取含待調用方法的Java類的jclass。若待調用方法屬於某個Java類對象,則該方法用來獲取Java類對象的jobject。

2. 調用GetMethodID()函數,獲取待調用方法的ID(jMethodID)。使用jclass與GetMethodID()函數

3. 根據返回值類型,調用相應的JNI函數,實現對Java方法的調用。若待調用的Java方法是靜態方法,則調用函數的形式應爲CallStatic<type>Method();若待調用的方法屬於某個類對象,則調用函數的形式應爲Call<type>Method()。

程序首先獲取含callByNative()方法的JniTest類的jclass。在獲取JniTest類的jclass時,可以直接調用FindClass()函數,將類引用保存在targetClass中。但是爲了向各位介紹GetObjectClass()這個JNI函數,因而在此調用了GetObjectClass()函數。

| JNI函數 CallStatic<type>Method() |                                                                                    |
|----------------------------------+------------------------------------------------------------------------------------|
| 形式                             | <jnitype>CallStatic<type>Method(JNIEnv *env, jcalss clazz,jmethodID methodID, ...) |
|----------------------------------+------------------------------------------------------------------------------------|
| 說明                             |調用methodID指定的類的靜態方法                                                                 |
|----------------------------------+------------------------------------------------------------------------------------|
| 參數                             |     env: JNI接口指針                                                                   |
|                                  |    clazz: 含待調方法的類                                                                  |
|                                  |    methodID:待調方法的ID 由GetStaticMethodID()函數獲取                                       |
|                                  |   ...:傳遞給待調方法的參數                                                                   |
|----------------------------------+------------------------------------------------------------------------------------|
| 返回值                           |      被調方法的返回值                                                                      |
|----------------------------------+------------------------------------------------------------------------------------|
| 參考                             |     <type>除了前面說<Get<type>FieldID()時列出的九種外又添加了void類型,返回值<jnitype>也增加了void類型。 待調方法的返回值不同,<type>也不同。若待調方法的返回值類型爲int, 則調用函數爲CallStaticIntMethod() |

| JNI函數 Call<type>Method() |                                                                               |
|----------------------------+-------------------------------------------------------------------------------|
| 形式                       | <jnitype>Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...) |
|----------------------------+-------------------------------------------------------------------------------|
| 說明                       | 調用methodID指定的java對象的方法                                                        |
|----------------------------+-------------------------------------------------------------------------------|
| 參數                       |     env: JNI接口指針                                                              |
|                            |    obj: 含待調方法的Java對象的引用                                                       |
|                            |    methodID: 待調用方法的ID,由GetMethodID()函數來獲取                                     |
|                            | ...: 傳遞給待調用方法的參數                                                   |
|----------------------------+-------------------------------------------------------------------------------|
| 返回值                  | 被調用方法的返回值                                                                     | 

**** 通過JNI設置成員變量的值  

-----
// 1. 獲取含IntField成員變量的JniTest類的jclass值
// 類引用已經被保存到targetClass中

// 2. 獲取JniTest對象的IntField變量值
fid = env->GetFieldID(targetClass, "intField", "I");

// 3. 將result值設置爲IntField值
env->SetIntField(newObject, fid, resutl);
-----

| JNI函數 SetStatic<type>Field |                                                                                     |
|------------------------------+-------------------------------------------------------------------------------------|
| 形式                         | void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, <type>value) |
|------------------------------+-------------------------------------------------------------------------------------|
| 說明                         | 設置fieldID指定的Java類靜態成員變量的值                                                           |
|------------------------------+-------------------------------------------------------------------------------------|
| 參數                         |      env: JNI接口指針                                                                   |
|                              |     clazz: 含待設置成員變量的類的引用                                                            |
|                              |     fieldID: 待設成員變量的ID,由GetStaticFieldID()函數獲取                                      |
|                              |     value: 指定設置值                                                                    | 

| JNI函數 Set<type>Field |                                                                                |
|------------------------+--------------------------------------------------------------------------------|
| 形式                   | void Set<type>Field (JNIEnv *env, jobject obj, jfieldID fieldID, <type> value) |
|------------------------+--------------------------------------------------------------------------------|
| 說明                   | 設置fieldID指定的Java對象的成員變量                                            |
|------------------------+--------------------------------------------------------------------------------|
| 參數                   |   env: JNI接口指針                                                                 |
|                        |   obj: 包含待設成員變量的Java對象的引用                                                      |
|                        |    fieldID: 待設成員變量的ID,由GetFieldID()函數獲取                                        |
|                        |    value:指定設置值                                                                 | 

* 在C程序中運行Java類

本節中學習在由C/C++編寫的主程序中如何運行Java類,這也是使用JNI的重要方式。

在C/C++程序中運行Java類也必須使用Java虛擬機。爲此JNI提供了一套Invocation API,它允許本地代碼在自身內存區域內加載Java虛擬機。

下面列出的可能使你決定使用Invocation API在C/C++代碼中調用Java代碼的集中典型情況:

+ 需要在C/C++編寫的本地應用程序中訪問用Java語言編寫的代碼或代碼庫

+ 希望在C/C++編寫的本地應用程序中使用標準的Java庫

+ 當需要把自己已有的C/C++程序與Java程序組織鏈接在一起時,使用Invocation API可以將它們組織成一個完整的程序

** Invocaton API 應用示例
實例程序由InvokeJava.cpp與InvocationTest.java兩個文件構成

示例程序將按如下順序執行:

(1) 主程序InvokeJava.cpp使用Invocation API加載Java虛擬機。

(2) 通過JNI函數加載InvocationTest類至內存中

(3) 執行被加載的InvocatonTest類main()方法

*** 分析Java代碼 InvocationApiTest.java

#+BEGIN_SRC java
public class InvocationAPiTest 
{
    public static void main(String[] args)
    {
        System.out.println(args[0]);
    }
}
#+END_SRC
=僅含有一個main()方法,該main()方法是一個靜態方法,帶有一個字符串對象數組,在方法體中僅有一條輸出語句,用來降低一個數組元素args[0]中的字符串輸出到控制檯上。=

*** 分析C代碼 invocationApi.c
#+BEGIN_SRC c
#include <jni.h>

int main() 
{
    JNIEnv *env;
    JavaVM *vm;
    JavaVMInitArgs vm_args;
    JavaVMOptions options[1];
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;

    // 1. 生成Java虛擬機選項
    options[0].optionString = "-Djava.class.path=."
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;

    // 2. 生成Java虛擬機
    res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);

    // 3. 查找並加載類
    cls = (*env)->FindClass(env, "InvocationApiTest");

    // 4. 獲取main()方法的ID
    mid = (*env)->GetStaticMethodID(env, cls, "main", ([Ljava/lang/String;)V);

    // 5. 生成字符串對象,用作main()方法的參數
    jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");    
    stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);

    // 6. 調用main()方法
    (*env)->CallStaticVoidMethod(env, cls, mid, args);
    
    // 7. 銷燬Java虛擬機
    (*vm)->DestroyJavaVM(vm);
}
#+END_SRC

下面開始分析代碼的主要部分

#include命令用來將jni.h頭文件包含到本文件中。jni.h頭文件包含C代碼使用JNI必須的各種變量類型或JNI函數的定義,在本地代碼中使用JNI時,必須將此頭文件包含到本地代碼中。

#+BEGIN_SRC java
    // 1. 生成Java虛擬機選項
    options[0].optionString = "-Djava.class.path=."
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;
#+END_SRC
生成一些參數或選項值,這些值在加載Java虛擬機時被引用,用來設置Java虛擬機的運行環境或控制Java虛擬機的運行,如設置CLASSPATH或輸出調試信息等。

在生成Java虛擬機選項時,使用JavaVMInitArgs與JavaVMOption結構體,它們定義在jni.h頭文件中
#+BEGIN_SRC c
typedef struct JavaVMInitArgs {
    jint version;
    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;

typedef struct JavaVMOption {
    char *optionString;
    void *extraInfo;
} JavaVMOption;
#+END_SRC
觀察JavaVMInitArgs結構體定義代碼,可以發現JavaVMInitArgs結構體內包含JavaVMOption結構體的指針。JavaVMOption結構體包含Java虛擬機的各個參數,JavaVMInitArgs結構體用來將這些參數選項傳遞給Java虛擬機。

接下來,看一下結構體中各個成員的含義。

JavaVMInitArgs結構體的versino成員用來指定傳遞誒虛擬機的選項的變量的形式,設定在jni.h頭文件中定義的 =JNI_Version_1_2= 的值。nOptions與options用來指定JavaVMInitArgs所指的JavaVMOption結構體數組值。nOptions指定JavaVMOption結構體數組元素的個數,options用來指向JavaVMOption結構體的地址。示例中只設置了一個Java虛擬機選項,即JavaVMOption結構體數組僅有一個元素,聲明如下
#+BEGIN_SRC c
JavaVMOption options[1];
#+END_SRC
爲了指定以上JavaVMOption結構體數組,需要指定JavaVMInitArgs的options與nOptions
#+BEGIN_SRC c
vm_args.options = options; // JavaVMOption 結構體的地址
vm_args.nOptions = 1; // JavaVMOption 結構體數組元素個數
#+END_SRC

ignoreUnrecognized是JavaVMInitArgs結構體jboolean類型的成員,當Java虛擬機獨到設置錯誤的選項值時,該成員用來決定Java虛擬機是忽略錯誤後繼續執行,還是返回錯誤後終止執行。若ignoreUnrecognized被設置爲 =JNI_TRUE= ,Java虛擬機遇到錯誤選項時,忽略錯誤後繼續執行;若被設置爲 =JNI_FALSE= ,當遇到錯誤選項,Java虛擬機將錯誤返回後終止執行。

接下來分析JavaVMOption結構體,它用來指定Java虛擬機的選項值。若想創建選項值,只要向結構體的optionString成員指定一個字符串,用作Java虛擬機選項的形式。比如示例中的"-Djava.class.path=.",用來設置標準選項,即將Java虛擬機要加載的類的默認目錄設置爲當前目錄(.),其形式爲-Dproperty=value。

#+BEGIN_SRC c
res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
#+END_SRC
本行代碼是整個程序的核心部分,即C應用程序調用 =JNI_CreateJavaVM()= 函數,生成並裝載Java虛擬機。 =JNI_CreateJavaVM()= 函數的第一個參數類型爲JavaVM,它表示Java虛擬機接口,用來生成或銷燬Java虛擬機。DestroyJavaVM()是接口函數之一,該函數用來銷燬Java虛擬機。

在 =JNI_CreateJavaVM()= 函數的第二個參數env中,保存着JNI接口的指針的地址。通過env所指的JNI接口指針,可以使用各種JNI函數,即在C/C++中,通過env,可以生成Java對象,調用相應方法等。

| JNI Invocation API- =JNI_CreateJavaVM= |                                                                 |
|-------------------------------------+-----------------------------------------------------------------|
| 形式                                | jint =JNI_CreateJavaVM= (javaVM **vm, JNIEnv **env, void *vm_args) |
|-------------------------------------+-----------------------------------------------------------------|
| 說明                                | 裝載並初始化Java虛擬機                                                   |
|-------------------------------------+-----------------------------------------------------------------|
| 參數                                |   vm: JavaVM指針的地址                                               |
|                                     | env: JNI接口指針的地址                                          |
|                                     | =vm_args:= 傳遞給Java虛擬機的參數                               |
|-------------------------------------+-----------------------------------------------------------------|
| 返回值                           | 成功,返回0;失敗,返回負值                                                  | 

爲了加載InvocationTest類和執行方法(向main方法傳遞字符串參數"Hello"),首先調用FindClass()函數,裝載InvocationApiTest類。而後調用GetStaticMethodID()函數,獲取main()方法的ID,準備調用main()方法。  

在使用CallStaticVoidMethod()函數調用main()方法之前,首先構造出傳遞給main()方法的參數。Java的main()方法的參數是String[]數組
#+BEGIN_SRC java
public static void main(String[] args)
#+END_SRC

示例中將"Hello Invocation API!!"字符串傳遞給main()方法。首先調用NewStringUTF()函數,將UTF-8形式的字符串,轉換成Java字符串對象String。然後調用NewObjectArray()函數,創建String對象數組,使用創建的String對象將其初始化。先創建一個含有一個元素的String[]數組,而後將"Hello Invocation API!!"字符串賦值給數組的第一個元素。

#+BEGIN_SRC c
    jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");    
    stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
#+END_SRC
調用JNI本地函數處理String對象的方法有些複雜。如果你對此仍迷惑不解,我們不妨將這部分代碼轉換成與其等價的Java代碼。

如下所示,首先創建包含一個元素的字符串數組,而後將"Hello Invocation API!!"字符串賦值給數組的首個元素
#+BEGIN_SRC java
String[] args = new String[1];
args[0] = "Hello Invocation API!"
#+END_SRC

| JNI函數 NeStringUTF |                                                      |
|---------------------+------------------------------------------------------|
| 形式              | jstring NewStringUTF(JNIEnv *env, const char *bytes) |
|---------------------+------------------------------------------------------|
| 說明              | 將UTF-8形式的C字符串轉換成java.lang.String對象       |
|---------------------+------------------------------------------------------|
| 參數              | env: JNI接口指針                                     |
|                     | bytes: 待生成String對象的C字符串的地址               |
|---------------------+------------------------------------------------------|
| 返回值        | 成功,返回String對象的jstring類型的引用;失敗,返回NULL |

| JNI函數 NewObjectArray |                                                                                              |
|------------------------+----------------------------------------------------------------------------------------------|
| 形式                   | jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initalElement) |
|------------------------+----------------------------------------------------------------------------------------------|
| 說明                   | 生成由elementClass對象組成的數組。數組元素個數由length指定,initalElement參數用來初始化對象數組 |
|------------------------+----------------------------------------------------------------------------------------------|
| 參數                 | env: JNI接口指針                                                                             |
|                        | length: 數組元素個數                                                                         |
|                        | elementClass:數組元素對象的類型                                                              |
|                        | initialElement: 數組初始化值                                                                 |
|------------------------+----------------------------------------------------------------------------------------------|
| 返回值              | 若成功,則返回數組引用;失敗,則返回NULL                                                                       |

#+BEGIN_SRC c
(*env)->CallStaticVoidMethod(env, cls, mid, args);
#+END_SRC
=本行代碼通過CallStaticVoidMethod()函數調用InvocationApiTest類的main()方法。在上面創建的Stringp[]數組是CallStaticVoidMethod()函數的第四個參數,該參數會被傳遞給InvocationApiTest類的main()方法。當InvocationApiTest類的main()方法被調用執行時,它會向控制檯輸出args字符串數組的 args[0]元素中的字符串。=

* 直接註冊JNI本地函數

Java虛擬機在運行包含本地方法的Java應用程序時,要經過以下兩個步驟。

1. 調用System.loadLibrary()方法,將包含本地方法具體實現的C/C++運行庫加載到內存中。

2. Java虛擬機檢索加載進來的庫函數符號,在其中查找與Java本地方法擁有相同簽名的JNI本地函數符號。若找到一致的,則將本地方法映射到具體的JNI本地函數。

在Android Framework這類複雜的系統下,擁有大量的包含本地方法的java類,Java虛擬機加載相應的運行庫,再逐一檢索,將各個本地方法與相應的函數映射起來,這顯然會增加運行時間,降低運行的效率。

爲此,JNI機制提供了名稱爲RegisterNatives()的JNI函數,該函數允許C/C++開發者將JNI本地函數與Java類的本地方法直接映射在一起。當不調用RegisterNative()函數時,Java虛擬機會自動檢索並將JNI本地函數與相應的Java本地方法鏈接在一起。但當開發者直接調用RegisterNatives()函數進行映射時,Java虛擬機就不必進行映射處理,這會極大提高運行速度,提升運行效率。

由於程序員直接將JNI本地函數與Java本地方法鏈接在一起,在加載運行庫時,Java虛擬機不必爲了識別JNI本地函數而將JNI本地函數的名稱與JNI支持的命名規則進行對比,即任何名稱的函數都能直接鏈接到Java本地方法上。

** 加載本地庫時,註冊JNI本地函數

#+BEGIN_SRC java
#include "jni.h"
#include <stdio.h>

// JNI本地函數原型
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);

JNIEXPORT jint JNICALL JNI_Onoad(JavaVM *vm, void *reserved)
{
    JNIEnv *env = NULL;
    JNINativeMethod nm[2];
    jclass cls;
    jint result = -1;

    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        printf("Error");
	return JNI_ERR;
    }
    
    cls = env->FindClass("HelloJNI");

    nm[0].name = "printHello";
    nm[0].signature = "()V";
    nm[0].fnPtr = (void*)printHelloNative;

    nm[1].name = "printString";
    nm[1].signature = "(Ljava/lang/String;)V";
    nm[1].fnPtr = (void*)printStringNative;

    env->RegisterNatives(cls, nm, 2);

    return JNI_VERSION_1_4;
}

// 實現JNI本地函數
void printHelloNative(JNIEnv *env, jobject obj)
{
    printf("Hello World!\n");
    return;
}

void printStringNative(JNIEnv *env, jobject obj, jstring string)
{
    const char *str = env->GetStringUTFChars(string, 0);
    printf("%s!\n, str);

    return;
}
#+END_SRC

#+BEGIN_SRC java
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);
#+END_SRC
此兩行代碼用來聲明JNI本地函數原型。如前所述,在使用RegisterNatives()函數機型映射時,不需要將JNI本地函數原型與JNI命名規則進行比對,所以使用的函數名比較簡單。但函數中的兩個公共參數必須指定爲"JNIEnv *env, jobject obj"。

#+BEGIN_SRC java
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        printf("Error");
	return JNI_ERR;
#+END_SRC
在 =JNI_OnLoad()= 函數中首先判斷JNI的版本,即調用GetEnv()函數,判斷Java虛擬機是否支持JNI1.4。若java虛擬機支持JNI1.4, =JNI_OnLoad()= 函數就會返回 =JNI_VERSION_1_4= ;若不支持, =JNI_OnLoad()= 函數就會返回 =JNI_ERR= ,並終止裝載庫的行爲。

當GetEnv()函數調用完畢後,JNI接口指針被保存到env變量中,在調用FindClass()、RegisterNatives()等JNI函數時,可以使用該變量。
| JNI Invocation API - GetEnv |                                                   |
|-----------------------------+---------------------------------------------------|
| 形式                        | jint GetEnv(JavaVM *vm, void **env, jint version) |
|-----------------------------+---------------------------------------------------|
| 說明                        | 判斷Java虛擬機是否支持version指定的JNI版本,而後將JNI接口指針設置到*env中   |
|-----------------------------+---------------------------------------------------|
| 參數                        |   vm: JavaVM接口指針的地址                               |
|                             | env: JNI接口指針地址                        |
|                             | version: JNI版本                            |
|-----------------------------+---------------------------------------------------|
| 返回值                   | 若執行成功,返回0;失敗,返回負值                                 |

#+BEGIN_SRC java
    cls = env->FindClass("HelloJNI");
#+END_SRC
爲了把聲明的JNI本地函數與JNI本地函數映射在一起,本行先調用FindClass()函數加載HelloJNI類,並將類引用保存到jclass變量cls中。

#+BEGIN_SRC java
    nm[0].name = "printHello";
    nm[0].signature = "()V";
    nm[0].fnPtr = (void*)printHelloNative;

    nm[1].name = "printString";
    nm[1].signature = "(Ljava/lang/String;)V";
    nm[1].fnPtr = (void*)printStringNative
#+END_SRC
該部分代碼用來將Java類的本地方法與JNI本地函數映射在一起。首先使用JNINativeMethod結構體數組,將待映射的本地方法與JNI本地函數的相關信息保存在數組中,而後調用RegisterNatives()函數進行映射。JNINativeMethod結構體定義如下
#+BEGIN_SRC c
typedef struct {
    char *name;    // 本地方法名稱
    char *signature; // 本地方法簽名
    void *fnPtr; // 與本地方法相對應的JNI本地函數指針
} JNINativeMethod
#+END_SRC
如代碼所示,nm是JNINativeMethod結構體數組,它保存着printHello()、printString()與printHelloNative()、printStringNative()函數的鏈接信息。

保存好映射信息後,將它們傳遞給RegisterNatives()函數,最後由RegisterNatives()函數完成映射。
| JNI函數 RegisterNatives |                                                                                                  |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 形式                    | jarray RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methdos, jint nMethods) |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 說明                    | 將clazz指定類中的本地方法與JNI本地函數鏈接在一起,鏈接信息保存在JNINativeMethod結構體數組中                                        |
|-------------------------+------------------------------------------------------------------------------------------------------------------------------------|
| 參數                    | env: JNI接口指針                                                                                     |
|                         | clazz: Java類                                                                                    |
|                         | methods: 包含本地方法與JNI本地函數的鏈接信息                                                     |
|                         | nMethods: methods數組元素的個數                                                                         |
|-------------------------+---------------------------------------------------------------------------------------------------------|
| 返回值                  | 若執行成功,返回數組引用;否則返回NULL                                                                            |

=總結一下,本節中通過JNI_OnLoad()函數將Java本地方法與JNI本地函數映射起來。=

** Android中的應用舉例

* 使用Android NDK開發

Andoird NDK ( Native Development Kit )

+ 包含將C/C++源代碼編譯成本地庫的工具(編譯器、連接器等)

+ 提供將編譯好的本地庫插入Android包文件(.apk)中的功能

+ 在生成本地庫時,Android平臺可支持的系統頭文件與庫

+ NDK開發相關的文檔、示例、規範

** 安裝Androdi NDK

網站: http://developer.android.com/sdk/ndk/index.html

** 使用Android NDK 開發步驟

=設置好NDK環境變量後,在<NDK_HOME>/apps目錄下,會看到一些NDK使用示例程序=

+ hello-jni: 調用本地庫,接收"Hello from JNI"字符串,並通過TextView將其輸出

+ two-libs: 調用本地庫,返回兩數之和,並通過TextView輸出

+ san-angeles: 調用本地OpenGL ES API, 渲染3D圖片

+ hello-gl2: 調用OpenGL ES 2.0, 渲染三角形

+ bitmap-plasma: 一個使用本地代碼訪問Android Bitmap對象的像素緩存區的示例程序

** hello-jni

內容:

AndroidManifest.xml default.properties /jni /res /src /tests

ndk-build後:

AndroidManifest.xml default.properties /jni /libs /obj /res /src /tests

#+BEGIN_SRC sh
hello-jni$ tree
.
├── AndroidManifest.xml
├── default.properties
├── jni
│   ├── Android.mk
│   └── hello-jni.c
├── libs
│   └── armeabi
│       ├── gdbserver
│       ├── gdb.setup
│       └── libhello-jni.so
├── obj
│   └── local
│       └── armeabi
│           ├── libhello-jni.so
│           └── objs-debug
│               └── hello-jni
│                   ├── hello-jni.o
│                   └── hello-jni.o.d
├── res
│   └── values
│       └── strings.xml
├── src
│   └── com
│       └── example
│           └── hellojni
│               └── HelloJni.java
└── tests
    ├── AndroidManifest.xml
    ├── default.properties
    └── src
        └── com
            └── example
                └── hellojni
                    └── HelloJniTest.java

19 directories, 15 files
#+END_SRC

關鍵的文件:

java層: HelloJni.java HelloJniTest.java 資源文件:strings.xml

jni層: hello-jni.c Android.mk

下面主要需要分析 HelloJni.java, hello-jni.c, Android.mk 三個文件

HelloJni.java
#+BEGIN_SRC java
package com.example.hellojni;

import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;

public class HelloJni extends Activity
{
    /** onCreate函數在activity第一次被創建的時候調用 */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        /* 創建一個TextView並且設置它的內容.
         * 文本的內容是通過本地方法獲取的
         */
        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() ); // 這裏調用了本地方法
        setContentView(tv);
    }

    /* 本地方法通過本地庫hello-jni實現
     * 本地庫與這個應用程序已經打包在了一起
     */
    public native String  stringFromJNI();

    /* 下面是另外一個方法的聲明,這個方法沒有通過hello-jni實現
     * 這是爲了說明,你可以聲明任意的本地方法,在java代碼中
     * 它們的實現會在裝載的本地庫裏尋找,當你首次調用它們的時候
     * 嘗試調用這個方法會引發java.lang.UnsatisfiedLinkError exception!
     */
    public native String  unimplementedStringFromJNI();

    /* 下面的代碼用來在應用程序開始的時候裝載hello-jni庫
     * 這個庫在安裝的時候由包管理器已經安裝好了
     */
    static {
        System.loadLibrary("hello-jni");
    }
}
#+END_SRC

hello-jni.c
#+BEGIN_SRC c
#include <string.h>
#include <jni.h>
jstring java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *env, jobject thiz)
{
    return (*env)->NewStringUTF(env, "Hello from JNI !");
}
#+END_SRC

Android.mk
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := hello-jni
LOCALSRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)
#+END_SRC

一個Android.mk文件首先必須定義好 =LOCAL_PATH= 變量,用於在開發樹中查找源文件。 =LOCAL_PATH= 變量在Android.mk文件的最開始被定義,若無特殊情況,一般採用如下編寫形式:
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)
#+END_SRC
$(call my-dir)用來保存my-dir宏函數的返回值。my-dir是一個宏函數,由編譯系統提供,用於返回包含Android.mk文件的目錄,即將Android.mk文件所在的目錄設置爲基本目錄。

一般來說,本地庫的源代碼與Android.mk文件在同一目錄下,即在 =<PROJECT_HOME>/jni= 目錄下。若將$(call my-dir)返回值保存到 =LOCAL_PATH= 變量中,即可準確指定NDK編譯的基本文件目錄。

=include $(CLEAR_VARS)= 用來初始化Android.mk文件中" =LOCAL_XXX= "即以 =LOCAL_= 開頭的變量,如 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等變量,但在一開始的 =LOCAL_PATH= 變量除外。由於Android編譯系統將會 =LOCAL_XXX= 變量用作全局變量,所以需要使用該命令初始化這些變量。

=LOCAL_MODULE= 變量必須被定義,以標識在Android.mk文件中描述的每個模塊,即要生成的庫的名稱。該名稱必須唯一,且不含空格,編譯系統會自動產生合適的前綴和後綴,比如設置 =LOCAL_MODULE= 變量爲ndk-exam, 編譯後綴會生成名爲libndk-exam.so的共享庫。

=LOCAL_SRC_FILES= 變量必須包含將要編譯打包進模塊中的各個源文件。這些源文件所在目錄即是 =LOCAL_PATH= 變量指定的目錄,即 =<PROJECT_HOME>/jni= 目錄。

=include $(BUILD_SHARED_LIBRARY)= 使用 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等變量值,創建名稱爲 =lib$(LOCAL_MODULE).so= 的共享庫。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章