由於筆者最近在開發 Firefox for Android 上的功能,也因此有機會接觸 Android 開發。Firefox for Android 也是採用與 Firefox(桌面版)相同的瀏覽器引擎 - Gecko。由於 Gecko 主要是採用 C/C++ 開發的,因此勢必需要使用到 Java 的 JNI 來銜接 C/C++ 的程式碼,在 Android 上也就需要 NDK 的支援。

這系列文章將分為三篇:

  1. 在 Android 上實作 JNI 函式與存取 Java 中的成員變數
  2. 在 Gecko 中實作 JNI 函式(標題暫定)
  3. Gecko 如何串接 JNI 與 C++ 的物件(標題暫定)

本文章為本系列第一章,將針對如何在 Android 上實作 JNI 函式,並於 C/C++ 端存取 Java 端的成員變數。目前還不太清楚如何使用 CLI 來開發 Android 應用程式,因此本文章將採用 Google 針對 Android 所開發的 IDE - Android Studio

開發環境

系統 / 工具 版本
OS X 10.10
Android Studio 2.1.2
Android SDK 24.4.1
Android NDK r12

範例程式

本文章的範例程式碼都放在 GitHub 上,有需要的話可以前往 https://github.com/KuoE0/AndroidJNITest

首先,我們先建立一個非常簡單的 Android 程式,這個程式會透過呼叫函式來取得字串,並顯示在畫面上。目前,我們先不使用 JNI,所有的工作都在 Java 端完成。請看以下程式碼:

app/src/main/java/tw/kuoe0/androidjnitest/MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package tw.kuoe0.androidjnitest;

import android.support.v4.widget.TextViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;


class JNIMethod {
    static public String getStringFromNativeForStaticFunction() {
        return sMsg;
    }
    public String getStringFromNativeForMemberFunction() {
        return mMsg;
    }

    private static final String sMsg = "Hello wrold from static!";
    private final String mMsg = "Hello wrold from member!";
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView sTextview = (TextView) findViewById(R.id.textStatic);

        sTextview.setText(JNIMethod.getStringFromNativeForStaticFunction());

        JNIMethod jni = new JNIMethod();
        TextView mTextview = (TextView) findViewById(R.id.textMember);
        mTextview.setText(jni.getStringFromNativeForMemberFunction());

    }
}

使用 JNI 函式

接下來,我們將試圖透過 JNI 來取得要輸出的字串。

Step 1. 將函式宣告為 JNI 函式

首先,我們必須要將 JNIMethod 裡頭的函式修改為 JNI 函式。只要在該函式的定義加上 native 的修飾字,並去除其函式主體即可,範例如下:

1
2
3
4
class JNIMethod {
    native static public String getStringFromNativeForStaticFunction();
    native public String getStringFromNativeForMemberFunction();
}

Step 2. 進行編譯與產生 C++ 標頭檔

做完這樣的修改後,記得先編譯一次,基本上編譯不會有任何錯誤。如果編譯後嘗試執行該程式,會發生找不到 getStringFromNativeForStatic 的 implementation 的錯誤。錯誤訊息如下:

1
2
3
E/art: No implementation found for java.lang.String tw.kuoe0.androidjnitest.JNIMethod.getStringFromNativeForStaticFunction() 
       (tried Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForStaticFunction 
       and Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForStaticFunction__)

接下來,我們開始使用 C++ 來實作前面定義的兩個 JNI 函式。打開終端機到 app/build/intermediates/classes/debug 資料夾,輸入以下指令:

1
$ javah tw.kuoe0.androidjnitest.JNIMethod

這時候該資料夾將會出現 tw_kuoe0_androidjnitest_JNIMethod.h 這個標頭檔,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class tw_kuoe0_androidjnitest_JNIMethod */

#ifndef _Included_tw_kuoe0_androidjnitest_JNIMethod
#define _Included_tw_kuoe0_androidjnitest_JNIMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     tw_kuoe0_androidjnitest_JNIMethod
 * Method:    getStringFromNativeForStaticFunction
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForStaticFunction
  (JNIEnv *, jclass);

/*
 * Class:     tw_kuoe0_androidjnitest_JNIMethod
 * Method:    getStringFromNativeForMemberFunction
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForMemberFunction
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

這邊,可以發現 static 函式與 non-static 函式在函式定義上會些許差異。主要是第二個參數是 jclass 型別還是 jobject 型別,這部分的差異會在本文章的第三部份提到。

而第一個參數的型別是 JNIEnv,這個參數算是在 C++ 中 Java 的 context。基本上,在 C++ 中要使用 Java 的函式都需要透過它!

Step 3. 使用 C++ 實作 JNI 函式

首先,建立一個 app/src/main/jni 資料夾,並將剛剛產生的標頭檔放到該資料夾下。接著在 app/src/main/jni 建立一個叫做 JNIMethod.cpp 的檔案,內容如下:

app/src/main/jni/JNIMethod.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "tw_kuoe0_androidjnitest_JNIMethod.h"

 // getStringFromNativeForStaticFunction
JNIEXPORT jstring JNICALL
Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForStaticFunction(JNIEnv *env, jclass cls)
{
	return env->NewStringUTF("Hello world from static!");
}

 // getStringFromNativeForMemberFunction
JNIEXPORT jstring JNICALL
Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForMemberFunction(JNIEnv *env, jobject obj)
{
	return env->NewStringUTF("Hello world from member!");
}

以上的程式碼可以發現,我們僅僅是很單純的回傳 literal 字串回來,所以這部分對於該函式是不是 static 函式目前看起來沒什麼差異。

Step 4. 修改設定檔來編譯 C++ 程式碼

app/build.gradledefaultConfig 區塊中加入以下設定:

1
2
3
ndk {
    moduleName "JNIMethod"
}

gradle.properties 中加入以下設定:

1
android.useDeprecatedNdk=true

此外,再於 local.properties 中加入 NDK 的路徑:

1
2
sdk.dir=/usr/local/Cellar/android-sdk/24.4.1_1
ndk.dir=/usr/local/Cellar/android-ndk/r12

進行上述設定後,基本上用 C++ 實作 JNI 函式已經完成了!不過,如果嘗試編譯後執行,還是會得到一樣的錯誤。因為,編譯出來的 C++ 函式庫並沒有被載入 Java 中。最後只要在 Java 的程式碼中仔入這個函式庫即可,程式碼如下:

1
2
3
4
5
6
7
8
9
10
class JNIMethod {
    static {
        System.loadLibrary("JNIMethod");  //defaultConfig.ndk.moduleName
    }
    native static public String getStringFromNativeForStaticFunction();
    native public String getStringFromNativeForMemberFunction();

    private static final String sMsg = "Hello wrold from static!";
    private final String mMsg = "Hello wrold from member!";
}

編譯執行後,應該就可以看到畫面上有正常顯示字串了!

在 JNI 函式中使用 Java 的成員變數

這部分我們將要示範如何在 JNI 函式中取得 Java 端的成員變數。在一開始純粹採用 Java 的方法中,筆者就留下了一個伏筆。在回傳字串時,是回傳該類別的成員變數。同樣的,成員變數也分為 static 與 non-static,畢竟 static 函式也只能存取 static 成員變數。

在 Java 端宣告 JNIMethod 這個類別時,我們定義了一個 static 成員變數 sMsg 與另一個 non-static 變數 mMsg。我們將在 JNI 函式中取得這些成員變數並直接回傳。修改後的程式碼如下:

app/src/main/jni/JNIMethod.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "tw_kuoe0_androidjnitest_JNIMethod.h"

 // getStringFromNativeForStaticFunction
JNIEXPORT jstring JNICALL
Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForStaticFunction(JNIEnv *env, jclass cls)
{
	jfieldID fid = env->GetStaticFieldID(cls, "sMsg", "Ljava/lang/String;");
	jstring str = (jstring) env->GetStaticObjectField(cls, fid);
	return str;
}

 // getStringFromNativeForMemberFunction
JNIEXPORT jstring JNICALL
Java_tw_kuoe0_androidjnitest_JNIMethod_getStringFromNativeForMemberFunction(JNIEnv *env, jobject obj)
{
	jclass cls = env->FindClass("tw/kuoe0/androidjnitest/JNIMethod");
	jfieldID fid = env->GetFieldID(cls, "mMsg", "Ljava/lang/String;");
	jstring str = (jstring) env->GetObjectField(obj, fid);
	return str;
}

取得 static 成員變數

要取得任何 Java 端的成員變數,都需要先取得其 field ID。對於 static 成員變數來說,需要有在 Java 端該類別的資訊(jclass 型別)以及該 field 的變數名稱以及型別的 signature。將這些資訊作為參數來呼叫 GetStaticFieldID() 即可得到 field ID。

以我們的例子來說,因為是在 static 函式要取得 static 成員函式,所以 Java 端該類別的資訊會作為第二個參數(jclass 型別的參數)被傳送進來。而我們也知道我們要取得的成員變數其變數名稱是 sMsg 以及其型別是 String。而 String 型別的 signature 是 Ljava/lang/String;。所以透過以下的程式碼就可以拿到 field ID:

1
jfieldID fid = env->GetStaticFieldID(cls, "sMsg", "Ljava/lang/String;");

取得 field ID 後,就可以透過 GetStaticObjectField() 來取得該變數了。

1
jstring str = (jstring) env->GetStaticObjectField(cls, fid);

取得 non-static 成員變數

對於 non-static 變數來說,除了要有該類別的資訊之外,也需要有該物件 (instance) 的資訊。而該物件的資訊,會作為 non-static 函式的第二個參數傳進來(jobject 型別)。所以,我們欠缺該類別的資訊,但可以透過 FindClass() 來取得。只要把該類別的「完整」名稱作為參數及可以取得其類別資訊,程式碼如下:

1
jclass cls = env->FindClass("tw/kuoe0/androidjnitest/JNIMethod");

有了 jclass 型別的類別資訊,即可透過 GetFieldID() 來取得 field ID。使用方式類似前面取得 static 成員變數的方式:

1
jfieldID fid = env->GetFieldID(cls, "mMsg", "Ljava/lang/String;");

有了 field ID 後,就可以透過 GetObjectField() 來取得該成員變數。要注意這邊第一個參數要傳入的是該物件的資訊,而不是該類別的物件。程式碼如下:

1
jstring str = (jstring) env->GetObjectField(obj, fid);

總結

以上就是如何在 Android 中實作 JNI 函式的範例,並且示範了如何在 JNI 函式中取得 Java 端的成員變數。透過 JNI,Java 與 C/C++ 就可以輕易地進行資料傳遞。

使用 C/C++ 的優點如下:

  • C/C++ 執行效能較 Java 高
  • 使用 C/C++ 既有的函式庫
  • 使用 platform-specific 的函式庫

但有優點就有缺點,缺點如下:

  • 涉及記憶體的操作可能使得程式穩定性下降
  • 使用了 platform-specific 的函式庫可能造成可攜性降低
  • Java 端與 C/C++ 端的 context switch 增加的 overhead

備註

在找 JNI 的範例程式碼時,常常「Copy and Paste from StackOverflow」大法也無法通過編譯。後來才發現原來 native 端使用 C 跟 C++ 是有差異的!

前面的 C++ 程式碼中,在使用 JNIEnv 的變數時,使用的方式都是像這樣:

1
env->GetObjectField(...);

然而,如果使用 C 的話,會需要修改為這樣:

1
(*env)->GetObjectField(env, ...);

這樣的差異在於 C 並沒有物件的概念。在 JNIEnv 中的 GetObjectField 應該只是一個「函式指標」,而在 C++ 中會是一個「成員函式」。C 也因為沒有物件的概念,因此更不會有 this 這個變數,所以才需要再把 JNIEnv 作為第一個參數傳進去。