在JNI代码中,有没有一种简单的方式将Java字符串转换为真正的UTF-8字节数组?很不幸,GetStringUTFChars() 函数几乎可以实现所需的功能,但并非完全如此,它返回一个"修改"过的UTF-8字节序列。主要区别在于修改后的UTF-8不包含任何空字符(因此您可以将其视为ANSI C空终止字符串),但另一个差异似乎是Unicode补充字符(例如表情符号)的处理方式。
像U+1F604 "微笑着张嘴和眯眼"这样的字符存储为代理对(两个UTF-16字符U+D83D U+DE04),并且具有4字节的UTF-8等效值 F0 9F 98 84,如果我在Java中将字符串转换为UTF-8,则会得到该字节序列:
char[] c = Character.toChars(0x1F604);
String s = new String(c);
System.out.println(s);
for (int i=0; i<c.length; ++i)
System.out.println("c["+i+"] = 0x"+Integer.toHexString(c[i]));
byte[] b = s.getBytes("UTF-8");
for (int i=0; i<b.length; ++i)
System.out.println("b["+i+"] = 0x"+Integer.toHexString(b[i] & 0xFF));
上面的代码输出如下:
c[0] = 0xd83d c[1] = 0xde04 b[0] = 0xf0 b[1] = 0x9f b[2] = 0x98 b[3] = 0x84
然而,如果我将's'传递给一个本地JNI方法并调用GetStringUTFChars(),我得到了6个字节。每个代理对字符都被单独转换为一个3字节序列:
JNIEXPORT void JNICALL Java_EmojiTest_nativeTest(JNIEnv *env, jclass cls, jstring _s)
{
const char* sBytes = env->GetStringUTFChars(_s, NULL);
for (int i=0; sBytes[i]!=0; ++i)
fprintf(stderr, "%d: %02x\n", i, sBytes[i]);
env->ReleaseStringUTFChars(_s, sBytes);
return result;
}
0: ed 1: a0 2: bd 3: ed 4: b8 5: 84
维基百科的UTF-8文章表明,GetStringUTFChars() 实际上返回CESU-8而不是UTF-8。这反过来会导致我的本地Mac代码崩溃,因为它不是一个有效的UTF-8序列:
CFStringRef str = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
CFURLRef url = CFURLCreateWithFileSystemPath(NULL, str, kCFURLPOSIXPathStyle, false);
我想我可以把所有的JNI方法都改成接受byte[]而不是String,并在Java中进行UTF-8转换,但这似乎有点丑陋,有更好的解决方案吗?
char*
指向真正的UTF-8数据而不是"修改后"的UTF-8数据,则可以采用以下两种方法之一:1)手动将UTF-8解码为UTF-16,然后将其传递给JNI的NewString()
函数;2)使用JNI将char
数据按原样复制到Java的byte[]
数组中,然后将该数组作为输入和字符集名称一起传递给String
构造函数,指定字符集为"UTF-8"。 - Remy Lebeauchar*
指向“修改过的”UTF-8数据,则可以直接使用JNI的NewStringUTF()
函数。 - Remy Lebeau