前言

JNI 中如果需要调用 Java 的方法,首先需要使用 FindClass 获取到 jclass ,然后使用 GetMethodID 获取到对应方法的 ID ,字段也是一样的要使用 GetFieldID 来获取。

如果每次都要查找一遍,是比较麻烦和耗时的,今天就来研究一下怎么把它们缓存起来以提高性能。

jclass

先来看看 jclass 是什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};

typedef _jclass*        jclass;
// ...
#else /* not __cplusplus */

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
//...

#endif /* not __cplusplus */

从源码中可以发现 jclass 是个引用类型,它所对应的 Java 类是 java.lang.Class

我们通过 FindClass 获取到的 jclass 是个局部引用,所以如果要缓存起来需要使用全局引用,因为局部引用会在栈退出后在触发 GC 的情况下被释放。

写个例子来看看获取到的局部引用是不是每次都不一样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MainActivity : AppCompatActivity() {

    companion object {
        init {
            System.loadLibrary("jcache")
        }
    }

    external fun printClassAddr()

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        printClassAddr()
        printClassAddr()
        printClassAddr()
    }
}

对应 C++ 代码

1
2
3
4
5
6
7
8
9
#include <jni.h>
#include <android/log.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_example_MainActivity_printClassAddr(JNIEnv *env, jobject thiz) {
    jclass clazz = env->FindClass("com/example/MainActivity");
    __android_log_print(ANDROID_LOG_VERBOSE, "JNI", "class address: %p", clazz);
}

结果如下

1
2
3
V/JNI: class address: 0x10001d
V/JNI: class address: 0x20001d
V/JNI: class address: 0x1d

从输出结果上来看,每次获取到的 jclass 地址都不一样,也就是我们说的它是个局部引用,需要用 NewGloabalRef 给缓存起来。

jmethodID/jfieldID

说完了 jclass 接下里看看 jmethodIDjfieldID ,依旧先来看下源码

1
2
3
4
5
struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID;     /* field IDs */

struct _jmethodID;                      /* opaque structure */
typedef struct _jmethodID* jmethodID;   /* method IDs */

从代码中看到 jmethodIDjfieldID 就是个指针,注释中说是不透明的类型,理解为指针就可以了。

由于 jmethodIDjfieldID 是指针,不是局部引用,所以可以直接保存起来,而不应该使用 NewGlobalRef 保存。

接下来依旧写个例子打印一下它们的地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MainActivity : AppCompatActivity() {

    companion object {
        init {
            System.loadLibrary("jcache")
        }
    }

    external fun printClassAddr()

    private lateinit var binding: ActivityMainBinding
    var name:String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        printClassAddr()
        printClassAddr()
        printClassAddr()
    }
}

对应 C++ 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <jni.h>
#include <android/log.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_example_MainActivity_printClassAddr(JNIEnv *env, jobject thiz) {
    jclass clazz = env->FindClass("com/example/MainActivity");
    jmethodID construct = env->GetMethodID(clazz, "onCreate", "(Landroid/os/Bundle;)V");
    jfieldID nameId = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
    __android_log_print(ANDROID_LOG_VERBOSE, "JNI", "methodId address: %p, fieldId address: %p", construct, nameId);
}

结果如下

1
2
3
V/JNI: methodId address: 0x7f8fada1a8, fieldId address: 0x7f8fada02c
V/JNI: methodId address: 0x7f8fada1a8, fieldId address: 0x7f8fada02c
V/JNI: methodId address: 0x7f8fada1a8, fieldId address: 0x7f8fada02c

可以看到,连续调用多次的情况下它们的值是相同的,这也印证了,我们可以直接保存的结论。

总结

  • jclass 是个局部引用,每次获取到的值不一定相同,需要使用 NewGlobalRef 来缓存
  • jmethodIdjfieldID 是个指针,每次获取到的值是一样的,可以直接保存起来

参考