JNI 开发中 Java 层向下传字符串比较常用的是 JNIEnv 的 GetStringUTFChars 方法将 jstring 转为 const char *,用完后使用 ReleaseStringUTFChars 方法释放。

1
2
3
4
5
const jchar * GetStringChars(JNIEnv *env, jstring string,
jboolean *isCopy);

void ReleaseStringChars(JNIEnv *env, jstring string,
const jchar *chars);

然而使用该方法返回的字符串却并非采用标准 UTF-8 编码,而是Modified UTF-8 Strings,即一种修改过的 UTF-8 编码。

标准的 UTF-8 编码以 8-bit 即一个字节为基本单位,一个字符可以由一到六个字节编码表示,只要留出每个字节的高几位作为标志位就可以表示出该字节的类型,这样就可以判断出其后有几个字节与该字节合在一起表示一个字符。比如最高位为 0 表示该字节单独表示一个字符;最高两位为 10 表示该字节是跟在其他字节后面的,仅包含数据;最高三位为 110 则表示它将与后一个字节共同表示同一字符。

在 Modified UTF-8 Strings 中,U+FFFF 以上编码的字符(比如 Emoji 字符)并没有继续遵循 UTF-8 编码的规则,而是将其拆分为两个部分,分别使用三个字节存放,共六个字节。但这样也就导致了使用 UTF-8 编码解析的话便将会其识别为两个字符。

1 1 1 0 1 1 0 1
1 0 1 0
1 0
1 1 1 0 1 1 0 1
1 0 1 1
1 0

解决方案

主要有的解决思路有两种,一种是使用 UTF-16 编码,系统提供了对应的 GetStringChars 和 ReleaseStringChars 方法。

1
2
3
4
5
const jchar * GetStringChars(JNIEnv *env, jstring string,
jboolean *isCopy);

void ReleaseStringChars(JNIEnv *env, jstring string,
const jchar *chars);

另一种是先在 Java 层拿到 UTF-8 编码的字符串 byte[] 数据,再以 jbyteArray 的形式传入。

1
byte[] data = str.getBytes("UTF-8");
1
2
3
4
5
// jbyteArray _bytes;
const jsize length = env->GetArrayLength(_bytes);
jbyte *bytes = env->GetByteArrayElements(_bytes, 0);

env->ReleaseByteArrayElements(_bytes, bytes, JNI_ABORT);