NDK | 带你梳理 JNI 函数注册的方式和时机

简介: NDK | 带你梳理 JNI 函数注册的方式和时机

目录

image.png

1. 概述


  • 什么是 JNI 函数注册: 当 Java 虚拟机调用 native 方法时,需要调用对应的 JNI 函数,而 JNI 函数注册讨论的就是如何确定 natvie 方法与 JNI 函数之间的映射关系。
  • JNI 函数注册有哪些方式: 静态注册 + 动态注册
  • 两种注册方式的优缺点:


  • 静态注册的优点是简单,因为静态注册采用的是基于约定的命名规则,所以可以通过 javah 或 IDE 自动生成函数声明。缺点是修改 Java 类名或方法名时,需要同步修改 JNI 函数命名;
  • 动态注册的优点是灵活,因为动态注册可以自由定义 Java 方法和 JNI 函数命名的映射,当 Java 类名或方法名时只需要修改映射关系即可。缺点是牺牲了静态注册基于约定带来的便捷性。


image.png

提示: 有的资料将 注册 JNI 函数 描述为 链接 JNI 函数 / 链接本地方法,其实是一个意思。


2. 静态注册


静态注册采用的是基于「约定」的命名规则,通过 javah 可以自动生成 native 方法对应的函数声明。例如:

HelloWorld.java


package com.xurui.hellojni;
public class HelloWorld {
    public native void sayHi();
}
// 执行命令:javac -h . HelloWorld.java(将 javac 和 javah 合并)
复制代码

com_xurui_hellojni_HelloWorld.h


...
JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi
(JNIEnv *, jobject);
...
复制代码

2.1 命名规则


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


  • 短名称规则(short name)
  • 1、前缀 Java_
  • 2、类的全限定名(带下划线分隔符_);
  • 3、方法名;
  • 长名称规则(long name)
  • 4、在短名称后追加两个下划线(__)和参数描述符


提示: 使用javap命令可以生成符合命名约定的头文件。


1.2 源码分析


我以加载 so 库为线索浏览源码,初步确定静态注册查找的源码。不过,因为没找到调用这个方法的源码,也没有查阅到对应的资料,所以只能说初步确定。


java_vm_ext.cc


共享库列表
std::unique_ptr<Libraries> libraries_;
(已简化)
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


-> 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


std::string GetJniShortName(const std::string& class_descriptor, const std::string& method) {
}
复制代码


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


  • 1、计算 native 方法的短名称和长名称;
  • 2、确定定义 native 方法类的类加载器,在已经加载的 so 库libraries_中搜索实现了本地方法的 JNI 函数;
  • 3、建立内部数据结构,使得对 native 方法的调用可以直接定向到 JNI 函数。


关于加载 so 库的流程在我之前写过的一篇文章里讲过:《NDK | 说说 so 库从加载到卸载的全过程》,加载后的共享库就是存储在libraries_表中。

image.png

3. 动态注册


除了基于约定的静态注册外,还可以通过动态注册来确定 native 方法和 JNI 函数的映射关系。动态注册需要使用 RegisterNatives(...) 函数。


3.1 RegisterNatives(...) 函数


一般会在JNI_Onload(...)函数中执行动态注册,例如:

android_media_MediaPlayer.cpp


jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
    ...
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }
    ...
}
-> 2、调用 AndroidRuntime::registerNativeMethods
static int register_android_media_MediaPlayer(JNIEnv *env) {
    return AndroidRuntime::registerNativeMethods(env,
        "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
1、native 方法与 JNI 方法的映射关系
static const JNINativeMethod gMethods[] = {
    {
        "nativeSetDataSource",
        "(Landroid/os/IBinder;Ljava/lang/String;[Ljava/lang/String;"
        "[Ljava/lang/String;)V",
        (void *)android_media_MediaPlayer_setDataSourceAndHeaders
    },
    {
        "_setDataSource",
        "(Ljava/io/FileDescriptor;JJ)V",
         (void *)android_media_MediaPlayer_setDataSourceFD}
    },
    ...
}
复制代码


以上代码中,gMethods数组定义了 native 方法与 JNI 方法的映射关系,而调用 AndroidRuntime::registerNativeMethods 函数最终会调用RegisterNatives(...)函数,依次绑定 gMethods 数组中的每个映射关系。


JNIHelp.cpp


-> 调用 AndroidRuntime::registerNativeMethods
-> 最终调用的是:JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
        const JNINativeMethod* gMethods, int numMethods) {
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    3、关注点:调用 RegisterNatives
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        ...
    }
    return 0;
}
复制代码


其中,JNINativeMethod 是定义在jni.h中的一个结构体:


typedef struct {
    const char* name; native 方法名
    const char* signature; native 方法的方法描述符
    void* fnPtr; JNI 函数指针
} JNINativeMethod;
复制代码


提示: 这里需要提醒下,很多资料都把 signature 说成是 JNI 这个知识下的概念,事实上它是 JVM 字节码中用于描述方法的字符串,是字节码中的概念。


4.  注册 JNI 函数的时机


经过我的总结,注册 JNI 函数的时机主要分为三种,这三种场景都是比较常见的:


注册的时机 对应的注册方式
1、虚拟机第一次调用 native 方法时 静态注册
2、Android 虚拟机启动时 动态注册
3、加载 so 库时 动态注册


  • 1、虚拟机第一次调用 native 方法时: 这种时机对应于静态注册,当虚拟机第一次调用该 native 方法时,会先搜索对应的 JNI 函数并注册。
  • 2、Android 虚拟机启动时: 在 App 进程启动流程中,在创建虚拟机后会执行一次 JNI 函数注册。我们在很多 Framework 源码中可以看到 native 方法,但找不到调用 System.loadLibrary(...) 的地方,其实是因为在虚拟机启动时就已经注册完成了。


AndroidRuntime.cpp


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);
    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;
}
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*);
}
复制代码


可以看到,Android 虚拟机启动时,会调用startReg()。其中会遍历调用gRegJNI数组,这个数组是一系列注册 JNI 函数的函数指针。


  • 3、加载 so 库时: 在加载 so 库时,会回调JNI_Onload(..),因此这是注册 JNI 函数的好时候,例如上面提到的MediaPlayer也是在这个时候注册 JNI 函数。


5. 总结


  • 应试建议 1、应理解注册 JNI 函数的两种方式:静态注册 & 动态注册
    2、应理解静态注册的函数命名约定、动态注册调用的RegisterNatives(...)
    3、应知晓注册 JNI 函数的三个时机。
目录
相关文章
|
4天前
|
iOS开发
iOS开发解释 App 生命周期,包括各个阶段的调用顺序。
iOS开发解释 App 生命周期,包括各个阶段的调用顺序。
28 1
|
存储 IDE Java
NDK 系列(6):说一下注册 JNI 函数的方式和时机
NDK 系列(6):说一下注册 JNI 函数的方式和时机
86 0
NDK 系列(6):说一下注册 JNI 函数的方式和时机
|
JavaScript 前端开发
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
540 0
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
|
Android开发
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 三 | 等待远程函数执行完毕 | 寄存器获取返回值 )
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 三 | 等待远程函数执行完毕 | 寄存器获取返回值 )
140 0
|
Android开发
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 二 | 准备参数 | 远程调用 mmap 函数 )
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 二 | 准备参数 | 远程调用 mmap 函数 )
113 0
|
Android开发
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 获取注入的 libbridge.so 动态库中的 load 函数地址 并 通过 远程调用 执行该函数 )
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 获取注入的 libbridge.so 动态库中的 load 函数地址 并 通过 远程调用 执行该函数 )
193 0
|
安全 Android开发
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 一 | mmap 函数简介 )
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 一 | mmap 函数简介 )
210 0
|
Android开发
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 获取 linker 中的 dlopen 函数地址 并 通过 远程调用 执行该函数 )
【Android 逆向】Android 进程注入工具开发 ( 注入代码分析 | 获取 linker 中的 dlopen 函数地址 并 通过 远程调用 执行该函数 )
217 0
|
Java Linux Android开发
【Android 逆向】Android 进程注入工具开发 ( 远程进程注入动态库文件操作 | 注入动态库 加载 业务动态库 | 业务动态库启动 | pthread_create 线程开发 )
【Android 逆向】Android 进程注入工具开发 ( 远程进程注入动态库文件操作 | 注入动态库 加载 业务动态库 | 业务动态库启动 | pthread_create 线程开发 )
166 0