深入理解JNI

  1. JNI概述
  2. 学习JNI实例:MediaScanner
  3. 注册JNI函数
  4. 数据类型转换
  5. JNIEnv介绍

一、JNI概述

JNI全称,JavaNativeInterface——Java提供了Java层与Native层交互的桥梁。通过JNI技术我们可以做到如下:

  1. Java程序中的函数可以调用Native[C/C++]语言编写的函数。
  2. Native层中的函数可以调用Java层的函数,也就是说C/C++函数中可以调用Java函数。

二、JNI学习实例:MediaScanner类

MediaScanner类中的部分函数由Native层实现,JNI层中对应的是libmedia_jni.so,media_jni为JNI库的名字。libmedia.so库完成了实际功能。MediaScanner通过JNI库libmedia_jni.so和Native层的libmedia.so进行交互。

#2.1 Java层的MediaScanner分析
  • MediaScanner
class MediaScanner {
    ......
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    private static native final void native_init();
    ......
}

Media类中的静态代码块执行了两个操作:

  1. 加载media_jni库。
  2. 调用native_init()完成native层初始化操作。
    Java函数中调用native函数,必须通过位于JNI层的动态库来实现。一般采用的做法是在类的static代码块中,通过调用System.loadLibrary(String libraryName)来完成对动态库的加载。
  • 加载JNI库
    由System类的静态成员函数loadLibrary负责加载动态库。
  • System
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

loadLibrary方法中调用了Runtime类的成员函数loadLibrary0();

  • Runtime
synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
    }
    String libraryName = libname;

    //如果 loader不为空进入该分支
    if (loader != null) {
        //查找库所在的路径
        String filename = loader.findLibrary(libraryName);
        if (filename == null) {
            // It's not necessarily true that the ClassLoader used
            // System.mapLibraryName, but the default setup does, and it's
            // misleading to say we didn't find "libMyLibrary.so" when we
            // actually searched for "liblibMyLibrary.so.so".
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                           System.mapLibraryName(libraryName) + "\"");
        }
        //加载库
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    String filename = System.mapLibraryName(libraryName);
    List candidates = new ArrayList();
    String lastError = null;
    for (String directory : getLibPaths()) {
        String candidate = directory + filename;
        candidates.add(candidate);

        if (IoUtils.canOpenReadOnly(candidate)) {
            String error = doLoad(candidate, loader);
            if (error == null) {
                return; // We successfully loaded the library. Job done.
            }
            lastError = error;
        }
    }

    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
    }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

loadLibrary0方法中所完成的操作是,如果ClassLoader不为空,则调用其成员函数findLibrary获取到库所在的路径,然后调用Runtime类的成员函数doLoad对库进行加载,doLoad函数则将具体的加载过程转发给Runtime类中定义的native层函数nativeLoad,进而完成后续加载过程。

  • ClassLoader.findLibrary

获取库所在的本地路径。

/**
 * Returns the absolute path name of a native library.  The VM invokes this
 * method to locate the native libraries that belong to classes loaded with
 * this class loader. If this method returns null, the VM
 * searches the library along the path specified as the
 * "java.library.path" property.
 *
 * @param  libname
 *         The library name
 *
 * @return  The absolute path of the native library
 *
 * @see  System#loadLibrary(String)
 * @see  System#mapLibraryName(String)
 *
 * @since  1.2
 */
protected String findLibrary(String libname) {
    return null;
}
  • Runtime.doLoad

调用native层函数nativeLoad完成对库加载。

private String doLoad(String name, ClassLoader loader) {
    ......
    // internal natives.
    synchronized (this) {
        return nativeLoad(name, loader, librarySearchPath);
    }
}

nativeLoad后续的执行步骤大致为:

  1. 调用dlopen函数,打开一个so文件并创建一个handle;
  2. 调用dlsym()函数,查看相应的so文件的JNI_OnLoad()函数指针,并执行相应函数。
#2.2 JNI层的MediaScanner分析

MediaScanner的JNI层代码在android_media_MediaScanner.cpp中,如下所示:

  • android_media_MediaScanner.cpp
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

Java层的native_init对应JNI层的android_media_MediaScanner_native_init,下面详细分析其绑定过程。

当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_Media_Scanner_native_init函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_Media_Scanner_native_init建立一个关联关系,其实就是保存JNI函数的函数指针。以后调用native_init函数时,直接使用这个函数指针就可以了,这项工作是由虚拟机完成的。


三、注册JNI函数

“注册”是将Java层的native函数和JNI层对应的实现函数关联起来。JNI函数注册的方式有两种:

  • 静态注册
  • 动态注册
#3.1 静态注册

静态注册的大体流程如下:

  • 先编写Java代码,然后编译生成.class文件。
  • 使用Java的工具程序javah,如javah -o output packagename.classname,这样会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
#3.1.1 :编写Java代码
package com.next.hhu.jnidemo;

public class StaticJNITest {
    public static native int add(int a, int b);
}
#3.1.2 编译生成.class文件

执行[1]操作进入到java目录下,然后执行[2]操作生成.class文件。

//[1] 进入到java目录下
cd app/src/main/java
//[2] 生成.class文件
javac com/next/hhu/jnidemo/StaticJNITest.java
#3.1.3 生成头文件

然后利用javah命令生成头文件,这样会在/java目录下生成com_next_hhu_jnidemo_StaticJNITest.h头文件。

javah com.next.hhu.jnidemo.StaticJNITest
#3.1.4 配置CMakeLists.text文件
# 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.4.1)

# 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.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp)

add_library( # Sets the name of the library.
             native-lib1

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/com_next_hhu_jnidemo_StaticJNITest.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.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
#3.2 动态注册

JNI中用JNINativeMethod结构来记录Java的Native方法和JNI方法的关联关系。

  • jni.h
typedef struct {
    const char* name; //Java方法名
    const char* signature; //Java方法的签名信息
    void*       fnPtr; //JNI中对应的函数指针
} JNINativeMethod;

MediaScanner JNI层中采用的是静态注册。

#3.2.1 定义JNINativeMethod[]

定义一个JNINativeMethod[],其成员就是MediaScanner中所有成员函数的一一对应关系。

android_media_MediaScanner.cpp

static JNINativeMethod gMethods[] = {
    {
        "processDirectory",
        "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processDirectory
    },

    {
        "processFile",
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (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
    },
};
#3.2.2 注册JNINativeMetod[]
  • android_media_MediaScanner.cpp
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

调用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);
}

调用JNIHelp.c中的jniRegisterNativeMethods方法来完成注册。

  • JNIHelp.c
int jniRegisterNativeMethods(JNIENV *env,const char *className,
                            const JNINativeMethod *gMethods,int numMethods) 
{
    jclass clazz;
    clazz = (*env)->findClass(env,className);
    ......
    //实际上调用JNIEnv的RegisterNatives函数完成注册的
    if((*env)->RegisterNatives(env,clazz,gMethods,numMethods) < 0) {
        return -1;
    }
    return 0;
}
#3.2.3 JNI_OnLoad函数中调用注册函数

当Java层通过System.loadLibrary加载完成JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,动态注册工作在这里完成。

//该函数的第一个参数类型为JavaVM,代表JNI层的Java虚拟机,每一个Java进程有且仅有一个
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserve) {
    JNIEnv *env = NULL;
    jint result = -1;
    
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        goto bail;
    }
    if (env == NULL) {
        return -1;
    }
    jclass clazz = env->FindClass("com/next/hhu/jnidemo/JNIHelper");
    if (clazz == NULL) {
        return -1;
    }
    ......
    //动态注册MediaScanner的JNI函数
    if(register_android_media_MediaScanner(env) < 0) {
        goto bail;
    }
    .....
    return JNI_VERSION_1_4;//必须返回这个值,否则会报错。
}

四、数据类型转换

#4.1 基本数据类型转换
Signature格式 Java Native
B byte jbyte
C char jchar
D double jdouble
F float jfloat
I int jint
S short jshort
J long jlong
Z boolean jboolean
V void void
#4.2 数组数据类型
Signature格式 Java Native
[B byte[] jbyte
[C char[] jchar
[D double[] jdouble
[F float[] jfloat
[I int[] jint
[S short[] jshort
[J long[] jlong
[Z boolean[] jboolean
#4.3 引用数据类型转换
Signature格式 Java Native
Ljava/lang/String; String jstring
L+classname+; 所有对象 jobject
[L+classname+; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable
#4.4 Signature
Java函数 对应的签名
void foo() ()V
float foo(int i) (I)F
long foo(int[] i) ([I)J
double foo(Class c) (Ljava/lang/Class;)D
boolean foo(int[] i,String s) ([ILjava/lang/String;)Z
String foo(int i) (I)Ljava/lang/String;

五、JNIEnv介绍

JNIEnv是一个与线程相关的代表JNI环境的结构体,JNIEnv提供了一些JNI系统函数。通过这些函数可以做到:

  • 调用JAVA函数。
  • 操作jobject对象等很多事情。

JNIEnv是一个线程相关的变量。由于线程相关,所以不能在一个线程中使用另一个线程的JNIEnv结构体,有一种情况当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv该如何处理?

//全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);

JavaVM和JNIEnv之间的关系:

  • 调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数。
  • 另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread的函数来释放对应的资源。
/*
 * C++ object wrapper.
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
struct _JNIEnv {
   /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)
......
#endif /*__cplusplus*/
};
#5.1 通过JNIEnv操作jobject

Java的引用类型除了少数几个外,最终在JNI层都会用jobject来表示对象的数据类型,操作jobject步骤:

#5.1.1 jfieldID和jmethodID介绍
    jfieldID    (*GetFieldID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass clazz, const char *name, const char *sig);

其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。

public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
            mScanFileMethodID(0),
            mHandleStringTagMethodID(0),
            mSetMimeTypeMethodID(0)
    {
        ALOGV("MyMediaScannerClient constructor");
        jclass mediaScannerClientInterface =
                env->FindClass(kClassMediaScannerClient);

        if (mediaScannerClientInterface == NULL) {
            ALOGE("Class %s not found", kClassMediaScannerClient);
        } else {
            mScanFileMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "scanFile",
                                    "(Ljava/lang/String;JJZZ)V");

            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");

            mSetMimeTypeMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "setMimeType",
                                    "(Ljava/lang/String;)V");
        }
    }

将scanFile和handleStringTag函数的jmethodId保存为MyMediaScannerClient的成员变量,供后续使用。

#5.1.2 使用jfieldID和jmethodID
    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
            path, lastModified, fileSize, isDirectory);

        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
        
        //调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:
        //第一个参数代表MediaScannerClient的jobject对象,
        //第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);

        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }

通过JNIEnv输出CallVoidMethod,再把jobject、jmethodID和对应的参数传进去,JNI层就能够调用Java对象的函数了。

#5.2 jstring介绍

Java中String为引用类型,JNI规范中单独创建一个jstring类型来表示Java中的String类型。

  • 调用JNIEnv的NewString(JNIEnv *env,const jchar *unicodeChars,jsize len),可以从Native字符串得到一个jstring对象。可以把jstring对象看作是Java中String对象在JNI层的代表,也就是说jstring是一个Java String。由于Java String中存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
  • 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。
  • JNIEnv中GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。
  • 如果在代码中调用了上面几个函数,在做完相关工作后,就需要调用ReleaseStringChars或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄漏。
static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    ALOGV("processFile");

    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }

    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }

    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}
#5.3 JNI类型签名介绍

Java中支持函数重载,也就是说,可以定义同名但不同参数的函数。因此,仅仅靠函数名没办法找到具体的函数。JNI技术中将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能找到Java中的函数了。
Java中提供了javap的工具来帮助生成函数或变量的签名信息。

//XXX是编译后的.class文件
javap -s -p XXX
C:\Users\hhu\Desktop\11\JNIDemo\app\src\main\java>javap -s -p com.next.hhu.jnidemo.StaticJNITest
Compiled from "StaticJNITest.java"
public class com.next.hhu.jnidemo.StaticJNITest {
  public com.next.hhu.jnidemo.StaticJNITest();
    descriptor: ()V

  public static native int add(int, int);
    descriptor: (II)I

  static {};
    descriptor: ()V
}
#5.3 垃圾回收

JNI提供了三种引用类型:

  • Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大特点是,一旦JNI层函数返回,这些object就可能被垃圾回收。
  • Global Reference:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
  • Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收,因此在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
    
    {
       ......
    }

    virtual ~MyMediaScannerClient()
    {
        ALOGV("MyMediaScannerClient destructor");
        mEnv->DeleteGlobalRef(mClient);
    }
#5.4 JNI中的异常处理

如果调用JNIEnv的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作。所以安全编码显得十分重要。

你可能感兴趣的:(深入理解JNI)