原文位址:http://www.kdab.com/qt-android-episode-5/
我們已經知道了如何搭建 Qt on Android 開發環境,怎樣使用 Qt on Android ,有哪些可用的部署政策以及如何為應用簽名,是時候繼續前進了。這篇文章,我們來講 JNI 。(BogDan 啊,我等你等了好久,當時我寫《Qt on Android核心程式設計》時沒等到……)
為什麼需要 JNI
因為 Qt 要實作 Android 的所有功能是不現實的。要想使用 Android 系統已經具備的功能,就需要通過 JNI 來通路它們。 JNI 是在 Java 和 C++ 之間互相調用的唯一途徑。
JNI 簡介
這篇文章我們來學習 JNI 的基本知識,下一篇文章呢,我們将研究如何使用本文介紹的 JNI 知識來擴充我們的 Qt 應用。
網上有太多太多關于 JNI 的讨論了,本文将聚焦于如何在 Android 系統上通過 Qt 來使用 JNI 。從 5.2 開始, Qt 攜帶了 Qt Android Extras 這個子產品。當我們不得不使用 JNI 時,它提供給我們更舒适的體驗。
我們會讨論兩件事:
- 從 C++ 中調用一個 Java 函數
- 從 Java 中回調一個 C++ 函數
從 C++ 中調用一個 Java 函數
使用 Qt Android Extras 來調用一個 Java 函數是相當簡單的。
首先,我們來建立一個 Java 的靜态方法:
// java file android/src/com/kdab/training/MyJavaClass.java
package com.kdab.training;
public class MyJavaClass
{
// this method will be called from C/C++
public static int fibonacci(int n)
{
if (n < 2)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
}
如你所見,我們在 com.kdab.training 包内的 MyJavaClass 類内定義了 fibonacci 這個靜态方法,這個方法會計算 fibonacci 數并傳回結果。
現在我們來看看怎麼從 Qt 中調用 fibonacci 這個方法。
第一步,因為我們要用到 Qt Android Extras ,是以要修改一下 .pro 檔案,如下:
# Changes to your .pro file
# ....
QT += androidextras
# ....
第二步,執行調用:
// C++ code
#include <QAndroidJniObject>
int fibonacci(int n)
{
return QAndroidJniObject::callStaticMethod<jint>
("com/kdab/training/MyJavaClass" // class name
, "fibonacci" // method name
, "(I)I" // signature
, n);
}
Yes ! 這就是所有的事兒喽。
讓我們仔細地來看看這段代碼,看看裡面都有什麼:
- 我們使用 QAndroidJniObject::callStaticMethod 來調用一個 Java 方法
- 第一個參數是全路徑類名,包名後跟一個類名,包名中的 . 被替換為 /
- 第二個參數是方法名
- 第三個參數是方法簽名
- 最後一個參數是我們要傳遞給 Java 方法的參數
請閱讀 QAndroidJniObject 的文檔來了解方法簽名和參數類型的細節。
從 Java 中回調一個 C++ 函數
為了從 Java 回調 C++ 方法,你可以按下面的步驟來做:
- 在 Java 中使用 native 關鍵字來聲明 native 方法
- 在 C++ 中注冊 native 方法
- 調用 Java 裡的 native 方法
使用 native 關鍵字聲明 native 方法
讓我們來稍稍改動一下之前的 Java 代碼:
// java file android/src/com/kdab/training/MyJavaClass.java
package com.kdab.training;
class MyJavaNatives
{
// declare the native method
public static native void sendFibonaciResult(int n);
}
public class MyJavaClass
{
// this method will be called from C/C++
public static int fibonacci(int n)
{
if (n < 2)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
// the second method that will be called from C/C++
public static void compute_fibonacci(int n)
{
// callback the native method with the computed result.
MyJavaNatives.sendFibonaciResult(fibonacci(n));
}
}
讓我們仔細瞄一瞄這段代碼裡都有什麼:
- 我個人比較傾向把所有的 native 方法都隔離到一個獨立的類裡,是以我在 com.kdab.training 包内定義了 MyJavaNatives 類,聲明了 sendFibonaciResult 這個 native 方法。靜态的 compute_fibonacci 方法會調用 sendFibonaciResult 來回調 C++ ,發送計算結果,而不再通過 fibonacci 的傳回值來做這件事。
- sendFibonaciResult ,這個方法會被 C++ 代碼調用,但它不像 fibonacci 那樣直接傳回計算結果,它使用 sendFibonaciResult 這個 native 方法來回調 C++ 世界來傳遞計算結果。
如果你嘗試運作現在的代碼,會失敗。因為 sendFibonaciResult 還沒注冊, JVM 根本不知道它是神馬玩意兒。
使用 Java_Fully_Qualified_ClassName_MethodName 注冊函數
代碼:
#include <jni.h>
#include <QDebug>
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL
Java_com_kdab_training_MyJavaNatives_sendFibonaciResult(JNIEnv *env,
jobject obj,
jint n)
{
qDebug() << "Computed fibonacci is:" << n;
}
#ifdef __cplusplus
}
#endif
讓我們仔細瞄一瞄,看看這段代碼有何神奇之處:
- 我們看到的第一件事,就是,所有的函數都必須導出為 C 函數,而不是 C++ 函數
- 函數名字必須遵循下面的模闆:Java 關鍵,包名,類名,方法名,之間用短下劃線分割
- 當 JVM 加載 so 檔案時,會掃描這個模闆,自動為你注冊你所有符合這個模闆的函數
- 第一個參數 env 是 JNIEnv 類型的指針,指向一個 JNIEnv 對象
- 第二個參數 obj ,代表你聲明這個 native 方法的那個 Java 對象
- 對于你要注冊的每一個函數,第一和第二個參數是強制的,必須的
- 從第三個參數開始,對應 Java 類裡聲明的 native 方法的參數,順次哦,一一對應。是以呢,我們的 C++ 代碼裡,第三個參數就對應 Java 代碼裡 sendFibonaciResult 的第一個參數。
使用這種方式來注冊和聲明 native 方法是很簡單的,但是它有幾個缺點:
- 函數名巨長,比如 Java_com_kdab_training_MyJavaNatives_sendFibonaciResult ,鬼才記得住,記得住敲得也煩不是
- 庫必須導出所有的函數
- 不安全, JVM 沒辦法檢查函數的簽名,因為函數是以 C 的方式導出的,而不是 C++ 的方式
使用 JNIEnv::RegisterNatives 來注冊 native 函數
為了使用 JNIEnv::RegisterNatives 來注冊 native 函數,我們需要做下面四步:
- 第一步,我們需要通路 JNIEnv 指針。最簡單的方法是定義和導出 JNI_OnLoad 方法,每個 so 檔案一次,可以定義在任何的 cpp 檔案裡。
- 第二步,為我們想導出的 C++ 方法建立一個數組
- 第三步,使用 JniEnv::FindClass 找到聲明這些 native 方法的 Java 類的 ID
- 第四步,調用 JNIEnv::RegisterNatives(java_class_ID, methods_vector, n_methods)
代碼如下:
// C++ code
#include <jni.h>
#include <QDebug>
// define our native method
static void fibonaciResult(JNIEnv */*env*/, jobject /*obj*/, jint n)
{
qDebug() << "Computed fibonacci is:" << n;
}
// step 2
// create a vector with all our JNINativeMethod(s)
static JNINativeMethod methods[] = {
{ "sendFibonaciResult", // const char* function name;
"(I)V", // const char* function signature
(void *)fibonaciResult // function pointer
}
};
// step 1
// this method is called automatically by Java VM
// after the .so file is loaded
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/)
{
JNIEnv* env;
// get the JNIEnv pointer.
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)
!= JNI_OK) {
return JNI_ERR;
}
// step 3
// search for Java class which declares the native methods
jclass javaClass = env->FindClass("com/kdab/training/MyJavaNatives");
if (!javaClass)
return JNI_ERR;
// step 4
// register our native methods
if (env->RegisterNatives(javaClass, methods,
sizeof(methods) / sizeof(methods[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
讓我們核對一下代碼以便更好地了解:
- static void fibonaciResult(JNIEnv *, jobject , jint n),這是我們注冊的方法, JVM 将會調用它。
- 第一個參數 env 指向 JNIEnv 對象
- 第二個參數 obj 是聲明 fibonaciResult 這個本地方法的 Java 對象的引用
- 第一、第二個參數對于要導出的函數是強制的、必須的
- 從第三個參數開始,對應 Java 類裡聲明的 native 方法的參數,順次哦,一一對應。是以呢,我們的 C++ 代碼裡,第三個參數就對應 Java 代碼裡 sendFibonaciResult 的第一個參數。
- 我們把這個方法加到了 JNINativeMethod 類型的方法數組裡
- JNINativeMethod 結構體有下列成員:
-
- const char* name - 函數名字,必須和 Java 裡聲明的 native 方法名字一緻
- const char* signature - 函數簽名,必須和 Java 裡聲明的 native 方法的參數一緻
- void* fnPtr - C++函數指針,我們的代碼裡呢,就是 fibonaciResult 。如你所見, C++ 函數名字是無所謂的,因為 JVM 隻需要一個指針。
- 剩下的代碼簡單、清晰,沒必要再解釋了吧親,還有注釋呢哈。
這種方式用起來看着有那麼一點點複雜,但是它有下列好處:
- C++ 方法的名字可以随你的便親
- 庫隻需要導出一個函數
- 安全, JVM 會校驗函數簽名
用哪種方式來注冊 native 函數是個人偏好問題,但我還是推薦使用 JNIEnv::RegisterNatives 這種方式,因為它提供了額外的保護:當 JVM 檢測到函數簽名不比對時會抛出一個異常。
總結
這篇文章我們學習了 JNI 的基本知識,下一篇文章呢,我們将研究如何使用本文介紹的 JNI 知識來擴充我們的 Qt 應用。我們會更多的讨論 Qt on Android 應用的架構、如何擴充你應用的 Java 部分,我們還會提供一個實際的例子來說明如何在 Qt 的線程和 Java 的 UI 線程之間互相調用。
回顧一下我翻譯的 Qt on Android Episode 系列文章:
- Qt on Android Episode 1(翻譯)
- Qt on Android Episode 2(翻譯)
- Qt on Android Episode 3(翻譯)
- Qt on Android Episode 4(翻譯)