再探JNI

162 阅读8分钟

JNI用法

根据JNI的定义可以知道,Java层想要调用JNI层函数,就需要JNI来实现。接下来看看JNI的使用方法:

JNI的使用主要有以下几个步骤:

1. Java 层

在Java层,主要完成两件事:

1.1 加载JNI库

使用System.loadLibrary()加载so.

例如,导入名为"hello_jni"的库。


static {

    System.loadLibrary("hello_jni");

}

加载的库的名字为“hello_jni”,这个库在不同的平台的拓展名不一样:

  • linux:在linux上会拓展为xxx.so。例子中的库会被拓展为hello_jni.so
  • windows:在windows上会拓展为xxx.dll。例子中的库会被拓展为hello_jni.dll

原则上,JNI库只要在调用定义的native函数之前加载就行,一般放在static中进行加载。

1.2 定义满足业务需求的函数

例如,定义一个函数,打印一个字符串。

public static native String sayHello();

其中native关键字表示这个函数将由JNI层实现。

完整的Java例子代码如下:

package com.example.jnidemo;

public class JniExample {

    static {

        System.loadLibrary("hello_jni");

    }

    public static void javaSayHello(){

        System.out.println("java say hello");

    }

    // 使用native关键字定义一个函数
    public static native String sayHello();

}

2. 编译生成头文件

2.1 编译生成class文件

将在Java层完成的代码编译程.class文件。在例子中就是将JniExample.java编译成JniExample.class。

有两个方法:

  1. 使用Android studio的 build->build/rebuild project

使用Android Studio编译生成的.class文件在app/build/intermediates/javac/debug/classes下面。

image.png

  1. 使用javac指令编译
javac xxx.java

例如:javac JniExample.java

使用指令生成的.class文件在对应的Java类的目录下。当然也可以修改生成的文件的路径。

2.2 反编译生成.h头文件

使用javah指令反编译2.1中生成的.class字节码文件

javah -o <targetjniname> <classpath>

例如 javah -o hello_jni.h com.example.jnidemo.JniExample

其中 -o 表示输出文件,-o之后的参数最好使用System.``loadLibrary导入的库的名字。如果反编译javac指令 编译出的.class出现问题,可以使用cdbuil生成的文件下使用javah反编译。

反编译完成的.h文件如下所示:

hello.h


/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_example_jnidemo_JniExample */



#ifndef _Included_com_example_jnidemo_JniExample

#define _Included_com_example_jnidemo_JniExample

#ifdef __cplusplus

extern "C" {

#endif

/*

 * Class:     com_example_jnidemo_JniExample

 * Method:    sayHello

 * Signature: ()V

 */

JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JniExample_sayHello

  (JNIEnv *env, jclass);



#ifdef __cplusplus

}

#endif

#endif

3. 环境配置

在实现反编译生成的头文件之前,需要配置环境。

3.1 配置ndk

  1. 下载NDK

下载NDK主要有两种方法,任选其一即可:

  • 在Android Studio中通过 File -> Settings -> Android SDK -> SDK Tools ,勾选 Show Package Details ,选择一个合适的NDK版本,点击 Apply等待下载,然后点击 OK
  • 在Android Studio的右上角点击 SDK Manager,选择版本下载。

image.png

  1. 配置项目的NDK

配置NDK也有两种方法,任选其一:

  • 在Android Studio 中点击Project Structure -> SDK Location -> Android NDK location 选择对应目录下的ndk。然后点击Apply -> OK

image.png

  • 点击Project下的 local.properties,手动添加ndk位置。

image.png

3.2 配置CMakeLists.txt

需要修改的部分已用注释标记出来。



# For more information about using CMake with Android Studio, read the

# documentation: https://d.android.com/studio/projects/add-native-code.html



# Sets the minimum version of CMake required to build the native library.



cmake_minimum_required(VERSION 3.10.2)



# Declares and names the project.


// 项目名称
project("jnidemo")



# Creates and names a library, sets it as either STATIC

# or SHARED, and provides the relative paths to its source code.

# You can define multiple libraries, and CMake builds them for you.

# Gradle automatically packages shared libraries with your APK.



add_library( # Sets the name of the library.
             // 库名称
             hello_jni



             # Sets the library as a shared library.

             SHARED



             # Provides a relative path to your source file(s).
             // 实现头文件的.cpp文件
             hello_jni.cpp)



# Searches for a specified prebuilt library and stores the path as a

# variable. Because CMake includes system libraries in the search path by

# default, you only need to specify the name of the public NDK library

# you want to add. CMake verifies that the library exists before

# completing its build.



find_library( # Sets the name of the path variable.

              log-lib



              # Specifies the name of the NDK library that

              # you want CMake to locate.

              log )



# Specifies libraries CMake should link to your target library. You

# can link multiple libraries, such as libraries you define in this

# build script, prebuilt third-party libraries, or system libraries.



target_link_libraries( # Specifies the target library.
                       // jni名称
                       hello_jni



                       # Links the target library to the log library

                       # included in the NDK.

                       ${log-lib} )

4. 实现函数

新建.cpp文件,实现头文件中定义的方法

如:新建hello_jni.cpp,实现hello_jni.h中定义的方法。

#include "hello_jni.h"

#include <string>



extern "C" JNIEXPORT jstring JNICALL Java_com_example_jnidemo_JniExample_sayHello

(JNIEnv *env, jclass)

{

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

}

5. 在Java层调用

以上打通了Java层到Native之间的隔阂,现在只需要在需要的地方调用Native层的函数就可以了。

例如:

package com.example.jnidemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        JniExample.javaSayHello();
        Log.d(TAG, "onCreate: "+JniExample.sayHello());
    }

}

项目最终结构如下所示:

image.png

run一下

image.png

从日志中可以看到完成了从Java到Native的调用。

JNI原理

在上面的hello_jni的例子中,函数的调用逻辑是这样的

image.png

让人好奇的是,JniExample.java中声明的函数是sayHello(),JNI层定义和实现的函数是: Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),这明显和Java层定义的函数不完全一样。从项目最终结构可以看到JniExample.java的位置在java.com``.example.jnidemo目录下。这似乎和JNI层实现的函数类似。仔细看看不难发现,JNI层定义和实现的Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),就是JniExample.sayHello()前面加上JniExample.java所在的位置,并且把所有的 " . " 换成了 “ _ ”。

image.png

在Java层调用sayHello()的时候,会从对应的JNI库中寻找Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass),如果找不到,则会报错;

例如:

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader:xxxxx couldn't find "libhello_jni.so);

如果找的到,则会为sayHello()Java_com_example_jnidemo_JniExample_sayHello(JNIEnv *env, jclass)建立联系。其实就是保存JNI层函数的指针。以后再调用sayHello()的时候,虚拟机可以直接使用这个指针。

静态注册

通过函数名建立起Java层函数和Native层函数之间的联系,JNI层函数遵守特定的格式,这种方法就是静态注册。上述详细描述JNI用法的示例,就是静态注册。可以看到静态注册有以下特点:

  • 需要先编译声明Native函数的Java类,生成二进制文件xxx.class,然后通过javah反编译,生成一个.h头文件。如果有多个声明了Native函数的Java类,都需要反编译生成对应的.h头文件。
  • 经过反编译生成的头文件中定义的函数有固定的命名方式,一般使用这个固定命名方式的函数名都会很长(因为包含包名)。
  • 第一次调用/建立联系的时候,需要通过函数名来检索字符串,最终建立联系。而函数名通常很长,String类占用内存多,检索速度也比较慢,因此第一次调用的时候比较很低。

既然Java和Native函数之间的联系是通过JNI指针建立的,那么如果能够让Native层的函数知道JNI层对应函数的函数指针,似乎就可以避免效率低等问题了。猜想一下,如果有这样一个结构体,应该记录以下信息:

  • Java层函数名
  • JNI层函数指针

动态注册

动态注册没使用过,只能使用AOSP中的代码来看看了。

JNI层的函数指针由一个结构体来记录:

libnativehelper/include_jni/jni.h

jni.h

typedef struct {
    // java层函数名,例如JniExample.java中定义的sayHello()
    const char* name;
    // signature?签名?
    const char* signature;
    // JNI层对应函数的函数指针
    void*       fnPtr;
} JNINativeMethod;

结构体JNINativeMethod中主要记录了三个信息:

  • Java层定义的函数名
  • 签名
  • JNI层对应的函数指针

从MediaScanner的进程看看这个结构体的使用方法

frameworks/base/media/jni/android_media_MediaScanner.cpp

android_media_MediaScanner.cpp


static const JNINativeMethod gMethods[] = {
    {
        // Java层函数名
        "processDirectory",
        // 签名
        "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        // JNI层函数指针
        (void *)android_media_MediaScanner_processDirectory
    },

    {
        "processFile",
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)Z",
        (void *)android_media_MediaScanner_processFile
    },

    {
        "setLocale",
        "(Ljava/lang/String;)V",
        (void *)android_media_MediaScanner_setLocale
    },

    {
        "extractAlbumArt",
        "(Ljava/io/FileDescriptor;)[B",
        (void *)android_media_MediaScanner_extractAlbumArt
    },

    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },

    {
        "native_setup",
        "()V",
        (void *)android_media_MediaScanner_native_setup
    },

    {
        "native_finalize",
        "()V",
        (void *)android_media_MediaScanner_native_finalize
    },

};


int register_android_media_MediaScanner(JNIEnv *env)
{
    // 调用AndroidRuntime. registerNativeMethods()完成注册。
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

gMethods[]数组中记录的结构体就是JNINativeMethod。然后调用register_android_media_MediaScanner``()完成动态注册。

接下来看看AndroidRuntime::registerNativeMethods``()

AndroidRuntime.cpp

/*

 * Register native methods using JNI.

 *//*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

调用jniRegisterNativeMethods``(),传入参数为刚才构建的JNINativeMethod数据等信息。

接下来看看jniRegisterNativeMethods``()

libnativehelper/JNIHelp.c

JNIHelp.c

int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* methods, int numMethods)
{
    ALOGV("Registering %s's %d native methods...", className, numMethods);
    // find calss 
    jclass clazz = (*env)->FindClass(env, className);
    ALOG_ALWAYS_FATAL_IF(clazz == NULL,
                         "Native registration unable to find class '%s'; aborting...",
                         className);
    // 调用JNIEnv的RegisterNatives()完成注册
    int result = (*env)->RegisterNatives(env, clazz, methods, numMethods);
    (*env)->DeleteLocalRef(env, clazz);
    if (result == 0) {
        return 0;
    }
    // Failure to register natives is fatal. Try to report the corresponding exception,
    // otherwise abort with generic failure message.
    jthrowable thrown = (*env)->ExceptionOccurred(env);
    if (thrown != NULL) {
        struct ExpandableString summary;
        ExpandableStringInitialize(&summary);
        if (GetExceptionSummary(env, thrown, &summary)) {
            ALOGF("%s", summary.data);
        }
        ExpandableStringRelease(&summary);
        (*env)->DeleteLocalRef(env, thrown);
    }
    ALOGF("RegisterNatives failed for '%s'; aborting...", className);
    return result;
}

也就是说最终是在JNIEnv中依次使用FindClass()registerNatives()完成的注册。

动态函数的注册方式也就明了了:

   jclass clazz = (*env)->FindClass(env, className);
   (*env)->RegisterNatives(env, clazz, methods, numMethods);
   

所以,在自己的JNI层代码中,只需要使用以上两个方法就可以完成动态注册了。

在Java层通过System.loadLibrary()加载完JNI库之后,会查找该库的JNI_OnLoad函数。如果有,经通过JNI_OnLoad()函数完成动态注册。

例如,在android_media_MediaPlayer.cpp中,实现了JNI_OnLoad()

frameworks/base/media/jni/android_media_MediaPlayer.cpp

android_media_MediaPlayer.cpp

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;
    }
    assert(env != NULL);

    if (register_android_media_ImageWriter(env) != JNI_OK) {
        ALOGE("ERROR: ImageWriter native registration failed");
        goto bail;
    }

    if (register_android_media_ImageReader(env) < 0) {
        ALOGE("ERROR: ImageReader native registration failed");
        goto bail;
    }

    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaRecorder(env) < 0) {
        ALOGE("ERROR: MediaRecorder native registration failed\n");
        goto bail;
    }

    // 注册mediascanner
    if (register_android_media_MediaScanner(env) < 0) {
        ALOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaMetadataRetriever(env) < 0) {
        ALOGE("ERROR: MediaMetadataRetriever native registration failed\n");
        goto bail;
    }

    if (register_android_media_ResampleInputStream(env) < 0) {
        ALOGE("ERROR: ResampleInputStream native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaProfiles(env) < 0) {
        ALOGE("ERROR: MediaProfiles native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpDatabase(env) < 0) {
        ALOGE("ERROR: MtpDatabase native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpDevice(env) < 0) {
        ALOGE("ERROR: MtpDevice native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpServer(env) < 0) {
        ALOGE("ERROR: MtpServer native registration failed");
        goto bail;
    }

    if (register_android_media_MediaCodec(env) < 0) {
        ALOGE("ERROR: MediaCodec native registration failed");
        goto bail;
    }

    if (register_android_media_MediaSync(env) < 0) {
        ALOGE("ERROR: MediaSync native registration failed");
        goto bail;
    }

    if (register_android_media_MediaExtractor(env) < 0) {
        ALOGE("ERROR: MediaCodec native registration failed");
        goto bail;
    }
    
    if (register_android_media_MediaMuxer(env) < 0) {
        ALOGE("ERROR: MediaMuxer native registration failed");
        goto bail;
    }

    if (register_android_media_MediaCodecList(env) < 0) {
        ALOGE("ERROR: MediaCodec native registration failed");
        goto bail;
    }

    if (register_android_media_Crypto(env) < 0) {
        ALOGE("ERROR: MediaCodec native registration failed");
        goto bail;
    }

    if (register_android_media_Drm(env) < 0) {
        ALOGE("ERROR: MediaDrm native registration failed");
        goto bail;
    }

    if (register_android_media_Descrambler(env) < 0) {
        ALOGE("ERROR: MediaDescrambler native registration failed");
        goto bail;
    }

    if (register_android_media_MediaHTTPConnection(env) < 0) {
        ALOGE("ERROR: MediaHTTPConnection native registration failed");
        goto bail;
    }

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

所以,通过动态注册还需要实现JNI_OnLoad()函数。

综上所述,使用动态注册JNI需要以下三个函数:

JNI_OnLoad()

jclass clazz = (*env)->FindClass(env, className);*

*(* env)->RegisterNatives(env, clazz, methods, numMethods);