Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >NDK 系列(6):说一下注册 JNI 函数的方式和时机

NDK 系列(6):说一下注册 JNI 函数的方式和时机

作者头像
用户9995743
发布于 2022-09-26 12:57:44
发布于 2022-09-26 12:57:44
39800
代码可运行
举报
文章被收录于专栏:彭旭锐彭旭锐
运行总次数:0
代码可运行

🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook[1] 中。

前言

在上一篇文章中,我们提到了注册 JNI 函数(建立 Java native 方法和 JNI 函数的映射关系)有两种方式:静态注册和动态注册。今天我们来详细说下这 2 种注册方式的使用方法和实现原理。


这篇文章是 NDK 系列文章第 6 篇,专栏文章列表:

一、语言基础:

  • 1、NDK 学习路线:怎么学 & 我的经验
  • 2、C 语言基础
  • 3、C ++ 语言基础
  • 4、C/C++ 编译过程:从源码到程序运行

二、NDK 开发:

  • 1、JNI 基础:Java 与 Native 交互
  • 2、注册 JNI 函数:静态注册 & 动态注册(本文)[2]
  • 3、NDK 基础:ndk-build & CMake
  • 4、so 文件加载过程分析:理解 Android 中 loadLibrary 的执行流程[3]
  • 5、so 文件适配 64 位架构:Gradle 插件一键检索未适配项[4]
  • 6、so 文件动态化:动态下载
  • 7、so 文件体积优化:文件精简

三、基础理论

  • 1、视频基础理论
  • 2、音频基础理论
  • 3、H.264 视频压缩编码
  • 4、音频压缩编码
  • 5、FFMPEG 基础
  • 6、OPENSL ES 基础
  • 7、PNG 图片:无损压缩编码[5]

四、计算机基础


1. 静态注册 JNI 函数

1.1 静态注册使用方法

静态注册采用的是基于「约定」的命名规则,通过 javah 可以自动生成 native 方法对应的函数声明(IDE 会智能生成,不需要手动执行命令)。例如:

HelloWorld.java

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.xurui.hellojni;

public class HelloWorld {
    public native void sayHi();
}

执行命令:javac -h . HelloWorld.java(将 javac 和 javah 合并),对应的 JNI 函数:

com_xurui_hellojni_HelloWorld.h

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi
(JNIEnv *, jobject);
...

静态注册的命名规则分为「无重载」和「有重载」2 种情况:无重载时采用「短名称」规则,有重载时采用「长名称」规则。

  • 短名称规则(short name): Java_[类的全限定名 (带下划线)]_[方法名] ,其中类的全限定名中的 . 改为 _
  • 长名称规则(long name): 在短名称的基础上后追加两个下划线(__)和参数描述符,以区分函数重载。

这里解释下为什么有重载的时候要拼接参数描述符的方式来呢?因为 C 语言是没有函数重载的,无法根据参数来区分函数重载,所以才需要拼接后缀来消除重载。

1.2 静态注册原理分析

现在,我们来分析下静态注册匹配 JNI 函数的执行过程。由于没有找到直接相关的资料和函数调用入口,我是以 loadLibrary() 加载 so 库的执行流程为线索进行分析的,最终定位到 FindNativeMethod() 这个方法,从内容看应该没错。

java_vm_ext.cc[6]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 共享库列表
std::unique_ptr<Libraries> libraries_;

// 搜索 native 对应 JNI 函数的函数指针
void* FindNativeMethod(Thread* self, ArtMethod* m, std::string& detail) {

    // 1、获取 native 方法对应的短名称与长名称
    std::string jni_short_name(m->JniShortName());
    std::string jni_long_name(m->JniLongName());

    // 2、在已经加载的 so 库中搜索
    void* native_code = FindNativeMethodInternal(self,
                                                 declaring_class_loader_allocator,
                                                 shorty,
                                                 jni_short_name,
                                                 jni_long_name);
    return native_code;
}

// 2、在已经加载的 so 库中搜索
void* FindNativeMethodInternal(Thread* self,
                               void* declaring_class_loader_allocator,
                               const char* shorty,
                               const std::string& jni_short_name,
                               const std::string& jni_long_name) {
    for (const auto& lib : libraries_) {
        SharedLibrary* const library = lib.second;

        // 2.1 检查是否为相同 ClassLoader
        if (library->GetClassLoaderAllocator() != declaring_class_loader_allocator) {
            continue;
        }

        // 2.2 先搜索短名称
        const char* arg_shorty = library->NeedsNativeBridge() ? shorty : nullptr;
        void* fn = dlsym(library, jni_short_name)

        // 2.3 再搜索长名称
        if (fn == nullptr) {
            fn = dlsym(library, jni_long_name)
        }
        if (fn != nullptr) {
            return fn;
        }
    }
    return nullptr;
}

art_method.cc[7]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 1、获取 native 方法对应的短名称与长名称

// 短名称
std::string ArtMethod::JniShortName() {
    return GetJniShortName(GetDeclaringClassDescriptor(), GetName());
}

// 长名称
std::string ArtMethod::JniLongName() {
    std::string long_name;
    long_name += JniShortName();
    long_name += "__";

    std::string signature(GetSignature().ToString());
    signature.erase(0, 1);
    signature.erase(signature.begin() + signature.find(')'), signature.end());

    long_name += MangleForJni(signature);

    return long_name;
}

descriptors_names.cc[8]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::string GetJniShortName(const std::string& class_descriptor, const std::string& method) {
    // 此处为短名称的计算逻辑
}

上面的代码已经非常简化了,主要流程如下:

  • 1、计算 native 方法的短名称和长名称;
  • 2、确定定义 native 方法类的类加载器,在已经加载的 so 库 libraries_ 中搜索 JNI 函数。如果事先没有加载 so 库,则自然无法搜索到,将抛出 UnsatisfiedLinkError 异常;
  • 3、建立内部数据结构,建立 Java native 方法与 JNI 函数的函数指针的映射关系;
  • 4、后续调用 native 方法,则直接调用已记录的函数指针。

关于加载 so 库的流程在 4、so 文件加载过程分析[9] 这篇文章里讲过,加载后的共享库就是存储在 libraries_ 表中。


2. 动态注册 JNI 函数

静态注册是在首次调用 Java native 方法时搜索对应的 JNI 函数,而动态注册则是提前手动建立映射关系,并且不需要遵守静态注册的 JNI 函数命名规则。

2.1 动态注册使用方法

动态注册需要使用 RegisterNatives(...) 函数,其定义在 jni.h 文件中:

jni.h

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct JNINativeInterface {
  // 注册
  // 参数二:Java Class 对象的表示
  // 参数三:JNINativeMethod 结构体数组
  // 参数四:JNINativeMethod 结构体数组长度
  jint        (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
  // 注销
  // 参数二:Java Class 对象的表示
    jint        (*UnregisterNatives)(JNIEnv*, jclass);
};

typedef struct {
    const char* name;      // Java 方法名
    const char* signature; // Java 方法描述符
    void*       fnPtr;     // JNI 函数指针
} JNINativeMethod;

示例程序

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 需要注册的 Java 层类名
#define JNIREG_CLASS "com/xurui/MainActivity"

// JNINativeMethod 结构体数组
static JNINativeMethod gmethod[] = {
        {"onStart","()I",(void*)onStart}, // JNINativeMethod 结构体
};

// 加载 so 库的回调
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

  // 执行动态注册
    if (!registerNatives(env)) {
        return -1;
    }

    result = JNI_VERSION_1_6;

    return result;
}

// 动态注册
static int registerNatives(JNIEnv* env)
{
  // 调用工具方法完成注册
    if (!registerNativeMethods(env,JNIREG_CLASS, gmethod, sizeof(gmethod) / sizeof(gmethod[0])))
        return JNI_FALSE;

  // JNINativeMethod 结构体数组
    JNINativeMethod methods[] = {
            {"onStop","()V",(void*)onStop}, // JNINativeMethod 结构体
    };
  // 调用工具方法完成注册
    if (!registerNativeMethods(env,JNIREG_CLASS, smethod,sizeof(smethod) / sizeof(smethod[0])))
        return JNI_FALSE;
    return JNI_TRUE;
}

// 一个工具方法
static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gmethod, int numMethods)
{
  // 根据类名获取 jclass 对象
    jclass clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
  // 调用 RegisterNatives()
    if (env->RegisterNatives(clazz, gmethod, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

代码不复杂,简单解释一下:

  • 1、registerNativeMethods 只是一个工具函数,简化了 FindClass 这一步;
  • 2、methods 是一个 JNINativeMethod 结构体数组,定义了 Java native 方法与 JNI 函数的映射关系,需要记录 Java 方法名、Java 方法描述符、JNI 函数指针;
  • 3、RegisterNatives 函数是最终的注册函数,需要传递 jclass、JNINativeMethod 结构体数组和数组长度。

2.2 动态注册原理分析

RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针,因此动态注册的 native 方法调用效率更高。此外,还能减少生成 so 库文件中导出符号的数量,则能够优化 so 库文件的体积。更多信息见 Android 对 so 体积优化的探索与实践 中 “精简动态符号表” 章节。


3. 注册 JNI 函数的时机

总结一下注册 JNI 函数的时机,主要分为 3 种:

注册时机

注册方式

描述

1、在第一次调用该 native 方法时

静态注册

虚拟机会在 JNI 函数库中搜索函数指针并记录下来,后续调用不需要重复搜索

2、加载 so 库时

动态注册

加载 so 库时会自动回调 JNI_OnLoad 函数,在其中调用 RegisterNatives 注册

3、提前注册

动态注册

在加载 so 库后,调用该 native 方法前,通过静态注册的 native 函数触发 RegisterNatives 注册。例如在 App 启动时,很多系统源码会提前做一次注册

以 Android 虚拟机源码为例:在 App 进程启动流程中,在创建虚拟机后会执行一次 JNI 函数注册。我们在很多 Framework 源码中可以看到 native 方法,但找不到调用 System.loadLibrary(...) 的地方,其实是因为在虚拟机启动时就已经注册完成了。

AndroidRuntime.cpp[10]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
    ...
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\\n");
    }
    ...
}

// start -> startReg:
int AndroidRuntime::startReg(JNIEnv* env) {
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
    env->PushLocalFrame(200);
  // 执行 JNI 注册
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);
    return 0;
}

// startReg->register_jni_procs:
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) {
  // 遍历二维数组
    for (size_t i = 0; i < count; i++) {
        // 执行 JNI 注册
        if (array[i].mProc(env) < 0) {
            return -1;
        }
    }
    return 0;
}

// JNINativeMethod 结构体数组的二维数组
static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    ...
}

struct RegJNIRec {
    int (*mProc)(JNIEnv*);
}

// 返回值为 JNINativeMethod 结构体数组
int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
{
    const JNINativeMethod methods[] = {
        { "nativeFinishInit", "()V",
            (void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
        { "nativeSetExitWithoutCleanup", "(Z)V",
            (void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
    };
    return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
        methods, NELEM(methods));
}

4. 总结

总结一下静态注册和动态注册的区别:

  • 1、静态注册基于命名约定建立映射关系,而动态注册通过 JNINativeMethod 结构体建立映射关系;
  • 2、静态注册在首次调用该 native 方法搜索并建立映射关系,而动态注册会在调用该 native 方法前建立映射关系;
  • 3、静态注册需要将所有 JNI 函数暴露到动态符号表,而动态注册不需要暴露到动态符号表,可以精简 so 文件体积。

参考资料

参考资料

[1]

GitHub · Android-NoteBook: https://github.com/pengxurui/Android-NoteBook

[2]

2、注册 JNI 函数:静态注册 & 动态注册(本文): https://juejin.cn/post/7125021894562349092

[3]

4、so 文件加载过程分析:理解 Android 中 loadLibrary 的执行流程: https://juejin.cn/post/6892793299427491854

[4]

5、so 文件适配 64 位架构:Gradle 插件一键检索未适配项: https://juejin.cn/post/7034201332500135966

[5]

7、PNG 图片:无损压缩编码: https://juejin.cn/post/6905635070397612039

[6]

java_vm_ext.cc: http://androidxref.com/9.0.0_r3/xref/art/runtime/java_vm_ext.cc

[7]

art_method.cc: http://androidxref.com/9.0.0_r3/xref/art/runtime/art_method.cc

[8]

descriptors_names.cc: http://androidxref.com/9.0.0_r3/xref/art/libdexfile/dex/descriptors_names.cc

[9]

4、so 文件加载过程分析: https://juejin.cn/post/6892793299427491854

[10]

AndroidRuntime.cpp: http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/jni/AndroidRuntime.cpp

[11]

JNI 提示: https://developer.android.google.cn/training/articles/perf-jni

[12]

Java 原生接口规范: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 彭旭锐 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JNI动态注册以及JNI签名
  注册native方法有两种方式,动态注册和静态注册。静态注册是在编译时进行注册,而且在java中声明的native方法和c/c++中的本地方法的对应关系是恒定的;比如说在com.example.test包下的Test类中声明了一个stringFromJNI()的native方法,那么它对应的c/c++中的本地方法名就是Java_com_example_test_Test_stringFromJNI();并且这个方法名不能做任何的修改,在java中调用stringFromJNI()函数时,就会按包名_类名_方法名的形式找到对应的方法并调用。而动态注册是在运行时进行注册的,而且本地方法的名字可以按自己的喜好随意取,只要说明了java中声明的native方法和c/c++中的本地方法的对应关系即可。下面用代码的形式来演示一下动态注册的使用步骤。
故乡的樱花开了
2023/10/22
5800
JNI动态注册以及JNI签名
【Android NDK 开发】JNI 动态注册 ( 动态注册流程 | JNI_OnLoad 方法 | JNINativeMethod 结构体 | GetEnv | RegisterNatives )
① 声明 Java 层 Native 方法 : 在 Java 类中声明 native 方法 ;
韩曙亮
2023/03/27
1.2K0
Android JNI和NDK学习(03)--动态方式实现JNI
前面总结了静态实现JNI的方法,本文介绍如何动态实现JNI:JNI在加载时,会调用JNI_OnLoad,而卸载时会调用JNI_UnLoad,所以我们可以通过在JNI_OnLoad里面注册我们的native函数来实现JNI。下面就介绍该方法。
战神伽罗
2019/07/30
8030
Android JNI和NDK学习(03)--动态方式实现JNI
NDK 开发中 Native 方法的静态注册与动态注册
NDK 开发中,通过 javah -jni 命令生成的包含 JNI 的头文件,接口的命名方式一般是: Java_<PackageName>_<ClassName>_<MethodName> 。
字节流动
2020/06/03
1.3K0
JNI中native方法的几种注册方式
面试NDK开发的时候,经常碰到一个问题:如何在jni中注册native函数,有几种注册方式? 答案:native方法的注册分为静态注册和动态注册
包子388321
2020/06/16
2.1K0
什么是动态注册?什么是静态注册?
默认情况下,就是静态注册,静态注册是最简单的方式,NDK开发过程中,基本上使用静态注册。前面的知识都是静态注册的方式。
派大星在吗
2021/12/18
2.1K0
JNI操作接口实践(三)
        前面我们介绍了JNI的常规注册方法(静态注册方法),并对JNI各种典型应用做了一个实践演练。本文我们介绍JNI的另一个注册方法:动态注册方法
用户4148957
2022/06/14
3380
JNI源码分析 (并实现JNI动态注册)
本博客转载自网址:http://blog.csdn.net/urrjdg/article/details/78091094
AWeiLoveAndroid
2018/09/03
1.7K0
注册JNI函数的两种方式
前言 前面介绍过如何实现在Android Studio中制作我们自己的so库,相信大家看过之后基本清楚如何在Android studio创建JNI函数并最终编译成不同cpu架构的so库,但那篇文章介绍注册JNI函数的方法(静态方法)存在一些弊端,本篇将介绍另外一种方法(动态注册)来克服这些弊端。 注册JNI函数的两种方法 静态方法 这种方法我们比较常见,但比较麻烦,大致流程如下: 先创建Java类,声明Native方法,编译成.class文件。 使用Javah命令生成C/C++的头文件,例如:javah -
巫山老妖
2018/07/20
1.3K0
JNI--动态注册
静态注册: 每个class都需要使用javah生成一个头文件,并且生成的名字很长书写不便;初次调用时需要依据名字搜索对应的JNI层函数来建立关联关系,会影响运行效率 用javah 生成头文件方便简单 动态注册: 使用一种数据结构JNINativeMethod来记录java native函数和JNI函数的对应关系 移植方便(一个java文件中有多个native方法,java文件的包名更换后)
aruba
2020/07/03
6740
Android JNI学习(三)——Java与Native相互调用
前面两篇文章简单的介绍了JNI,下面我们就进一步了解下一下JNI的调用原则,要想了解JNI的调用原则, 前面我们说了JNI中的JNIEnv以及Java类型和native中的类型映射关系。下面我们先来看注册native函数
隔壁老李头
2018/08/30
3.1K0
Android JNI学习(三)——Java与Native相互调用
你应该了解的JNI知识(一)——静态注册与动态注册
由于我是做Android的,因此重点关注JNI,主要是总结应该知道的一些JNI知识。
用户1108631
2019/08/17
2.3K0
【Android】JNI静态与动态注册介绍
JNI(Java Native Interface),即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互.
后端码匠
2023/02/27
1.1K0
【Android】JNI静态与动态注册介绍
Android深入理解JNI(一)JNI原理与静态、动态注册
前言 JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层。这一个系列我们来一起深入学习JNI。 1.JNI概述 Android系统按语言来划分的话由两个世界组成,分别是Java世界和Native世界。那为什么要这么划分呢?Android系统由Java写不好吗?除了性能的之外,最主要的原因就是在Java诞生之前,就有很多程序和库都是由Native语言写的,因此,重复利用这些Native语言编写的库是十分必要
用户1269200
2018/02/01
3K0
Android深入理解JNI(一)JNI原理与静态、动态注册
JNI原理
1.通过System.loadLibrary()或System.load()加载动态库
老马的编程之旅
2022/06/22
9700
JNI原理
JNI函数加载
当Java代码中执行Native的代码的时候,首先是通过一定的方法来找到这些native方法。JNI有如下两种注册native方法:
小蚂蚁与大象
2020/07/22
8190
# JNI方法注册源码分析(JNI_OnLoad|动态注册|静态注册|方法替换)
开发Android应用时,有时候Java层的编码不能满足实际需求,需要通过JNI的方式利用C/C++实现重要功能并生成SO文件,再通过System.loadLibrary()加载进行调用。常见的场景如:加解密算法、音视频编解码、数据采集、设备指纹等。通常核心代码都封装在SO文件中,也自然成为“黑客”攻击的目标对象,利用IDA Pro等逆向工具,可以轻松反编译未采取任何保护措施的SO文件,生成近似源代码的C代码,业务逻辑、核心技术将直接暴露在攻击者的眼前。进一步造成核心技术泄漏、隐私数据泄漏、业务逻辑恶意篡改等危害。
静默加载
2022/04/29
3K0
# JNI方法注册源码分析(JNI_OnLoad|动态注册|静态注册|方法替换)
JNI 解析以及在 Android 中的实际应用
JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(在Android里面主要是C&C++)。
字节流动
2021/12/28
1.5K0
JNI 解析以及在 Android 中的实际应用
【Android 内存优化】Android 原生 API 图片压缩原理 ( 图片质量压缩方法 | 查找 Java 源码中的 native 方法对应的 C++ 源码 )
在博客 【Android 内存优化】图片文件压缩 ( Android 原生 API 提供的图片压缩功能能 | 图片质量压缩 | 图片尺寸压缩 ) 简要介绍了 图片文件压缩格式 , 以及 Android 提供的图片质量 , 尺寸压缩原生 API ;
韩曙亮
2023/03/27
9670
【Android 内存优化】Android 原生 API 图片压缩原理 ( 图片质量压缩方法 | 查找 Java 源码中的 native 方法对应的 C++ 源码 )
Android跨进程通信IPC之3——关于"JNI"的那些事
在分析IPC基于Android 6.0)的过程中,里面的核心部分是Native的,并且还包含一些linux kernel,而作为Android开发者看到的代码大部分都是Java层,所以这就一定会存在Java与C/C++代码的来回跳转,那么久很有必要来先说一下JNI,本文主要内容如下:
隔壁老李头
2018/08/30
2.8K0
Android跨进程通信IPC之3——关于"JNI"的那些事
推荐阅读
相关推荐
JNI动态注册以及JNI签名
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验