2. JNI 静态注册与动态注册对比实现

JNI 静态注册与动态注册对比实现 学习卡片

两种注册方式的核心差异

JNI 方法从 Java 层被调用时,虚拟机需要把 native 方法与本地函数建立映射。静态注册依赖命名约定,由虚拟机在首次调用时按 Java_包名_类名_方法名 规则查找符号;动态注册则在库加载阶段通过 JNI_OnLoad 主动调用 RegisterNatives 建立映射表。两者最终都能完成调用,但符号可见性、加载时机和可维护性不同。

JNI 静态注册 vs 动态注册 — 符号表与调用链对比 静态注册(命名约定) Java 层 public native String getMessage(); 首次调用时查找 符号查找规则 Java_com_example_jni_MainActivity_getMessage 包名.类名.方法名 → 下划线连接 Native 函数(C/C++) JNIEXPORT jstring JNICALL Java_..._getMessage(JNIEnv*, jobject) 符号表特征(nm -D libjni.so) T Java_com_example_jni_MainActivity_getMessage T Java_com_example_jni_MainActivity_add T Java_com_example_jni_MainActivity_toUpper 符号名长、可读性差、暴露包名结构 动态注册(RegisterNatives) Java 层 public native String getMessage(); System.loadLibrary 时 JNI_OnLoad 入口 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) 获取 JNIEnv → 调用 RegisterNatives JNINativeMethod 映射表 {"getMessage", "()Ljava/lang/String;", (void*)native_getMessage} 符号表特征(nm -D libjni.so) T JNI_OnLoad t native_getMessage t native_add 符号名短、内部函数不暴露、映射集中管理 对 比 →

静态注册实现

静态注册要求 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 中,但同一方法不应被重复注册。