请点赞关注,你的反对对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。

前言

在上一篇文章中,咱们提到了注册 JNI 函数(建设 Java native 办法和 JNI 函数的映射关系)有两种形式:动态注册和动静注册。明天咱们来具体说下这 2 种注册形式的应用办法和实现原理。


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

一、语言根底:

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

二、NDK 开发:

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

三、基础理论

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

四、计算机根底

  • 1、字符编码:ASCII、Unicode、UTF-8、UTF-16、UTF-32

1. 动态注册 JNI 函数

1.1 动态注册应用办法

动态注册采纳的是基于「约定」的命名规定,通过 javah 能够主动生成 native 办法对应的函数申明(IDE 会智能生成,不须要手动执行命令)。例如:

HelloWorld.java

package com.xurui.hellojni;public class HelloWorld {    public native void sayHi();}

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

com_xurui_hellojni_HelloWorld.h

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

// 共享库列表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

// 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 函数。如果当时没有加载 so 库,则天然无奈搜寻到,将抛出 UnsatisfiedLinkError 异样;
  • 3、建设外部数据结构,建设 Java native 办法与 JNI 函数的函数指针的映射关系;
  • 4、后续调用 native 办法,则间接调用已记录的函数指针。

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


2. 动静注册 JNI 函数

动态注册是在首次调用 Java native 办法时搜寻对应的 JNI 函数,而动静注册则是提前手动建设映射关系,并且不须要恪守动态注册的 JNI 函数命名规定。

2.1 动静注册应用办法

动静注册须要应用 RegisterNatives(...) 函数,其定义在 jni.h 文件中:

jni.h

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;

示例程序

// 须要注册的 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

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 文件体积。

参考资料

  • JNI 编程指南
  • JNI 提醒 —— Android 官网文档
  • Java 原生接口标准 —— Java 官网文档
  • Android 对 so 体积优化的摸索与实际 —— 洪凯 常强(美团技术团队)著
你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!