Lynx https://github.com/hxxft/lynx-native 内核是由 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{$92
- }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 中查看.
如何在已有工程中使用
git clone https://github.com/hxxft/lynx-native.git
抽取所需文件并将文件添加入 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
抽取 build 文件夹, 将 jni_load.cc 加入 CMakeLists.txt, 并根据需求修改此文件
修改 jni_files 加入需要自动生成 JNI 代码的 Java 文件
编写 Java 文件, 使用前面讲解时使用的方法注释函数或者修饰方法
修改 prebuild.sh 中 ROOT_LYNX_JAVA_PATH 等路径, 根据自己工程配置进行修改
在编译之前执行 prebuild.sh 生成所需要的文件
总结
这篇文章主要介绍 Lynx 的 JNI 自动生成的方法, 自动生成的方法省去了大部分重复代码, 因此在编写代码过程中可以专注于方法的实现上, 对需要使用 JNI 的工程来说, 可以提供巨大的便利.
来源: https://juejin.im/post/5add4e416fb9a07acd4d543d