JNI方法动态注册

字数 1210阅读 74

简介

虚拟机在加载so库的时候,会调用JNI_OnLoad方法,所以可以在这JNI_OnLoad完成JNI方法动态注册。不清楚为什么系统会调用JNI_OnLoad方法的,请查看上一篇文章System.loadLibrary源码分析

分类

加载so会有这样一条逻辑:

JVM_NativeLoad
       //art/runtime/openjdkjvm/OpenjdkJvm.cc
       ---->LoadNativeLibrary()
                    ---->sym = library->FindSymbol("JNI_OnLoad", nullptr)
                            --->调用JNI_OnLoad方法

在我们要加载so库中查找JNI_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功,如果找到JNI_OnLoad就会调用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注册的函数,所以如果采用动态注册就必须要实现JNI_OnLoad方法,否则调用java中申明的native方法时会抛出异常。

  • 静态注册:
    缺点:方法名很长,不便于书写,初次调用时需要依据名字搜索对应的JNI层函数来建立关联关系,会影响运行效率。
  • 动态注册:
    使用一种数据结构JNINativeMethod来记录java native函数和JNI函数的对应关系移植方便(一个java文件中有多个native方法,java文件的包名更换后)。

JNI开发简单流程

  • main目录下创建cpp目录

  • module目录下创建CMakeLists.txt

  • build.gradle加上ndk相关配置

    externalNativeBuild {
       cmake {
            cppFlags "-frtti -fexceptions"
        }
    }
    
     ndk{
        moduleName "app"
        abiFilters "armeabi","x86" //指定平台
        cFlags "-DANDROID_NDK"
     }
    
     externalNativeBuild {
         cmake {
            path "CMakeLists.txt"
          }
      }
    
    1. -fexceptions
      对整个应用启用异常
    2. -frtti
      对整个应用启用 RTTI

    更多请查看官网C++ 库支持

  • 编写一个有native方法的kotlin类
    比如我这里新建了一个CSocket.java,里面定义了一个本地方法:

    external fun connect(ip: String, port: Int,time: Int): Int
    
  • cpp目录新建C/C++文件
    在cpp文件里创建对应的jni函数,方法名规则是:
    Java + kotlin文件的包路径 + kotlin类名 + native方法名,之间全部用"_"隔开。当然也可以用javah来生成jni头文件,然后头文件里面会生成对应的方法名。写好jni方法后,添加打印代码。

    extern "C" JNIEXPORT int JNICALL
    Java_blog_pds_com_socket_core_client_CSocket_connect( JNIEnv *env,   jobject instance,jstring ip, int port,int time){
      LOGI("start connect!!!!!!");
    }
    
  • 编写CMakeLists.txt文件内容。

    cmake_minimum_required(VERSION 3.4.1)
    add_library(
          socket_client-lib
          SHARED
           src/main/cpp/CSocket.cpp)
    
    find_library(
          log-lib
          log)
    
    target_link_libraries( # Specifies the target library.
          socket_client-lib
          ${log-lib})
    
  • 编译工程
    如果编译不通过,请仔细查看控制台日志,确保NDK库路径设置正确,CMakeLists.txt配置配置正确,特别是里面文件的路径。

  • 加载库文件

    System.loadLibrary("socket_client-lib")
    
  • 运行查看效果
    控制台打印日志:

    2019-06-20 14:53:33.123 28244-28259/? I/socket:: start connect!!!!!!
    

动态注册jni方法

那么接下来就是最总要的部分了,那么怎么进行JNI动态注册呢?其实系统很多地方都用到的动态注册,我们参考MediaPlayer.java里面的native方法的动态注册。看一下MediaPlayer.java的native方法:

private native void _setDataSource(FileDescriptor fd, long offset, long length)
            throws IOException, IllegalArgumentException, IllegalStateException;

定位到/frameworks/base/media/jni/android_media_MediaPlayer.cpp里面的JNI_OnLoad方法。

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
   JNIEnv* env = NULL;
   jint result = -1;

   if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
       ALOGE("ERROR: GetEnv failed\n");
       goto bail;
   }
   ...
   if (register_android_media_MediaPlayer(env) < 0) {
       ALOGE("ERROR: MediaPlayer native registration failed\n");
       goto bail;
   }
   ...
}

看一下register_android_media_MediaPlayer(env)方法

static int register_android_media_MediaPlayer(JNIEnv *env)
{
   return AndroidRuntime::registerNativeMethods(env,
               "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
  • env:JNI运行环境,对应线程
  • className:class类名
  • gMethods:JNI和native方法对应表
  • NELEM(gMethods):gMethods数组长度
gMethods
static const JNINativeMethod gMethods[] = {
   ...

    {"_setDataSource",      "(Ljava/io/FileDescriptor;JJ)V",    (void *)android_media_MediaPlayer_setDataSourceFD},

   ...
}

来分析一下数组的写法:

  • 第一个元素:java文件里面的native函数名

  • 第二个元素:函数的参数和返回类型
    ()里面是参数类型,后面是返回类型

    先看一下Java中的方法签名


    签名列表.png

    L全类名; 引用类型 以L开头、;结尾,中间是引用类型的全类名

  • 第三个参数:函数指针,指向C函数。
    即Java调用_setDataSource native方法,会执行JNI函数名为android_media_MediaPlayer_setDataSourceFD的函数。即用android_media_MediaPlayer_setDataSourceFD替代了之前静态注册使用的函数名。

static void
android_media_MediaPlayer_setDataSourceFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
    ...
}
NELEM(gMethods)

看一下宏定义:

#define NELEM(m) (sizeof(m) / sizeof((m)[0]))

返回的就是数组的长度。

实现自己的动态注册

  • cpp文件添加如下方法,该方法在加载库的时候就会调用,所以可以在该方法里面进行jni方法动态注册。至
    #define NELEM(m) (sizeof(m) / sizeof((m)[0]))
    
    static const JNINativeMethod gMethods[] =
      {
        {"videoSplit","(Ljava/lang/String;Ljava/lang/String;I)V",(void*)native_video_split},
        {"videoMerge","(Ljava/lang/String;Ljava/lang/String;I)V",(void*)native_video_merge},
      };
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
    {
        LOGI("开始动态注册JNI");
       JNIEnv* env = NULL;
       jint result = -1;
       if ((*vm)->GetEnv(vm,(void**) &env , JNI_VERSION_1_4) != JNI_OK){
          LOGI("ERROR: GetEnv failed\n");
          return -1;
       }
        assert(env != NULL);
        registerNatives(env);
        return JNI_VERSION_1_4;
    }
    
    注册
  • registerNatives
    static int registerNatives(JNIEnv* engv){
    
        LOGI("registerNatives begin");
        jclass clazz;
    
        clazz = (*engv)>FindClass(engv,"kt/edu/pds/kt/mrp/ndk/CVideoSplitAndMerge");
        if (clazz == NULL){
            LOGI("clazz is null");
            return JNI_FALSE;
        }
    
        if ((*engv)->RegisterNatives(engv,clazz,gMethods,NELEM(gMethods)) < 0){
        LOGI("RegisterNatives error");
          return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    

推荐阅读更多精彩内容