Android JNI 程式碼自動生成

hxxft發表於2018-04-23

Lynx核心是由C++編寫,方便跨平臺使用。這樣在Android端與Java層通訊就需要使用JNI,Lynx在JNI層為了避免直接手寫JNI註冊程式碼以及反射呼叫Java的程式碼,使用自動化的方式來自動生成這部分程式碼。

JNI的註冊方式

1. Java呼叫C/C++方法

通常Java呼叫C/C++方法的JNI方法註冊分為靜態註冊和動態註冊兩種。

  • 靜態註冊的方式

將Java中的Native方法在C/C++檔案宣告對應成Java_$PackageName_$MethodName(JNIEnv *env, args…) 完成完成靜態註冊。以Lynx程式碼中的自動化測試模組程式碼GTestDriver.java為例

package com.lynx.gtest;

...
    
public class GTestDriver {
  ...
  //java native 方法
  private static native int nativeRunGTestsNative(String[] gtestCmdLineArgs);
}
複製程式碼

對於C方法的編寫如下

#include <jni.h>
...
JNIEXPORT jint Java_com_lynx_gtest_nativeRunGTestsNative(JNIEnv *env, jobjectArray gtestCmdLineArgs)
...
複製程式碼

從例子中可以看出靜態註冊的名字非常長,不便於書寫。並且在初次呼叫的時候需要依據名字找到對應方法,對於大型工程中方法數多的情況下,效率低且易出錯。

  • 動態註冊方式

通過在C/C++中宣告一個JNINativeMethod nativeMethod[]陣列,然後在JNI_OnLoad中呼叫RegisterNatives方法來完成動態註冊。譬如上面的例子用動態註冊的方式為:

#include <jni.h>
...
static jint RunGTestsNative(JNIEnv *env,  jclass jcaller, jobjectArray gtestCmdLineArgs) {
....
}

static const JNINativeMethod kMethodsGTestDriver[] = {
  { "nativeRunGTestsNative",
    "("
    "[Ljava/lang/String;"
    ")"
    "I", reinterpret_cast<void*>(RunGTestsNative) },
};
...
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
  JNIEnv *env;
  if ((*jvm) -> GetEnv(jvm, (void**) &env, JNI_VERSION_1_6) != JNI_OK) {
      return -1;
  }

  jclass clz = (*env) -> FindClass(env, "com/lynx/gtest/GTestDriver");

  (*env) -> RegisterNatives(env, clz, kMethodsGTestDriver, sizeof(kMethodsGTestDriver) / sizeof(kMethodsGTestDriver[0]));

  return JNI_VERSION_1_6;
}

複製程式碼

動態註冊是Lynx JNI的基礎,後面展開介紹。

2. C/C++呼叫Java方法

JNI的C程式碼呼叫Java程式碼。實現原理:使用JNI提供的反射介面來反射得到Java方法,進行呼叫。以Lynx程式碼中的文字測量的程式碼LabelMeasurer.java為例

package com.lynx.core;
...
public class LabelMeasurer {
	...
    @CalledByNative
    public static Size measureLabelSize(String text, Style style, int width,
                                        int widthMode, int height, int heightMode) {
        ...
    }
}
複製程式碼

在C/C++程式碼中呼叫measureLabelSize需要通過反射來完成,基本實現如下

// 查詢LabelMeasurer類
jclass clazz = (*env)->FindClass(env,"com/lynx/core/LabelMeasurer");

//獲取measureLabelSize方法
jmethodID method = (*env)->GetMethodID(env,clazz, "measureLabelSize", "(Ljava/lang/String;Lcom/lynx/base/Style;IIII)Lcom/lynx/base/Size;");

//執行
jobject ret = env->CallStaticObjectMethod(clazz, method, text, style,
                                          width, widthMode, height, heightMode);
複製程式碼

編寫這樣的呼叫會有非常多的重複程式碼,當介面需要進行修改時非常不方便。

Lynx JNI 自動生成方式

從上述介紹可得,JNI的註冊是一個重複性的操作。Lynx為了提高效率,將JNI的註冊交由自動化指令碼生成。

1. Java呼叫C/C++方法

Lynx上對Native註冊進行了約定,所有對Java註冊的Native方法都以native開頭,自動化指令碼會在Java檔案中找到這些方法,並自動生成相應的檔案。

同樣以 GTestDriver.java 為例,可以看到GTestDriver中包含JNI的Native方法nativeRunGTestsNative,並且以約定的native關鍵開頭。對於這個檔案會由prebuild.sh指令碼執行生成一個GTestDriver_jni.h的標頭檔案

#ifndef com_lynx_gtest_GTestDriver_JNI
#define com_lynx_gtest_GTestDriver_JNI

#include <jni.h>

#include "base/android/android_jni.h"

// Step 1: forward declarations.
namespace {
const char kGTestDriverClassPath[] = "com/lynx/gtest/GTestDriver";
// Leaking this jclass as we cannot use LazyInstance from some threads.
jclass g_GTestDriver_clazz = NULL;
#define GTestDriver_clazz(env) g_GTestDriver_clazz

}  // namespace

static jint RunGTestsNative(JNIEnv* env, jclass jcaller,
    jobjectArray gtestCmdLineArgs);

// Step 2: method stubs.

// Step 3: RegisterNatives.

static const JNINativeMethod kMethodsGTestDriver[] = {
    { "nativeRunGTestsNative",
"("
"[Ljava/lang/String;"
")"
"I", reinterpret_cast<void*>(RunGTestsNative) },
};

static bool RegisterNativesImpl(JNIEnv* env) {

  g_GTestDriver_clazz = reinterpret_cast<jclass>(env->NewGlobalRef(
      base::android::GetClass(env, kGTestDriverClassPath).Get()));

  const int kMethodsGTestDriverSize =
      sizeof(kMethodsGTestDriver)/sizeof(kMethodsGTestDriver[0]);

  if (env->RegisterNatives(GTestDriver_clazz(env),
                           kMethodsGTestDriver,
                           kMethodsGTestDriverSize) < 0) {
    return false;
  }

  return true;
}

#endif  // com_lynx_gtest_GTestDriver_JNI
複製程式碼

Lynx的JNI使用的動態註冊方式,將Java檔案中定義的nativeRunGTestsNative與RunGTestsNative函式指標關聯,這樣對標頭檔案中定義的RunGTestsNative方法進行實現,並在jni_onload的時候呼叫RegisterNativesImpl就可以實現對JNI方法的完整註冊。這個方式比起JNI原有的方式也簡單很多,隱藏了很多重複性質的程式碼。

2. C/C++呼叫Java方法

Lynx對C/C++呼叫Java方法也做了定義,如果在Java檔案中定義了可以被C/C++呼叫的程式碼,可以在方法前加上@CalledByNative,自動化腳步會在java檔案中找到這些方法,並自動生成相應的檔案。同樣以上文中的 LabelMeasurer.java 為例。

在Java檔案中申明瞭一個可以被C/C++使用的函式measureLabelSize。進過prebuild.sh指令碼處理之後會生成LabelMeasurer_jni.h

#ifndef com_lynx_core_LabelMeasurer_JNI
#define com_lynx_core_LabelMeasurer_JNI

#include <jni.h>

#include "base/android/android_jni.h"

// Step 1: forward declarations.
namespace {
const char kLabelMeasurerClassPath[] = "com/lynx/core/LabelMeasurer";
// Leaking this jclass as we cannot use LazyInstance from some threads.
jclass g_LabelMeasurer_clazz = NULL;
#define LabelMeasurer_clazz(env) g_LabelMeasurer_clazz

}  // namespace

// Step 2: method stubs.

static intptr_t g_LabelMeasurer_measureLabelSize = 0;
static base::android::ScopedLocalJavaRef<jobject>
    Java_LabelMeasurer_measureLabelSize(JNIEnv* env, jstring text,
    jobject style,
    int width,
    int widthMode,
    int height,
    int heightMode) {
  jmethodID method_id =
      base::android::GetMethod(
      env, LabelMeasurer_clazz(env),
      base::android::STATIC_METHOD,
      "measureLabelSize",

"("
"Ljava/lang/String;"
"Lcom/lynx/base/Style;"
"I"
"I"
"I"
"I"
")"
"Lcom/lynx/base/Size;",
      &g_LabelMeasurer_measureLabelSize);

  jobject ret =
      env->CallStaticObjectMethod(LabelMeasurer_clazz(env),
          method_id, text, style, int(width), int(widthMode), int(height),
              int(heightMode));
  base::android::CheckException(env);
  return base::android::ScopedLocalJavaRef<jobject>(env, ret);
}

// Step 3: RegisterNatives.

static bool RegisterNativesImpl(JNIEnv* env) {

  g_LabelMeasurer_clazz = reinterpret_cast<jclass>(env->NewGlobalRef(
      base::android::GetClass(env, kLabelMeasurerClassPath).Get()));

  return true;
}

#endif  // com_lynx_core_LabelMeasurer_JNI
複製程式碼

這樣在使用的時候就可以直接引入標頭檔案,並呼叫Java_LabelMeasurer_measureLabelSize方法即可。使用起來也是非常簡便的。

自動生成的檔案省去了編寫反射獲取函式方法以及呼叫的處理,全部有指令碼直接生成,對於獲取method id的方式,Lynx做了一層封裝,可以對method id進行儲存,這樣查詢只會在第一次呼叫的時候執行,節約整體呼叫時間 , 具體可以base/android/android_jni.h 中檢視。

同時Lynx也對jobject進行了自動引用的處理,在不再使用jobject物件的時候會自動進行DeleteLocalRef,避免忘記釋放後local ref過多超出最大值的情況,具體可以在 base/android/scoped_java_ref.h 中檢視。

如何在已有工程中使用

  1. git clone https://github.com/hxxft/lynx-native.git

  2. 抽取所需檔案並將檔案新增入CMakeLists.txt

    base/android/android_jni.h

    base/android/android_jni.cc

    base/android/java_type.h

    base/android/java_type.cc

    base/android/scoped_java_ref.h

    base/android/scoped_java_ref.cc

  3. 抽取build資料夾,將jni_load.cc加入CMakeLists.txt,並根據需求修改此檔案

  4. 修改jni_files加入需要自動生成JNI程式碼的Java檔案

  5. 編寫Java檔案,使用前面講解時使用的方法註釋函式或者修飾方法

  6. 修改prebuild.sh中ROOT_LYNX_JAVA_PATH等路徑,根據自己工程配置進行修改

  7. 在編譯之前執行prebuild.sh生成所需要的檔案

總結

這篇文章主要介紹Lynx的JNI自動生成的方法,自動生成的方法省去了大部分重複程式碼,因此在編寫程式碼過程中可以專注於方法的實現上,對需要使用JNI的工程來說,可以提供巨大的便利。

請持續關注 Lynx,一個高效能跨平臺開發框架。

相關文章