Android ICS 4.0中NDK的NewStringUTF导致应用程序崩溃

15

我在JNI C/C++中有一个方法,它接受jstring并返回jstring,就像下面这样:

  NATIVE_CALL(jstring, method)(JNIEnv * env, jobject obj, jstring filename)
  {

// Get jstring into C string format.
  const char* cs = env->GetStringUTFChars (filename, NULL);
  char *file_path = new char [strlen (cs) + 1]; // +1 for null terminator
  sprintf (file_path, "%s", cs);
  env->ReleaseStringUTFChars (filename, cs);


  reason_code = INTERNAL_FAILURE;
  char* info = start_module(file_path);  


  jstring jinfo ;


  if(info==NULL)
  {
      jinfo = env->NewStringUTF(NULL);
  }
  else
  {
      jinfo = env->NewStringUTF(info);

  }


  delete info;

  info = NULL;
  return jinfo;
  }

这段代码在先前的 Android 4.0 版本(如 2.2、2.3 等)中运行得很好。但在 ICS 4.0 中,由于默认启用了检查 JNI 功能,导致应用程序崩溃并抛出以下错误:

 08-25 22:16:35.480: W/dalvikvm(24027): **JNI WARNING: input is not valid Modified UTF-8: illegal  continuation byte 0x40**
08-25 22:16:35.480: W/dalvikvm(24027):              
08-25 22:16:35.480: W/dalvikvm(24027): ==========
08-25 22:16:35.480: W/dalvikvm(24027): /tmp/create
08-25 22:16:35.480: W/dalvikvm(24027): ==========
08-25 22:16:35.480: W/dalvikvm(24027): databytes,indoorgames,drop
08-25 22:16:35.480: W/dalvikvm(24027): ==========���c_ag����ϋ@�ډ@�����@'
 08-25 22:16:35.480: W/dalvikvm(24027):              in Lincom/inter       /ndk/comNDK;.rootNDK:(Ljava/lang/String;)Ljava/lang/String; **(NewStringUTF)**
08-25 22:16:35.480: I/dalvikvm(24027): "main" prio=5 tid=1 NATIVE
08-25 22:16:35.480: I/dalvikvm(24027):   | group="main" sCount=0 dsCount=0 obj=0x40a4b460   self=0x1be1850
08-25 22:16:35.480: I/dalvikvm(24027):   | sysTid=24027 nice=0 sched=0/0 cgrp=default handle=1074255080
08-25 22:16:35.490: I/dalvikvm(24027):   | schedstat=( 49658000 26700000 48 ) utm=1 stm=3 core=1
08-25 22:16:35.490: I/dalvikvm(24027):   at comrootNDK(Native Method)

我不知道哪里出错了。如果您看到上面的NewStringUTF正在向c Char *字节添加一些垃圾值。

  1. 您有任何关于为什么会发生这种情况的想法吗?
  2. 欢迎任何替代方案来实现以上内容

如果您能帮助我,我真的会非常感激。提前致谢。

regds 我


由于在未初始化的字符数组上调用strcat()函数,我遇到了相同的错误,但是一旦对数组进行了初始化,问题就得到了解决。 - Napolean
10个回答

22
这个问题的原因与NDK/JNI中的GetStringUTFChars()函数(以及类似的NewStringUTF函数)存在已知的UTF-8 bug直接相关。这些NDK函数不能正确转换补充Unicode字符(即值为U+10000及以上的Unicode字符),导致不正确的UTF-8和随后的崩溃。
当处理包含表情符号字符(请参见相应的Unicode图表)的用户输入文本时,我遇到了崩溃。表情符号字符位于补充Unicode字符范围内。
问题分析:
1. Java客户端向JNI/NDK传递包含补充Unicode字符的字符串。 2. JNI使用NDK函数GetStringUTFChars()提取Java字符串的内容。 3. GetStringUTFChars()将字符串数据返回为不正确和无效的UTF-8。

已知存在一个 NDK bug,GetStringUTFChars() 错误地转换了补充字符集的 Unicode 字符,导致生成了不正确和无效的 UTF-8 序列。

在我的情况下,生成的字符串是一个 JSON 缓冲区。当缓冲区被传递给 JSON 解析器时,解析器立即失败,因为提取的 UTF-8 中有一个无效的 UTF-8 前缀字节。

可能的解决方法

我使用的解决方案可以概括如下:

  1. 目标是防止 GetStringUTFChars() 执行补充 Unicode 字符的错误 UTF-8 编码。
  2. 这通过 Java 客户端将请求字符串编码为 Base64 来实现。
  3. Base64 编码的请求被传递给 JNI。
  4. JNI 调用 GetStringUTFChars(),提取 Base64 编码的字符串而不执行任何 UTF-8 编码。
  5. JNI 代码然后解码 Base64 数据,生成原始的 UTF-16(宽字符)请求字符串,包括补充的 Unicode 字符。

这样我们就避免了从Java字符串中提取补充的Unicode字符的问题。相反,在调用GetStringUTFChars()之前,我们将数据转换为Base-64 ASCII,使用GetStringUTFChars()提取Base-64 ASCII字符,并将Base-64数据转换回宽字符。


2
非常有帮助,谢谢!看起来 Lollipop源代码 ,NewStringUTF() 进行了检查(entry == true),而 GetStringUTFChars() 没有进行检查,所以它们的行为不同。 - Over17
byte[] byteArray =Base64.encodeBase64(messageText.getText().toString().trim().getBytes()); String encodedString = new String(byteArray); 这样将字符串转换为Base64并将其传递给jni,但我的应用程序仍然崩溃。有什么想法吗? - Prashanth Debbadwar
1
(1) 你的崩溃性质是什么?有带符号的堆栈跟踪吗?(2) 在将<messageText>转换为base-64之前,您能否发布其十六进制转储以及JNI中GetStringUTFChars()返回的数据的十六进制转储? - Moshe Rubin
JNI在应用程序中检测到错误:输入无效的Modified UTF-8编码:非法的续字节0xc5,字符串为'Accepted�Ś�7',在调用NewStringUTF时出现问题。 - Prashanth Debbadwar
这个问题发生在标准的Java函数File.listFiles(File.java)中,所以我无法在手机上编辑源Linux/JNI/NDK代码来解决这个问题... - user924
显示剩余2条评论

13

这是我完成此任务的方法。

1- 将字符数组转换为JByteArray。

2- 将JByteArray转换为JString。

3- 将jstring返回给Java端。

JNI代码; (.c)格式

jstring Java_com_x_y_z_methodName(JNIEnv *env, jobject thiz) {
    int size = 16;
    char r[] = {'P', 'K', 'd', 'h', 't', 'X', 'M', 'm', 'r', '1', '8', 'n', '2', 'L', '9', 'K'};
    jbyteArray array = (*env)->NewByteArray(env, size);
    (*env)->SetByteArrayRegion(env, array, 0, size, r);
    jstring strEncode = (*env)->NewStringUTF(env, "UTF-8");
    jclass cls = (*env)->FindClass(env, "java/lang/String");
    jmethodID ctor = (*env)->GetMethodID(env, cls, "<init>", "([BLjava/lang/String;)V");
    jstring object = (jstring) (*env)->NewObject(env, cls, ctor, array, strEncode);

    return object;
}

Java 代码;

native String methodName();

其他方法对我无效;

我也尝试了return (*env)->NewStringUTF(env, r)但返回的一些字符不在char数组中,在字符串的末尾,伴随着JNI WARNING: input is not valid Modified UTF-8: illegal continuation byte 0x40警告。

例如:PKdhtXMmr18n2L9K�ؾ�����-DL

编辑:

C++版本

jstring clientStringFromStdString(JNIEnv *env,const std::string &str){
//    return env->NewStringUTF(str.c_str());
    jbyteArray array = env->NewByteArray(str.size());
    env->SetByteArrayRegion(array, 0, str.size(), (const jbyte*)str.c_str());
    jstring strEncode = env->NewStringUTF("UTF-8");
    jclass cls = env->FindClass("java/lang/String");
    jmethodID ctor = env->GetMethodID(cls, "<init>", "([BLjava/lang/String;)V");
    jstring object = (jstring) env->NewObject(cls, ctor, array, strEncode);
    return object;
}

以上解决方案对于不同的ABI返回不同的结果。例如,对于支持arm64-v8a,armeabi-v7a,armeabi的设备,它会返回完美的字符串,但是对于其他类型的设备,它会返回像PKdhtXMmr18n2L9K�ؾ�����-DL这样的字符串。 - Om Infowave Developers

11

我通过返回字节数组而不是字符串来解决了这个问题。在Java端,我现在将字节数组转换为字符串。效果很好!在Android 4.0及以上版本上,不要使用NewStringUTF(),因为Google Android NDK已经报告了一个错误。


@rana 请提供代码-您如何在Java端将byte[]转换为String。我总是遇到编码问题。 - dooplaye
1
如果您不想在各处更改您的jni代码,您可以在jni中执行new String(byte_array, "utf-8")(获取jni中的String构造函数,并从jni中调用它)。 - Helin Wang
Jni文件是以.so为后缀的,因此我无法编辑它们以接受bytearray而不是字符串。所以我需要对字符串进行编码或者做一些变通处理。请问有什么解决方案吗? - Prashanth Debbadwar

2

当我更改Application.mk文件时,遇到了这个问题。

从这一行开始:

APP_STL := stlport_static

至:

APP_STL := gnustl_static

我把它改回来后问题就解决了。


两年半过去了,情况可能已经发生了改变 :) 但我再也没有遇到这个问题了。 - Daniel Ryan

1

对我来说,解决方案是将内容放在const char *上:

const char* string = name_sin.c_str();
jstring utf8 = env_r->NewStringUTF(string);

和函数:

jclass cls_Env = env_r->FindClass(CLASS_ACTIVITY_NAME); 
jmethodID mid = env_r->GetMethodID(cls_Env, "Delegate",
                                 "(Ljava/lang/String;)V");


//todo importante hacerlo asi, si pasas directamente c_str a veces da error de carater no UTF 8
const char* string = name_sin.c_str();
jstring utf8 = env_r->NewStringUTF(string);

env_r->CallVoidMethod(*object_r, mid, utf8);

env_r->DeleteLocalRef(utf8);

1

我也曾经遇到过同样的问题,最后花了一天时间找到了解决方案。希望我的回复可以节省其他人的时间。

问题在于我在本地函数中调用了另一个函数,并直接使用返回的字符串,在较旧版本的安卓系统中导致崩溃。

所以,首先我将从另一个函数返回的字符串保存到一个变量中,然后再使用它,问题就解决了 :D

下面的示例可能会清楚你的概念。

//older code with error
//here key_ is the string from java code

const char *key = env->GetStringUTFChars(key_, 0);
const char *keyx = getkey(key).c_str();
return env->NewStringUTF(keyx);

这是我如何解决这个错误的。
//newer code which is working
//here key_ is the string from java code

const char *key = env->GetStringUTFChars(key_, 0);
string k = getkey(key);
const char *keyx = k.c_str();
return env->NewStringUTF(keyx);

愉快的编程 :D


1

你传递给NewStringUTF()的字符串需要是有效的修改过的UTF-8。看起来你的start_Inauthroot()函数返回的字符串是用其他编码方式编码的,或者只是返回了一个无效的字符串。在将其传递给JNI函数之前,你需要将该字符串转换为UTF-8。或者你可以使用其中一个字符集感知的String构造函数来构建String对象。


很抱歉再次打扰您..但是如何做到这一点..您对我以下的代码有什么建议, jclass strClass = env->FindClass("java/lang/String"); jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); <<<< 我可能在这里错了 jstring encoding = env->NewStringUTF("UTF-8"); jbyteArray bytes = env->NewByteArray(strlen(rInfo)); env->SetByteArrayRegion(bytes, 0, strlen(rInfo), (jbyte*)rootInformation); grootInfo = (jstring)env->NewObject(strClass, ctrId, bytes,"UTF-8"); - rana
是的,看起来没错,除了你需要将你的“encoding”jstring变量传递给NewObject(),而不是原始字符串“UTF-8”。当然,你需要传递实际编码的字符串,而不仅仅是“UTF-8”。 - kelnos
但是它在这一行抛出了一个错误, env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");有任何想法应该是什么原因? - rana
在这一行代码中抛出了一个错误:env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); 需要注意的是,在4.0及更高版本中,FindClass() 的返回值是一个本地引用。 - Alex Cohn
是的,我看到了;那就是@rana所说的。然而,当我运行这段代码时,它完全正常工作。我的问题是:到底抛出了什么错误?NoSuchMethodError?还是其他什么错误?FindClass()返回本地引用并不是问题,因为strClass立即在本地范围内使用。 - kelnos

1
在我看来,这不是一个 bug。
NewStringUTF 从一个使用修改后的 UTF-8 编码的字符数组构建一个新的 java.lang.String 对象。
修改后的 UTF-8 不是标准的 UTF-8。请参见 Modified UTF-8
在大多数情况下,UTF-8 编码的字符串是有效的修改后的 UTF-8。因为修改后的 UTF-8 和 UTF-8 很相似。但是,当涉及到超出基本多语言平面的 Unicode 字符串时,它们就不兼容了。
解决方案:将 UTF-8 字节传递给 Java 层并使用 new String(bytes, "UTF-8") 创建 jstring,然后将其传递给 JNI。

0

c Android NDK 的工作原理如下:

JNIEXPORT jstring JNICALL
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                                  jobject thiz,jstring str )
{

    jboolean isCopy;
    const char* szHTML = (*env)->GetStringUTFChars(env, str, &isCopy);
     return (*env)->NewStringUTF(env, szHTML);
}

0

这在我的c++中有效

extern "C" JNIEXPORT
jstring Java_com_example_ndktest_MainActivity_TalkToJNI(JNIEnv* env, jobject javaThis, jstring strFromJava)
{
    jboolean isCopy;
    const char* szHTML = env->GetStringUTFChars(strFromJava, &isCopy);

    std::string strMine;
    strMine = szHTML;
    strMine += " --- Hello from the JNI!!";

    env->ReleaseStringUTFChars(strFromJava, szHTML);
    return env->NewStringUTF(strMine.c_str());
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接