JNI原理基础知识

352 阅读6分钟

1. Native方法注册

native方法注册分为静态注册和动态注册,静态注册多用于NDK开发,动态注册多用于framework开发

1.1 静态注册

通过AS写一个简单的JniTest.java文件

public class JniTest {

    static {
        System.loadLibrary("jni-test");
    }

    public static void main(String[] args) {
        JniTest jniTest = new JniTest();
        System.out.println("JNI测试 " + jniTest.get());
        jniTest.set("测试语句");
    }

    public native String get();

    public native void set(String str);
}

进入项目的通过命名行

  • 编译class文件 javac /Users/tangpeng/Downloads/android/Troll/app/src/main/java/com/troll/troll/jni/JniTest.java 生成JiniTest.class文件
  • 通过javah命令导出JNI的头文件 通过终端进入tangpeng@tangpengdeMacBook-Pro java java这个文件夹下 然后javah com.troll.troll.jni.JniTest 生成com_troll_troll_jni_JniTest.h文件
.....
/*
 * Class:     com_troll_troll_jni_JniTest
 * Method:    get
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_troll_troll_jni_JniTest_get
  (JNIEnv *, jobject);

/*
 * Class:     com_troll_troll_jni_JniTest
 * Method:    set
 * Signature: (Ljava/lang/String;)V
 */

JNIEXPORT void JNICALL Java_com_troll_troll_jni_JniTest_set
  (JNIEnv *, jobject, jstring);
.....

get()方法被声明为Java_com_troll_troll_jni_JniTest_get(),其中

  • Java开头,说明是在Java平台中国调用JNI方法
  • com_troll_troll_jni_JniTest_get 指:包名+类名+方法名格式
  • JNIEnv是Native世界中Java环境的代表,通过JNIEnv* 指针可以在Native世界中访问Java世界的代码进行操作,只在创建他的线程中有效,不能跨线程传递
  • jclass、jobject、jstring是JNI的数据类型,对应Java的java.lang.Class实例、Object、String

静态注册:就是根据方法名,将Java方法和JNI函数建立关联,其实是保存JNI的函数方法指针,再猜调用get()方法直接使用这个函数指针 缺点:

  • JNI层的函数名称过长
  • 声明Native方法的类需要用javah生成头文件
  • 初次调用Native方法时需要建立关联,影响效率

动态注册

JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系JNINativeMethod,它是在jni.h中被定义:


typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

例子: 系统的MediaRecorder采用的就是动态注册


static const JNINativeMethod gMethods[] = {
    {"setCamera",            "(Landroid/hardware/Camera;)V",    (void *)android_media_MediaRecorder_setCamera},
    {"setVideoSource",       "(I)V",                            (void *)android_media_MediaRecorder_setVideoSource},
    {"setAudioSource",       "(I)V",                            (void *)android_media_MediaRecorder_setAudioSource},
    {"setOutputFormat",      "(I)V",                            (void *)android_media_MediaRecorder_setOutputFormat},
    {"setVideoEncoder",      "(I)V",                            (void *)android_media_MediaRecorder_setVideoEncoder},
    {"setAudioEncoder",      "(I)V",                            (void *)android_media_MediaRecorder_setAudioEncoder},
    {"setParameter",         "(Ljava/lang/String;)V",           (void *)android_media_MediaRecorder_setParameter},
   ......

上面定义了一个JNINativeMethod类型的getMethod数组,里面存储是MediaRecorder的Native方法与JNI层函数的对应关系,

  • setCamera 是Java层的Native方法,对应的JNI层的函数为android_media_MediaRecorder_setCamera
  • (I)V是start方法的签名信息

只定义了JNINativeMethod类型的数组是没有用的,还需要注册它,注册函数为register_android_media_MediaRecorder(),调用的地方

frameworks base media jni android_media_MediaPlayer.cpp

....

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

在JNI_OnLoad函数中调用了整个多媒体框架注册JNINativeMethod数组的函数,上面就是注册的函数

frameworks base media jni android_media_MediaRecorder.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaRecorder", gMethods, NELEM(gMethods));
}

在register_android_media_MediaRecorder方法中返回了AndroidRuntime的registerNativeMethods函数,

frameworks base core jni 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);
}
libnativehelper JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
.....
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    return 0;
}

最终通过调用JNIEnv的RegisterNatives函数来完成JNI的注册。

2. 数据类型的转换

基本数据类型转换 bf8307e1ac2c25fce69638dba7e5c98c.png

引用数据类型的转换 b312979384cbb6d36da44da7013e2ef7.png

3. 方法签名(Signature)

方法签名是有签名格式组成的

 static const JNINativeMethod gMethods[] = {
 {"setParameter",         "(Ljava/lang/String;)V",           (void *)android_media_MediaRecorder_setParameter}
 }

getMethods数组中存储了MediaRecorder的Native方法与JNI层函数的对应关系,其中"(I)V"和”(Ljava/lang/String;V)“都是方法签名 作用:

Java有重载方法,可以定义方法名相同,但是参数不同的方法。JNI中仅仅通过方法名是无法找到Java中对应的具体方法的,JNI将参数类型和返回值类型组合在一起作为方法签名 JNI方法签名格式: (参数签名格式...)返回值签名格式

4. 解析JNIEnv

JNIEnv是Native世界中Java环境的代表,通过JNIEnv* 指针可以在Native世界中方法Java世界的代码,它只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv是彼此独立的

作用:

  • 调用Java的方法
  • 操作Java(操作Java中的变量和对象
libnativehelper/include/nativehelper/jni.h

JNIEnv的定义:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

使用预定义宏——cplusplus来区分C和C++,如果定义了_cplusplus,就是C++代码中的定义; JavaVM,它是虚拟机在JNI层的代表,在虚拟机进程中只有一个JavaVM,该进程的所有线程都可以使用这个javaVM。通过JavaVM的AttachCurrentThread函数可以获取到这个线程的JNIEnv,这样就可以在不同线程中调用Java方法。使用AttachCurrentThread函数的线程退出前,要调用DetachCurrentThread函数来释放资源

从C中JNIEnv的类型是JNINativeInterface* ,C++中JNIEnv的类型是_JNIEnv,查看_JNInv,是如何被定义的:

libnativehelper/include/nativehelper/jni.h
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;


#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); 
    
    ......
    
    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }

    jmethodID FromReflectedMethod(jobject method)
    { return functions->FromReflectedMethod(this, method); }

    jfieldID FromReflectedField(jobject field)
    { return functions->FromReflectedField(this, field); }

    jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic)
    { return functions->ToReflectedMethod(this, cls, methodID, isStatic); }

    jclass GetSuperclass(jclass clazz)
    { return functions->GetSuperclass(this, clazz); }

....
    jclass GetObjectClass(jobject obj)
    { return functions->GetObjectClass(this, obj); }

    jboolean IsInstanceOf(jobject obj, jclass clazz)
    { return functions->IsInstanceOf(this, obj, clazz); }

    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }

_JNIEnv是一个结构体,内部包含了JNINativeInterface。同时在_JNIEnv中定义了很多的函数,这些都是用来获取Java中的一些数据

不管_JNIEnv都是跟JNINativeInterface有关的

libnativehelper/include/nativehelper/jni.h
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);

    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
                        jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);

...

上述结构定义很多和JNIEnv结构体对应的函数指针,通过函数指针的定义,能够定义位到虚拟机中的JNI寒暑表,从而时间JNI层在虚拟机中的函数调用,这样JNI层就可以调用Java世界的方法

5. 引用类型

JNI的引用类型,分别分为本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Glabal References)

5.1 本地引用

JNIEnv提供的函数所返回的引用基本上都是本地引用,本地引用的特点:

  • 当Native函数返回时,这个本地引用就会被自动释放
  • 在创建他的线程有效,不能够跨进程使用
  • 局部引用是JVM负责的引用类型,受JVM管理
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;

    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
   ...
   }   

这个clazz就是本地引用,他会在android_media_MediaRecorder_native_init函数调用返回后被自动释放。 当然也可以使用JNIEnv的DeleteLocalRef函数来手动删除本地引用

5.2 全局引用

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

JNIEnv的NewGloblaRef函数用来创建全局引用,调用JNIEnv的DeleteGlobalRef函数来释放全局引用


JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{

    jclass clazz = env->GetObjectClass(thiz);
// 创建全局引用
    mClass = (jclass)env->NewGlobalRef(clazz);
    mObject  = env->NewGlobalRef(weak_thiz);
}

JNIMediaRecorderListener::~JNIMediaRecorderListener()
{
    // 释放全局引用
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->DeleteGlobalRef(mObject);
    env->DeleteGlobalRef(mClass);
}

5.2 弱引用

弱全局引用是特殊的全局引用,不同的是弱全局是可以被GC回收的,弱全局引用被GC回收之后会指向NULL。JNIEnv的NewWeakGlobalRef函数用来创建弱全局引用,调用JNIEnv的DeleteWeakGlobalRef函数来释放弱全局引用

判断弱全局引用是否被GC回收的方法:

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

后记

  • 参考刘望舒《Android进阶解密》