天天看點

Qt on Android Episode 5(翻譯)為什麼需要 JNI JNI 簡介從 C++ 中調用一個 Java 函數從 Java 中回調一個 C++ 函數總結

    原文位址: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 函數,我們需要做下面四步:

  1. 第一步,我們需要通路 JNIEnv 指針。最簡單的方法是定義和導出 JNI_OnLoad 方法,每個 so 檔案一次,可以定義在任何的 cpp 檔案裡。
  2. 第二步,為我們想導出的 C++ 方法建立一個數組
  3. 第三步,使用 JniEnv::FindClass 找到聲明這些 native 方法的 Java 類的 ID 
  4. 第四步,調用 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(翻譯)

繼續閱讀