面试题十四:JNI如何实现数据传递?

JNI如何实现数据传递?

一、面试官视角:这道题想考察什么?

  • 是否有Native开发经验
  • 是否对JNI数据传递中的细节有认识
  • 是否能够合理的设计JNI的界限

二、题目剖析

1、传递什么数据?

2、如何实现内存回收?

3、性能如何?

4、结合实例来分析更有效

举例一:在Java层有个Bitmap(Bitmap.java)类,在Native层也有一个类(Bitmap.h/cpp)与之对应,那么这两者如何关联起来呢?

通过在Java层Bitmap.java持有的private final long mNativePtr(指针),这个mNativePtr就是Native层的Bitmap.h/cpp的指针。

1
2
3
4
5
6
7
8
9
10
11
// Java层Native方法
// 该方法用于压缩Bitmap到stream(流)里面。
// nativeBitmap是指针,对应的底层Native函数中的bitmapHandle参数,然后在底层函数中使用bitmapHandle传入到bitmap(bitmapHandle)函数中,获取到对应的底层Bitmap对象,获取到之后就可以操作底层的Bitmap了。
private static native boolean nativeCompress(long nativeBitmap, int format, int quality, OutputStream stream, byte[] tempStorage);

// 底层Native函数
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle, jint format, jint quality, jobject jstream, jbyteArray jstorage) {
LocalScopeBitmap bitmap(bitmapHandle);
}

// 结论:在底层有一个类,在Java层也有一个类,如果关联起来呢?通过底层的指针,这个指针在Java层就是一个长整型(long),如果你确定是32位的机器的话,给个整型(int)也可以。

举例二:字符串操作

  • GetStringUTFChars/ReleaseStringUTFChars

    • const char*

    • 拷贝出Modified-UTF-8的字节流(字节码存字符串使用MUTF-8)

    • \0编码成0xC080,不会影响C字符串结尾

    • const char* GetStringUTFChars(jstring string, jboolean* isCopy)

      isCopy:并不是要你指定是否要拷贝,而是要你传进去一个引用,然后它会把Get之后的结果是不是拷贝的告诉你,本质上isCopy是一个返回值。

      在调用完GetStringUTFChars方法之后,*isCopy == false,表示没有复制,为什么?

      比如在Java虚拟机里面有个”HelloWorld”,那么Native函数中的const char*指向的就是它,假设内存GC了,GC之后一般都要进行整理,GC的算法有些就是要标记、整理和复制,这样做主要是为了减少内存碎片,让内存更加的连续,从而适应大对象的分配,但这个时候你因为要复制这个”HelloWorld”对象的话,就意味着要把这个对象锁定,为啥要锁定,因为const char*指针是直接指向它的,这样的话,就不能GC了,因为一旦GC,这个指针不就跑了嘛!那你就指向了一个野指针,所以这个就要看虚拟机支持不支持,有些虚拟机还真就不支持,它们倾向于复制,而不是通过指针直接指向,因为可以减少它的逻辑,让它更轻松一些,所以很多虚拟机都会返回一个*isCopy == true,那么这种情况就是,它帮你复制一分”HelloWorld”这块内存过来,然后指针指向的就是C层的一块内存了,跟Java虚拟机内存没有了任何关系了,Jvm GC就GC呗,无所谓!。

  • GetStringChars/ReleaseStringChars

    • const jchar*(Java中的两个char对应一个jchar)
    • JNI函数自动处理字节序转换
    • const char* GetStringChars(jstring string, jboolean* isCopy)
  • GetStringUTFRegion/getStringRegion

    • 先在C层创建足够容量的空间
    • 将字符串的某一部分复制到开辟好的空间
    • 针对性复制,少量读取时效率更优
  • GetStringCritical/ReleaseStringCritical

    • 调用对中间会停掉 Jvm GC

    • 调用对之间不可有其他操作

    • 调用对可嵌套

    • const jchar* GetStringCritical(jstring string, jboolean* isCopy)

      在调用完GetStringCritical方法之后,很容易返回一个*isCopy == false的情况,因为它给Java虚拟机加了把锁,阻止了Jvm GC,从而直接指向Java虚拟机内存,该方法调用时也要注意,期间不能调用其它JNI函数,不然容易出现死锁,而且用的时候尽量时间要短,用完之后马上归还(调用ReleaseStringCritical)。

举例三:对象数组传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void useJObjectArray2(JNIEnv *env, jclass, jobjectArray objArray) {
jfieldID latFieldId = NULL, lngFieldId = NULL;
jint length = env->GetArrayLength(objArray);
for(int i = 0; i < length; i++) {
// localRef有个数限制,常见最多512个
jobject obj = env->GetObjectArrayElement(objArray, i);
if(latFieldId == NULL) {
//init latFieldId & lngFieldId
}

// GetDoubleField:访问Java对象是通过使用Java反射方式来访问的
// 在设计JNI函数边界的时候,应该要尽可能的让底层接触更少的Java层的对象,比如给底层传一些基本的类型,这种代价会比较小,代价小主要体现在第一个方面拷贝的代价比较小,第二个方面就是访问的代价比较小,第三个方面就是逻辑的代价也比较小,尽可能的让底层关心效率,而不关心逻辑。
jdouble lat = env->GetDoubleField(obj, latFieldId);
jdouble lng = env->GetDoubleField(obj, lngFieldId);
LOGD("LatLng(%f, %f)", lat, lng);
}
}

举例四:DirectBuffer

DirectBuffer是直接在物理内存上开辟了一块空间,所以对于Java虚拟机来说,可以直接读写它,对于Native层也可以直接读写它,这样的话,就不需要拷贝了,而且拷贝也是需要成本的。

如下代码所示:

在Jave层直接往ByteBuffer里面写了一串数值比如1 2 3 4 5 6,在Native层可以直接读,但是要注意字节序的问题。

1
2
3
4
5
6
Java虚拟机(ByteBuffer)

ByteBuffer buffer = ByteBuffer.allocateDirect(100);
buffer.putInt(...);
buffer.flip();
NativeCInf.useDirectBuffer(buffer, buffer.limit());
1
2
3
4
5
int* bufPtr = (int*)env->GetDirectBufferAddress(buf);
for(int i = 0; i< length / sizeof(int); i++) {
// bufPtr[i],此处要注意字节序的问题
LOGI("useArray: %d", bufPtr[i]);
}

三、题目结论

  • 通过long类型传递底层对象指针给Java层
  • 注意String的几组函数操作的区别与适用场景
  • 注意对象数组较大时localRef超过上限的问题
  • 注意使用DirectBuffer时字节序的问题