美文网首页Android开发Android开发Android进阶之路
NDK | 带你梳理 JNI 函数注册的方式和时机

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

作者: 彭旭锐 | 来源:发表于2020-11-09 21:42 被阅读0次

    前言

    • 调用 Java 类中定义的 native 方法时,虚拟机会调用对应的 JNI 函数,而这些 JNI 函数需要先注册才能使用;
    • 在这篇文章里,我将带你梳理 JNI 函数注册的方式和时机。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

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

    相关文章


    目录


    1. 静态注册

    1.1 静态注册的命名规则

    静态注册采用的是 基于约定 方式,即:对于一个 native 方法,与之对应的 JNI 函数的命名规则如下:

    • 短名称(short name)

    1、前缀 Java_
    2、类的全限定名(带下划线分隔符_);
    3、方法名;

    • 长名称(long name)

    由于存在方法重载的情况,而上面说的短名称规则是无法区分方法重载的。因此,有重载方法的时候,还需要在短名称后追加参数描述符。

    4、追加两个下划线(__)和参数描述符

    举个例子,MediaPlayer#native_init()对应的 JNI 函数为android_media_MediaPlayer_native_init()

    android.media.MediaPlayer.java

    public class MediaPlayer extends PlayerBase implements ... {
        private static native final void native_init();
    }
    

    android_media_MediaPlayer.cpp

    static void
    android_media_MediaPlayer_native_init(JNIEnv *env)
    {
        jclass clazz;
        clazz = env->FindClass("android/media/MediaPlayer");
        fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
        ...
    }
    

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

    1.2 静态注册查找源码分析

    静态注册查找的源码应该在这里:

    提示:为什么要说应该?因为丑丑也没有找到调用这个方法的源码,也没有查阅到对应的资料,所以不是很确定。

    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、在已经加载的 so 库libraries_中搜索,关于加载 so 库的流程在我之前写过的一篇文章里讲过:《NDK | 说说 so 库从加载到卸载的全过程》。加载后的共享库就是存储在libraries_表中。
    • 2.1 检查是否为相同 ClassLoader
    • 2.2 先搜索短名称
    • 2.3 再搜索长名称

    2. 动态注册

    2.1 RegisterNatives(...) 函数

    除了基于约定的静态注册外,还可以调用RegisterNatives(...)来注册 native 方法与关联的 JNI 函数。一般来说,会在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}
        },
        ...
    }
    

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

    上面的代码已经非常简化了,主要关注以下几点:

    • 1、gMethods数组表示了 native 方法与 JNI 方法的映射关系
    • 2、调用 AndroidRuntime::registerNativeMethods
    • 3、最终调用的是:RegisterNatives(...)函数

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

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

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

    2.2 优缺点对比

    • 静态注册优点:简单直接,不需要编写注册 JNI 函数的代码,但是修改 Java 代码类名或方法后,需要修改 JNI 函数命名;

    • 动态注册优点:灵活性高,当需要修改类名或方法名时,不需要修改 JNI 函数。


    3. 注册 JNI 函数的时机

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

    • 1、虚拟机第一次调用 native 方法时

    这种时机对应于静态注册,当虚拟机第一次调用该 native 方法时,会先搜索对应的 JNI 函数并注册。

    • 2、Android 虚拟机启动时

    在 App 进程启动流程中,在创建虚拟机后会执行一次 JNI 函数注册,源码为:

    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 函数。

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

    4. 总结

    • 应试建议
      1、应理解注册 JNI 函数的两种方式:静态注册 & 动态注册
      2、应理解静态注册的函数命名约定、动态注册调用的RegisterNatives(...)
      3、应知晓注册 JNI 函数的三个时机。

    参考资料

    推荐阅读

    感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

    相关文章

      网友评论

        本文标题:NDK | 带你梳理 JNI 函数注册的方式和时机

        本文链接:https://www.haomeiwen.com/subject/ojnlbktx.html