文章目录
- JNI Zero
- 1. 概述
- 2. 用法
- 3. 构建规则
- 4. JNI Benchmarking
- 5. Under the Hood 幕后
- 6. Changing JNI Zero
JNI Zero
一款零开销(甚至更优!)的JNI中间件。
1. 概述
JNI(Java Native Interface)是一种机制,它使得Java代码能够调用本地函数,同时本地代码也能调用Java函数。
- Java → Native:通过 native 关键字声明无方法体的函数,调用时自动触发本地代码。
- Native → Java:通过 <jni.h> 提供的 API(类似 Java 反射)操作 Java 对象。
JNI Zero生成样板代码,旨在让我们的代码:
- 易于编写:自动生成 JNI 胶水代码,减少手写错误。
- 类型安全:通过代码生成避免类型不匹配问题。
- 优化性能:生成更高效的代码,甚至超越手动优化。
JNI Zero使用正则表达式解析.java
文件,所以不要编写过于复杂的代码。例如:
- 类必须显式导入,否则会被认为与当前文件在同一个包中。要使用
java.lang
包中的类,需显式导入。 - 内部类需要通过外部类来引用。例如:
void call(Outer.Inner inner)
。
1.1 暴露Native方法
有两种方法可以让Java找到原生方法:
- 使用JNI的
RegisterNatives()
函数显式注册 名称→函数指针 的映射关系。 - 从共享库中导出符号,由
运行时
在首次调用本地方法时自动解析它们(使用dlsym()
)。
通常更倾向于(2),因为它代码量更小,前期工作也更少,但在某些情况下(例如,当操作系统bugs 阻止dlsym()
工作时),(1)是必需的。该工具同时支持这两种方式。
1.2 暴露Java方法
- 传统 JNI 中,Native 代码调用 Java 方法需通过繁琐的反射 API(如 FindClass, GetMethodID, CallVoidMethod)。
- JNI Zero 的改进:
自动生成类型安全的包装函数,将反射调用转换为直接的函数调用,减少错误并提升可读性。
Java方法只需使用@CalledByNative
进行注解即可。默认情况下,在 native side 生成的方法存根 没有命名空间。可以使用@JNINamespace("your_namespace")
将生成的函数放入一个命名空间中。
2. 用法
2.1 编写构建规则
找到或添加一个针对你的.java
文件的generate_jni
目标,然后将这个generate_jni
目标添加到你的android_library
目标的srcjar_deps
中:
generate_jni("abcd_jni") {
sources = [ "path/to/java/sources/with/jni/Annotations.java" ]
}
android_library("abcd_java") {
...
# 允许Java文件看到生成的`${OriginalClassName}Jni`类。
srcjar_deps = [ ":abcd_jni" ]
}
source_set("abcd") {
...
# 允许cpp文件包含生成的`${OriginalClassName}_jni.h`头文件。
deps = [ ":abcd_jni" ]
}
2.2 Calling Java -> Native
对于每个JNI方法:
- 会生成C++存根,这些存根会将调用转发到你必须编写的C++函数。默认情况下,你需要实现的C++函数并不属于某个类,也就是说,它们是全局函数,而不是类的成员函数。
- 如果第一个参数是一个C++对象(例如
long native${OriginalClassName}
),那么绑定将不会调用静态函数,而是将该变量转换为一个cpp${OriginalClassName}
指针类型,然后在该对象上调用具有该名称的成员方法。
要向一个类添加JNI:
- 创建一个用
@NativeMethods
注解的嵌套接口,该接口包含你希望实现的相应静态方法的声明。 - 使用
${OriginalClassName}Jni.get().${method}
调用原生函数。 - 在C++代码中,
#include
头文件${OriginalClassName}_jni.h
(路径将取决于generate_jni
BUILD规则的位置,这个BUILD规则列出了你的Java源代码。举例:#include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h"
)。由于该头文件定义了函数,所以只能从单个.cc
文件中包含此头文件。该.cc
文件必须通过为静态方法定义名为JNI_${OriginalClassName}_${UpperCamelCaseMethod}
的非成员函数,以及为非静态方法定义名为${OriginalClassName}::${UpperCamelCaseMethod}
的成员函数来实现你的原生代码。成员函数也需要在头文件中声明。
示例:
Java代码:
class MyClass {
// 不能是私有的。必须是包级或公共的。
@NativeMethods
/* 包级 */ interface Natives {
void foo();
double bar(int a, int b);
// 要么`nativeMyClass`参数名称中的`MyClass`部分必须与原生类名完全匹配,
// 要么必须使用`@NativeClassQualifiedName("MyClass")`方法注解。
//
// 如果原生类是嵌套的,使用`@NativeClassQualifiedName("FooClassName::BarClassName")`
// 并将参数称为`nativePointer`。
void nonStatic(long nativeMyClass);
}
void callNatives() {
// MyClassJni是由generate_jni规则生成的。
// 将MyClassJni.get()存储在一个字段中会削弱一些所需的R8优化效果,但局部变量是可以的。
Natives jni = MyClassJni.get();
jni.foo();
jni.bar(1,2);
jni.nonStatic(mNativePointer);
}
}
C++代码:
#include "third_party/jni_zero/jni_zero.h"
#include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h"
class MyClass {
public:
void NonStatic(JNIEnv* env);
}
// 注意,与Java不同,C++中的函数名是大写的。
// 静态函数名应遵循此格式,并且不需要声明。
void JNI_MyClass_Foo(JNIEnv* env) { ... }
void JNI_MyClass_Bar(JNIEnv* env, jint a, jint b) { ... }
// 成员函数需要声明。
void MyClass::NonStatic(JNIEnv* env) { ... }
2.3 Calling Native -> Java
由于生成的头文件既包含定义也包含声明,所以不能被多个源文件#include
。如果有多个源文件需要调用的Java函数,应该选择一个源文件通过额外的包装函数将这些函数暴露给其他源文件。
-
使用
@CalledByNative
注解一些方法,生成器将会在${OriginalClassName}_jni.h
头文件中生成存根,以便从cpp中调用这些Java方法。- 内部类方法必须显式提供内部类名称(例如
@CalledByNative("InnerClassName")
)。
- 内部类方法必须显式提供内部类名称(例如
-
在C++代码中,
#include
头文件${OriginalClassName}_jni.h
(路径将取决于列出你的Java源代码的generate_jni
构建规则的位置)。该.cc
文件可以使用生成的名称JAVA_${OriginalClassName}_${UpperCamelCaseMethod}
调用存根。
注意:对于仅用于测试的方法,使用@CalledByNativeForTesting
,这将确保它在我们的发布二进制文件中被剥离。
2.4 使用@JniType
进行自动类型转换
通常,Java类型映射到<jni.h>
中的C++类型(例如java.lang.String
对应jstring
)。大多数人做的第一件事就是将JNI规范类型转换为标准C++类型。
@JniType
来解决这个问题。通过使用@JniType("cpp_type_here")
注解一个参数或返回类型,生成的代码将自动从JNI类型转换为注解中列出的类型。示例如下:
2.4.1 原始代码:
class MyClass {
@NativeMethods
interface Natives {
void foo(
String string,
String[] strings,
MyClass obj,
MyClass[] objs)
}
}
#include "third_party/jni_zero/jni_zero.h"
#include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h"
void JNI_MyClass_Foo(JNIEnv* env, const JavaParamRef<jstring>&, const JavaParamRef<jobjectArray>&, const JavaParamRef<jobject>&, JavaParamRef<jobjectArray>&) {...}
2.4.2 使用@JniType
之后的代码:
class MyClass {
@NativeMethods
interface Natives {
void foo(
@JniType("std::string") String convertedString,
@JniType("std::vector<std::string>") String[] convertedStrings,
@JniType("myModule::CPPClass") MyClass convertedObj,
@JniType("std::vector<myModule::CPPClass>") MyClass[] convertedObjects);
}
}
#include "third_party/jni_zero/jni_zero.h"
#include "<path to BUILD.gn>/<generate_jni target name>/MyClass_jni.h"
void JNI_MyClass_Foo(JNIEnv* env, std::string&, std::vector<std::string>>&, myModule::CPPClass&, std::vector<myModule::CPPClass>&) {...}
2.4.3 实现转换函数(Implementing Conversion Functions)
必须为@JniType
中出现的所有类型定义转换函数。如果忘记添加一个,将在链接时导致错误。
// 转换函数的主模板。
template <typename O>
O FromJniType(JNIEnv*, const JavaRef<jobject>&);
template <typename O>
O FromJniType(JNIEnv*, const JavaRef<jstring>&);
template <typename O>
ScopedJavaLocalRef<jobject> ToJniType(JNIEnv*, const O&);
一个示例转换函数如下所示:
#include "third_party/jni_zero/jni_zero.h"
namespace jni_zero {
template <>
EXPORT std::string FromJniType<std::string>(JNIEnv* env, const JavaRef<jstring>& input) {
// 进行实际的转换为std::string的操作。
const char* chars = env->GetStringUTFChars(str.obj(), nullptr);
std::string result(chars);
env->ReleaseStringUTFChars(str.obj(), chars);
return result;
}
template <>
EXPORT ScopedJavaLocalRef<jstring> ToJniType<std::string>(
JNIEnv* env,
const std::string& input) {
// 进行实际的从std::string的转换操作。
}
} // namespace jni_zero
如果缺少一个转换函数,由于我们在使用它们之前会对转换函数进行前向声明,所以会得到一个链接器错误。
2.4.4 数组转换函数(Array Conversion Functions)
由于部分特化,数组转换函数看起来有所不同。ToJniType
方向还接受一个jclass
参数,该参数是数组成员的类,因为在创建非基本类型数组时Java需要这个参数。
template <typename O>
struct ConvertArray {
static O FromJniType(JNIEnv*, const JavaRef<jobjectArray>&);
static ScopedJavaLocalRef<jobjectArray> ToJniType(JNIEnv*, const O&, jclass);
};
JniZero provides implementations for partial specializations to wrap and unwrap std::vector
for object arrays and some primitive arrays.
JniZero提供了偏特化的实现,用于包装和解包 对象数组和一些基本类型数组的 std::vector
。
JniZero提供了偏特化的实现,以包装和解包 std::vector
用于对象数组和一些原始数组。
2.4.5 可空性(Nullability)
所有非基本的默认JNI C++类型(例如jstring
,jobject
)都是指针类型(即可为空)。一些C++类型(例如std::string
)不是指针类型,因此不能为空指针。这意味着一些返回不可为空类型的转换函数必须处理传入的Java类型为空的情况。
如果T
不是可空类型,你可以通过将转换结果设为std::optional<T>
而不是T
。
2.5 测试可模拟的原生方法(Testing Mockable Natives)
- 将
JniMocker
规则添加到你的测试中。 - 在
setUp()
方法中为每个你想要 stub 掉的接口调用JniMocker#mock
。 JniMocker
将在tearDown()
期间重置这些 stub。
/**
* Tests for {@link AnimationFrameTimeHistogram}
*/
@RunWith(RobolectricTestRunner.class)
public class AnimationFrameTimeHistogramTest {
// Optional: Resets test overrides during tearDown().
// Not needed when using Chrome's test runners.
@Rule public JniResetterRule jniResetterRule = new JniResetterRule();
@Mock
AnimationFrameTimeHistogram.Natives mNativeMock;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
AnimationFrameTimeHistogramJni.setInstanceForTesting(mNativeMock);
}
@Test
public void testNatives() {
AnimationFrameTimeHistogram hist = new AnimationFrameTimeHistogram("histName");
hist.startRecording();
hist.endRecording();
verify(mNativeMock).saveHistogram(eq("histName"), any(long[].class), anyInt());
}
}
如果在单元测试中调用了一个原生方法但没有设置模拟对象,将抛出UnsupportedOperationException
异常。
2.6 Special case: APK Splits
翻译版本一:每个带有自己本地库的APK split 都有自己生成的GEN_JNI
,格式为<module_name>_GEN_JNI
。为了使您的split’s JNI使用<module_name>
前缀,您必须将模块名称添加到@NativeMethods
注解的参数中。
翻译版本二:特殊情况:DFM(动态功能模块):DFM有它们自己生成的GEN_JNIs
,即<module_name>_GEN_JNI
。为了让你的DFM的JNI使用<module_name>
前缀,你必须在@NativeMethods
注解的参数中添加你的模块名称。
例如,假设你的模块名为test_module
。你应该用@NativeMethods("test_module")
注解你的Natives
接口,这将生成test_module_GEN_JNI
。
2.7 使用get()
测试就绪性(Testing for readiness: use get()
)
JNI生成器会自动生成断言,以验证Natives
接口可以安全调用。这些检查会在发布版本的编译中被排除,这是确定你的代码是否被安全调用的绝佳方式。
然而,仅仅使用<Class>Jni.get()
来保证原生代码已初始化是不够的——它只是一个调试工具,用于确保你在加载原生代码之后使用原生代码。
如果你期望你的代码被外部调用者调用,提前知道上下文是否有效(即,要么原生库已加载,要么已安装模拟对象)通常是很有帮助的。在这种情况下,调用get()
方法很有帮助,它会执行上述所有调试检查,但不会实例化一个新对象来与原生库进行交互。请注意,get()
方法返回的未使用值将在发布版本中被优化掉,所以忽略它也没有坏处。
2.7.1 处理Jni.get()
异常(Addressing Jni.get()
exceptions.)
当你发现导致异常的场景时,将相应的调用重新定位(或延迟),移到你知道原生库已初始化的地方(或时间点),(例如onStartWithNative
,onNativeInitialized
等)。
请避免在新代码中调用LibraryLoader.isInitialized()
/ LibraryLoader.isLoaded()
。使用LibraryLoader
调用会使单元测试更加困难:
- 此调用无法验证是否使用了模拟对象,这使得模拟对象的使用更加复杂。
- 使用
LibraryLoader.setLibrariesLoadedForNativeTests()
会改变后续执行测试的状态,不准确地报告这些受影响测试的不稳定性和失败情况。 - 在你的代码中引入
LibraryLoader.is*()
调用会立即影响所有调用者,迫使调用栈上层的代码作者覆盖LibraryLoader
的内部状态,以便能够对他们的代码进行单元测试。
然而,如果你的代码在原生代码初始化之前和之后都会被调用,你不得不调用LibraryLoader.isInitialized()
来进行区分。调用<Class>Jni.get()
仅提供断言,并且如果在原生代码未准备好时调用它,会在调试版本中失败。
2.8 Java对象与垃圾回收
所有指向Java对象的指针都必须向JNI注册,以防止垃圾回收使它们失效。
对于字符串和数组——常见的做法是尽快使用//base/android/jni_*
辅助函数将它们转换为std::vectors
和std::strings
。
对于其他对象——使用智能指针来存储它们:
ScopedJavaLocalRef<>
——当生命周期是当前函数的作用域时使用。ScopedJavaGlobalRef<>
——当生命周期长于当前函数的作用域时使用。JavaObjectWeakGlobalRef<>
——弱引用(不阻止垃圾回收)。JavaParamRef<>
——用于接受上述任何一种类型作为函数参数,而无需创建冗余的注册。
2.9 额外的指导原则/建议
- 尽量减少两端之间的表面API。与其跨边界调用多个函数,不如只调用一个(然后在另一端,根据需要调用尽可能多的小函数)。
- 如果一个Java对象“拥有”一个原生对象,通过
"long mNativeClassName"
存储指针。确保最终调用一个原生方法来删除该对象。例如,有一个close()
方法来删除原生对象。 - 在任一方向上传送“复合”类型的最佳方法是创建一个包含POD(普通旧式数据)的内部类和一个工厂函数。如果可能的话,将所有字段标记为
final
。
最小化双方之间的表面 API 调用。与其跨边界调用多个函数,不如只调用一个(然后在另一侧根据需要调用多个小函数)。
如果一个 Java 对象“拥有”一个原生对象,通过 long mNativeClassName
存储指针。确保最终调用一个原生方法来删除该对象。例如,提供一个 close()
方法来删除原生对象。
跨方向传递“复合”类型的最佳方式是创建一个包含 POD(Plain Old Data)和工厂函数的内部类。如果可能,将所有字段标记为“final”。
3. 构建规则
generate_jni
- 给定一组 Java 文件,生成一个头文件以调用所有带有@CalledByNative
注解的函数到 Java。如果存在@NativeMethods
注解,还会生成一个包含<ClassName>Jni.java
的.srcjar
文件,应通过生成的 GN 目标<generate_jni's target name>_java
来依赖它。generate_jar_jni
- 给定一个.jar
文件,如果每个方法和公共字段都被@CalledByNative
注解标记,则生成一个类似于generate_jni
的头文件。generate_jni_registration
- 生成整个程序的 Java 和原生链接,所有通过@NativeMethods
注解调用到原生的 Java 代码都需要此步骤。shared_library_with_jni
- 一个原生shared_library
的包装器,还会为该库插入一个__jni_registration
目标。component_with_jni
- 与shared_library
类似,但适用于component
。
有关 GN 模板的更多信息,请参阅chromium源码 third_party/jni_zero/jni_zero.gni。
4. JNI Benchmarking
Refer to the performance README.
请参阅chromium源码 third_party/jni_zero/benchmarks/README.md
5. Under the Hood 幕后
对于 @CalledByNative
,我们直接调用 <jni.h>
中的方法,这些方法本质上只是反射 API,然后添加一个 ProGuard 规则以确保被注解的方法/字段在 Java 中被保留。由于我们没有进行任何代理操作,因此注册步骤对这种 JNI 方向没有任何作用。不过,之前已经讨论过对 @CalledByNatives
使用注册步骤:go/proxy-called-by-natives-proposal。
JNI Zero 对 @NativeMethods
有两种主要模式。在每种模式下,我们都会为每个被注解的类插入一个“代理”类,这使我们能够进行测试模拟并更好地优化性能。我们插入一个名为 <EnclosingClass>Jni
的类,这个类只是一个可测试的适配层,用于访问真正的 GEN_JNI
类。GEN_JNI
类是在注册步骤生成的,而注册的工作方式在不同模式下有所不同。
举例,假设我们有以下两个类:
class org.foo.Foo {
@NativeMethods
interface Natives {
int f();
}
}
class org.bar.Bar {
@NativeMethods
interface Natives {
int b();
}
}
会有两个 generate_jni
步骤输出类似以下内容:
// Java .srcjar outputs
class FooJni {
public int f() {
return GEN_JNI.org_foo_Foo_f();
}
}
class BarJni {
public int b() {
return GEN_JNI.org_bar_Bar_b();
}
}
// C++ header outputs
class FooJni {
int Java_GEN_JNI_org_foo_Foo_f() {
return JNI_Foo_f(); // User implements this native function.
}
int Java_GEN_JNI_org_bar_Bar_b() {
return JNI_Bar_b(); // User implements this native function.
}
5.1 Debug Mode
在调试模式下,GEN_JNI
是一个文件,其中包含与程序中每个 generate_jni
中的每一个 @NativeMethods
注解相匹配的本地方法。
class GEN_JNI {
public static native int org_foo_Foo_f();
public static native int org_bar_Bar_b();
}
5.2 Release Mode
在发布模式下,GEN_JNI.java
只是一个调用转发存根,用于连接 N.java
(这是一个简短的名称,用于减小文件大小),而 N
通过签名类型进行多路复用,以减少 JNI 函数的数量。然后,我们生成一个与 N
中较短的函数列表名称相匹配的 C++ 文件,该文件将调用解复用回原始函数。
class GEN_JNI {
public static int org_foo_Foo_f() {
return N._I(0);
}
public static int org_bar_Bar_b() {
return N._I(1);
}
}
class N {
public static native int _I(int switchNum);
}
// Generated C++ to be compiled into the final binary.
int Java_N__1V(jint switch_num) {
switch (switch_num) {
case 0:
return org_foo_Foo_f();
case 1:
return org_bar_Bar_b();
}
}
我们还有“优先级”类的概念,这些类需要位于多路复用编号的前面。这并非出于性能考虑,而是为了使 Chrome 能够通过单个 Java 文件支持多个应用程序二进制接口(ABI)——我们将较小的(子集)ABI 切换编号放在前面,而让超集 ABI 的独特类获得最后的切换编号。
5.3 Legacy Modes 传统模式
这些是 JNI 当前提供的模式,但我们希望将其移除。请不要再添加对这些模式的任何新使用。
5.3.1 Hashed Names 哈希名称
这是我们旧的发布模式。GEN_JNI会像当前发布模式一样调用N,但与其进行多路复用,我们只会取名称的短哈希值,这样我们就有了更短的导出字符串字面量。这也会改变由generate_jni生成的头文件的输出,因为它们同样需要生成一个经过哈希处理的名称。
class GEN_JNI {
public static int org_foo_Foo_f() {
return N.MaQxW612();
}
public static int org_bar_Bar_b() {
return N.M2R2WaZb();
}
}
class N {
public static native int MaQxW612();
public static native int M2R2WaZb();
}
5.3.2 Per-File Natives
这样做是为了让过渡到 JNI Zero 更容易。其理念是,这允许你在无需使用注册步骤的情况下部分接入,因此根本不会生成 GEN_JNI
,而且 generate_jni
步骤的输出与“正常”模式下的输出有所不同。
class FooJni {
public static int f() {
nativeF();
}
public static native nativeF();
}
class BarJni {
public static int b() {
nativeB();
}
public static native nativeB();
}
6. Changing JNI Zero
- Python 黄金测试位于 test/integration_tests.py
- 一个可运行的演示应用程序作为 sample:jni_zero_sample_apk 存在,并且该应用程序在 sample:jni_zero_sample_apk_test 中进行测试。
- 仅编译测试存在于 test:jni_zero_compile_check_apk
- 我们是一个在 Chromium 仓库中开发的 Chromium 项目,但我们打算不依赖 Chromium,以便此项目可以轻松移植。
- jni_zero.py 包含我们的标志并是入口点,jni_generator.py 是每个库生成步骤的主文件,而 jni_registration_generator.py 是整个程序注册步骤的主文件。