Android JNI

JNI是 Java Native Interface 的缩写,顾名思义,翻译为 Java 本地接口,是 Java 与 C++/C 语言通信的桥梁。当 Java 语言无法胜任时,便通过 JNI 技术,调用 C++/C 语言来处理。

大致有三种情况需要使用 JNI 技术,第一种:需要调用 UNIX 系统的某个功能,而这个功能并非 Java 语言完成的;第二种:需要使用早期用 C++/C 语言开发的一些功能;第三种:游戏、音视频开发涉及的音视频编解码和图像绘制需要更快的处理速度。

Android 系统按语言来划分的话,分为 Java 层和 Native 层。 Java 通过 JNI 技术可以访问 Native 层级的代码, Native 层级的代码自然也可以通过 JNI 技术访问 Java层级的代码。

我们学习 JNI 技术的直接目的就是为了深入 Android Native 层,理解 Native 层级的代码。

Java 访问 Native

Java 要访问 Native 的前提是把 Java 层的 Native 方法注册到 Native 层,而注册主要有两种方式,一种是静态注册,一种是动态注册。

静态注册

通过 Java 的 native 方法生成 Jni 方法有两种方式,分别是自动生成和手动生成。

  • 自动生成
    静态注册在 AndroidStudio 中可以实现 native 方法的自动生成,只需要创建 native project 或者引入 native library ,利用 cmake 进行编译即可。
  • 手动生成
    自动生成的方式并不会将生成的 native 方法存放到一个 .h 文件,只会存放到 .cpp 文件中。项目中往往需要 .h 文件的存在,这个时候选择手动生成。手动生成通过 javac 和 javah 方法。以下面的内容为示例:
public class HelloWorld {
    static {
        System.loadLibrary("hello_world");
    }

    public native void helloWorld();

    public static void main(String[] args) {
        new HelloWorld().helloWorld();
    }
}

因为是当前路径下执行,所以源代码不添加package。

javac HelloWorld.java
javah HelloWorld

分别会生成两个文件,分别是 class 文件和 .h 文件,只看 .h 文件,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include  // 注释1
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" { // 2
#endif
/*
 * Class:     HelloWorld
 * Method:    helloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_helloWorld
  (JNIEnv *, jobject); // 3

#ifdef __cplusplus
}
#endif
#endif
  1. 注释 1 处的 jni.h 位于 /usr/lib/jvm/java-8-openjdk-amd64/include 路径下。(Ubuntu环境)
  2. 注释 2 处的 extern "C" 的主要作用就是为了能够正确实现 C++ 代码调用其他 C 语言代码。加上 extern "C" 后,会指示编译器这部分代码按 C 语言的进行编译,而不是 C++ 的。
  3. 注释 3 处的方法为 Java 的 native 方法生成。
    JNIEXPORT 和 JNICALL 为两个宏,用于设置函数可见性,以及调用栈约定,这里可以忽略这两个宏,void 为方法的返回值。
    方法以Java_开头,接着是包名和类名,以_替换.,最后是方法名。
    Java 中的 native 方法并没有参数,在 Jni 方法中会增加两个参数:JNIEnv *指针类型和jobject类型的参数。
    JNIEnv 是 Natvie 世界中 Java 环境的代表,通过 JNIEnv * 指针就可以在 Native 世界中访问 Java 世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递。jobject 同样也是 JNI 的数据类型,对应于 Java 的 Object,指向 this 的指针,用于获取类相关的信息(变量、方法等)。

以上只是生成了 .h 文件,还需要生成 .c 文件。

#include "HelloWorld.h"
#include 
#include 

JNIEXPORT void JNICALL Java_HelloWorld_helloWorld (JNIEnv *env, jobject obj) {
  printf("HelloWorld JNI!\n");
}

编译共享文件库:

g++ -shared -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux HelloWorld.c -o libhello_world.so

运行 java 文件:

$java -Djava.library.path=. HelloWorld
HelloWorld JNI!

动态注册

以 framework services 模块 services/java/com/android/server/SystemServer.java 中的 native 方法 startSensorService 调用流程举例,讲解如何进行动态注册。

    /**
     * Start the sensor service. This is a blocking call and can take time.
     */
    private static native void startSensorService();
  1. 加载 libandroid_servers.so,然后调用 JNI_OnLoad 方法。
    当执行 services/java/com/android/server/SystemServer.java 中的 System.loadLibrary("android_servers"); 时,会调用 JNI_OnLoad 方法,这个方法位于 services/core/jni/onload.cpp 中。
extern "C" 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("GetEnv failed!");
        return result;
    }
    ALOG_ASSERT(env, "Could not retrieve the env!");

    ...
    register_android_server_SystemServer(env);
    ...
    return JNI_VERSION_1_4;
}
  1. 调用 register_android_server_SystemServer(env);
    这个方法位于 services/core/jni/com_android_server_SystemServer.cpp 中。
int register_android_server_SystemServer(JNIEnv* env)
{
    return jniRegisterNativeMethods(env, "com/android/server/SystemServer",
            gMethods, NELEM(gMethods));
}
  1. 调用 jniRegisterNativeMethods
    jniRegisterNativeMethods 位于 libnativehelper/JNIHelp.cpp 中。
inline int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
    return jniRegisterNativeMethods(&env->functions, className, gMethods, numMethods);
}
MODULE_API int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);

    scoped_local_ref c(env, findClass(env, className));
    ALOG_ALWAYS_FATAL_IF(c.get() == NULL,
                         "Native registration unable to find class '%s'; aborting...",
                         className);

    int result = e->RegisterNatives(c.get(), gMethods, numMethods);
    ALOG_ALWAYS_FATAL_IF(result < 0, "RegisterNatives failed for '%s'; aborting...",
                         className);

    return 0;
}

这里会调用 RegisterNatives 方法完成注册。流程介绍完了,我们重点看 jniRegisterNativeMethods 方法,它的参数分别是类名,方法数组,方法个数。重点看方法数组 gMethods 。

  1. gMethods: Java native 方法 与 Jni 方法映射表
/*
 * JNI registration.
 */
static const JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "startSensorService", "()V", (void*) android_server_SystemServer_startSensorService },
    ...
};

SystemServer.java 中的 startSensorService 映射到了 services/core/jni/com_android_server_SystemServer.cpp 中的 android_server_SystemServer_startSensorService 方法上。这样每次在 SystemServer.java 中添加一个 native 方法,就只需在 gMethods 里面映射一个 Jni 方法即可。

  1. JNINativeMethod 类型
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

gMethods 数组的每个元素的类型是 JNINativeMethod 。它是一个结构体,由三个元素组成,分别是 Java native 方法名,Java native 方法的签名信息,Jni 方法的指针。

总结:

  • 如果在 framework 或者 services 模块的某个类中添加 native 方法,只需要在相应的 gMethods 数组中建立与 Jni 方法的映射关系即可。
  • 如果在新文件中添加native方法,那就在 JNI_OnLoad 方法中,将相应的 cpp 类进行注册即可。

数据类型

Java 的数据类型分为基本数据类型和引用数据类型,JNI 层同样分为了这两种类型。Java 的数据类型到了 JNI 层需要转换为 JNI 层的数据类型。

基本数据类型的转换

基本数据类型的转换

引用数据类型的转换

引用数据类型的转换

数组的 JNI 层数据需要以 Array 结尾,签名格式的开头都会有 [ 。需要注image.png意有些数据类型的签名以 “;” 结尾,引用数据类型还具有继承关系,如下图所示:


引用数据类型的继承关系

改写之前的 HelloWorld.java ,给 helloWorld 方法添加参数,Java 数据类型分别是 Object 和 String 。

public native void helloWorld(Object object,String str);
javac HelloWorld.java
javah HelloWorld

生成新的 JNI 方法

/*
 * Class:     HelloWorld
 * Method:    helloWorld
 * Signature: (Ljava/lang/Object;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloWorld_helloWorld
  (JNIEnv *, jobject, jobject, jstring);

对比发现,Object 数据类型转换为了 jobject,String 数据类型转换为了 jstring。

方法签名

由于 Java 是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在 JNI 中仅仅通过方法名是无法定位 Java 中对应的具体方法的,JNI 为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的 Java 方法。

static const JNINativeMethod gBinderProxyMethods[] = {
     /* name, signature, funcPtr */
    {"pingBinder",          "()Z", (void*)android_os_BinderProxy_pingBinder},
    {"isBinderAlive",       "()Z", (void*)android_os_BinderProxy_isBinderAlive},
    {"getInterfaceDescriptor", "()Ljava/lang/String;", (void*)android_os_BinderProxy_getInterfaceDescriptor},
    {"transactNative",      "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z", (void*)android_os_BinderProxy_transact},
    {"linkToDeath",         "(Landroid/os/IBinder$DeathRecipient;I)V", (void*)android_os_BinderProxy_linkToDeath},
    {"unlinkToDeath",       "(Landroid/os/IBinder$DeathRecipient;I)Z", (void*)android_os_BinderProxy_unlinkToDeath},
    {"getNativeFinalizer",  "()J", (void*)android_os_BinderProxy_getNativeFinalizer},
};
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

上文有讲,JNINativeMethod 类型的第二个参数即为方法签名,如 gBinderProxyMethods 数组所示,方法签名的格式为:

(参数签名格式...) 返回值签名格式

以 Java transactNative 方法举例:

    public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;
{"transactNative",      "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z", (void*)android_os_BinderProxy_transact},

Native 访问 Java

JNIEnv 解析

Natvie 访问 Java 通过 JNIEnv , JNIEnv 是 Native 世界中 Java 环境的代表,通过 JNIEnv * 指针就可以在 Native 世界中访问 Java 世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,因此不同线程的 JNIEnv 是彼此独立的。
JNIEnv 的主要动作有两点:

  1. 调用 Java 的方法。
  2. 操作 Java (操作 Java 中的变量和对象)
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

JNIEnv 定义在 libnativehelper/include_jni/jni.h 文件中。
使用 宏 __cplusplus 来区分 C 和 C++ 两种代码。在 C 中,JNIEnv 的类型是 JNINativeInterface* ,在 C++ 中的类型是 _JNIEnv 。我们这里重点看 C++ 中的类型,也就是 _JNIEnv 。它也在 libnativehelper/include_jni/jni.h 中定义。

/*
486 * C++ object wrapper.
487 *
488 * This is usually overlaid on a C struct whose first element is a
489 * JNINativeInterface*.  We rely somewhat on compiler behavior.
490 */
491struct _JNIEnv {
492    /* do not rename this; it does not seem to be entirely opaque */
493    const struct JNINativeInterface* functions;
494
495#if defined(__cplusplus)
...
504    jclass FindClass(const char* name)
505    { return functions->FindClass(this, name); }
...
591    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
592    { return functions->GetMethodID(this, clazz, name, sig); }
...
694    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
695    { return functions->GetFieldID(this, clazz, name, sig); }
...
1032};

_JNIEnv 如上所示是一个结构体,其内部有一个 JNINativeInterface* 类型的静态常量 functions 。可见 C++ 的 JNIEnv 是对 C 的 JNIEnv 的一次封装。通过 functions 可以调用很多函数。这里说下常用的三个函数:

  1. FindClass: 用来找到 Java 中指定的名称的类。
  2. GetMethodID: 用来得到 Java 中的方法。
  3. GetFieldID: 用来得到 Java 中的成员变量

以上三个函数都调用 functions ,它是 JNINativeInterface* 类型的变量,来看下 JNINativeInterface* 类型:

struct JNINativeInterface {

    jclass      (*FindClass)(JNIEnv*, const char*);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
    ...
}

JNINativeInterface 结构中定义了很多和 JNIEnv* 相关联的函数指针,至于函数指针是何时被赋值的,我们就不再关注了,感兴趣的可以查询下相关资料。

示例

#include "HelloWorld.h"
#include 
#include 

JNIEXPORT void JNICALL Java_HelloWorld_helloWorld (JNIEnv* env, jobject obj,jobject obj1,jstring jstr) {
  jclass clazz;
  clazz = env->FindClass("HelloWorld"); //1. 获取类
  //clazz = env->GetObjectClass(obj); //2. 获取类 obj代表调用当前jni方法的java对象
  if (clazz == NULL) {
    return;
  }
  jmethodID methodID = env->GetMethodID(clazz,"helloWorldFromJava","(Ljava/lang/String;)V");
  if (methodID == NULL) {
    return;
  }
  jfieldID fieldID = env->GetFieldID(clazz,"mField","Ljava/lang/String;"); //获取变量
  jstring fieldName = static_cast(env->GetObjectField(obj,fieldID)); //获取变量的值
  env->CallVoidMethod(obj, methodID,fieldName); //把成员变量的值作为参数传递给方法
}

public class HelloWorld {

    private String mField = "HelloWorld";

    static {
        System.loadLibrary("hello_world");
    }

    public native void helloWorld(Object object,String str);

    public static void main(String[] args) {
        new HelloWorld().helloWorld("HelloWorldObj","HelloWorldStr");
    }
    
    private void helloWorldFromJava(String str) {
        System.out.println(str);;
    }

}
javac HelloWorld.java
javah HelloWorld
g++ -shared -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux HelloWorld.c -o libhello_world.so
java -Djava.library.path=. HelloWorld
  1. Jni 中通过 FindClass 获取 Java HelloWorld 类对象
  2. Jni 中通过 GetMethodID 获取 Java HelloWorld 类方法 helloWorldFromJava
  3. Jni 中通过 GetFieldID GetObjectField 获取 Java HelloWorld 类变量 mField 的值
  4. Jni 中通过 CallVoidMethod 调用 helloWorldFromJava 方法,并传入 mField 的值

本地引用

JNIEnv 提供的函数所返回的引用基本上都是本地引用,上一小节示例代码中的 clazz 和 methodID、fieldID 都属于本地引用。本地引用有如下特点:

  1. 当 Native 函数返回时,这个本地引用就会被自动释放。
  2. 只在创建它的线程中有效,不能跨线程使用。
  3. 局部应用是 JVM 负责的引用类型,受 JVM 管理。
    可以使用 DeleteLocalRef 等函数来手动删除本地引用,这些函数的使用场景主要是在 Native 函数返回前占用了大量内存,需要立即删除本地引用。
  env->DeleteLocalRef(clazz);
  env->DeleteLocalRef(fieldName);

其他的不作演示了

jclass jcls;//需要DeleteLocalRef
jobject jcls;//需要DeleteLocalRef
jstring jcls;//需要ReleaseStringUTFChars DeleteLocalRef
jarray jcls;//需要DeleteLocalRef
jmethodID jfieldID//不需要DeleteLocalRef

全局引用

全局引用和本地引用几乎是相反的。它主要的特点有:

  1. 在 native 函数返回时不会被自动释放,需要手动进行释放,并且不会被 GC 回收。
  2. 全局引用是可以跨线程使用的。
  3. 全局引用不受到 JVM 管理。

JNIEnv 的 NewGlobalRef 函数用来创建全局引用,用 DeleteGlobalRef 函数删除全局引用。

  mGlobalClazz = static_cast(env->NewGlobalRef(clazz)) ; //创建全局引用
  if (mGlobalClazz == NULL) {
    return;
  }
  env->DeleteGlobalRef(mGlobalClazz); // 释放全局引用

弱全局引用

弱全局引用是一种特殊的全局引用,它可以被 GC 回收,弱全局引用被 GC 回收后会指向NULL。JNIEnv 的 NewWeakGlobalRef 用来创建一个弱全局引用,用 DeleteWeakGlobalRef 来释放全局引用。

mClass = (jclass)env->NewWeakGlobalRef(clazz);
env->DeleteWeakGlobalRef(mClass);

由于可能被 GC 回收,因此在使用它之前先判断它是否被回收了,利用函数 IsSameObject进行判断。

  if (env->IsSameObject(mGlobalClazz, NULL)){
      return;
  }

疑问

在编译全局引用的时候,报错了。

$g++ -shared -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux HelloWorld.c -o libhello_world.so
/usr/bin/ld: /tmp/cctLHX7X.o: relocation R_X86_64_PC32 against symbol `mGlobalClazz' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: 最后的链结失败: bad value
collect2: error: ld returned 1 exit status

当我为全局引用声明为局部变量并赋值之后,再编译就报错了,百度了下,说加上编译选项 -fPIC ,但依然不知道为什么会报错。如果谁明白,可以留言,感谢。

参考

  1. http://liuwangshu.cn/framework/jni/2-signature-jnienv.html
  2. https://furzoom.blog.csdn.net/article/details/113730538
  3. https://blog.csdn.net/weixin_35671110/article/details/117596800

你可能感兴趣的:(Android JNI)