2. JNI 静态注册与动态注册对比实现
两种注册方式的核心差异
JNI 方法从 Java 层被调用时,虚拟机需要把 native 方法与本地函数建立映射。静态注册依赖命名约定,由虚拟机在首次调用时按 Java_包名_类名_方法名 规则查找符号;动态注册则在库加载阶段通过 JNI_OnLoad 主动调用 RegisterNatives 建立映射表。两者最终都能完成调用,但符号可见性、加载时机和可维护性不同。
静态注册实现
静态注册要求 C/C++ 函数名严格遵循 Java_包名_类名_方法名 格式,包名中的点替换为下划线。以下示例在 Java 层声明三个 native 方法,C++ 层按规则命名实现。
// MainActivity.java
package com.example.jni;
public class MainActivity extends AppCompatActivity {
static { System.loadLibrary("jni"); }
public native String getMessage();
public native int add(int a, int b);
public native String toUpper(String input);
}
// native_static.cpp
#include <jni.h>
#include <string>
extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_jni_MainActivity_getMessage(JNIEnv* env, jobject /*thiz*/) {
return env->NewStringUTF("Hello from static registration");
}
JNIEXPORT jint JNICALL
Java_com_example_jni_MainActivity_add(JNIEnv* /*env*/, jobject /*thiz*/, jint a, jint b) {
return a + b;
}
JNIEXPORT jstring JNICALL
Java_com_example_jni_MainActivity_toUpper(JNIEnv* env, jobject /*thiz*/, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
std::string s(str);
for (auto& c : s) c = toupper(c);
env->ReleaseStringUTFChars(input, str);
return env->NewStringUTF(s.c_str());
}
} // extern "C"
静态注册的符号名由包名和类名决定,一旦 Java 包名或类名变更,C/C++ 函数名必须同步修改,否则运行时报
UnsatisfiedLinkError。动态注册实现
动态注册在 JNI_OnLoad 中通过 RegisterNatives 将 Java 方法与本地函数指针绑定。本地函数可以使用任意名称,映射关系集中在一张 JNINativeMethod 表中。
// native_dynamic.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "JNI_DEMO"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 本地函数使用自定义名称,不遵循 Java_ 命名规则
static jstring native_getMessage(JNIEnv* env, jobject /*thiz*/) {
return env->NewStringUTF("Hello from dynamic registration");
}
static jint native_add(JNIEnv* /*env*/, jobject /*thiz*/, jint a, jint b) {
return a + b;
}
static jstring native_toUpper(JNIEnv* env, jobject /*thiz*/, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
std::string s(str);
for (auto& c : s) c = toupper(c);
env->ReleaseStringUTFChars(input, str);
return env->NewStringUTF(s.c_str());
}
// 映射表:Java 方法名、方法签名、本地函数指针
static const JNINativeMethod gMethods[] = {
{"getMessage", "()Ljava/lang/String;", (void*)native_getMessage},
{"add", "(II)I", (void*)native_add},
{"toUpper", "(Ljava/lang/String;)Ljava/lang/String;", (void*)native_toUpper},
};
static const char* kClassName = "com/example/jni/MainActivity";
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
JNIEnv* env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass(kClassName);
if (clazz == nullptr) {
LOGI("FindClass failed: %s", kClassName);
return JNI_ERR;
}
jint ret = env->RegisterNatives(clazz, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
if (ret != JNI_OK) {
LOGI("RegisterNatives failed: %d", ret);
return JNI_ERR;
}
LOGI("Dynamic registration succeeded, %zu methods registered",
sizeof(gMethods) / sizeof(gMethods[0]));
return JNI_VERSION_1_6;
}
动态注册中
FindClass 使用的类名必须是斜杠分隔的 JNI 格式(com/example/jni/MainActivity),不是点分隔的 Java 格式。方法签名必须与 Java 声明完全一致,否则 RegisterNatives 返回负值。符号表验证方法
编译生成 libjni.so 后,使用 NDK 工具链中的 nm 或 readelf 查看动态符号表,可以直接观察两种注册方式的差异。
# 进入 NDK 工具链目录(以实际路径为准)
# 查看动态符号表
$NM -D --defined-only libjni.so | grep -E "Java_|JNI_OnLoad|native_"
# 静态注册典型输出:
# T Java_com_example_jni_MainActivity_add
# T Java_com_example_jni_MainActivity_getMessage
# T Java_com_example_jni_MainActivity_toUpper
# 动态注册典型输出:
# T JNI_OnLoad
# t native_add
# t native_getMessage
# t native_toUpper
| 对比维度 | 静态注册 | 动态注册 |
|---|---|---|
| 函数命名 | 必须遵循 Java_包名_类名_方法名 | 任意名称,通过映射表绑定 |
| 绑定时机 | 首次调用时由虚拟机查找 | 库加载时 JNI_OnLoad 主动注册 |
| 符号表可见性 | 导出完整 Java_ 符号,暴露包名结构 | 仅导出 JNI_OnLoad,内部函数不暴露 |
| 方法签名检查 | 编译期不检查,运行时按名称匹配 | RegisterNatives 时校验签名,失败可提前发现 |
| 批量注册 | 每个方法独立命名,无法集中管理 | 一张 JNINativeMethod 表集中管理 |
| 适用场景 | 方法少、包名稳定、快速验证 | 方法多、需要混淆或动态加载 |
调用行为验证
在 Java 层调用三个 native 方法,观察返回值是否与预期一致。两种注册方式下调用行为完全相同,差异仅体现在符号表和加载阶段。
// 在 Activity 中调用
TextView tv = findViewById(R.id.tv_result);
String msg = getMessage(); // "Hello from ... registration"
int sum = add(3, 5); // 8
String upper = toUpper("jni"); // "JNI"
tv.setText(msg + "\n" + sum + "\n" + upper);
验证要点:
- 静态注册:若 C/C++ 函数名拼写错误,首次调用抛出
UnsatisfiedLinkError: No implementation found for ...。 - 动态注册:若
RegisterNatives返回负值,检查方法签名是否与 Java 声明一致;若FindClass返回 null,检查类名是否为斜杠格式。 - 两种方式可以共存于同一个 .so 中,但同一方法不应被重复注册。