
Android JNI 层在调用 NewStringUTF 时拒绝非法 Modified UTF-8 字符串(如含 Bengali 数字的线程名),导致崩溃;根本原因是 androidx.core 的 DefaultTaskExecutor 使用无本地化参数的 String.format 生成线程名,引发非 ASCII 字符编码异常。
jni 中遇到 modified utf-8 编码非法字节错误的根源与修复方案:android jni 层在调用 `newstringutf` 时拒绝非法 modified utf-8 字符串(如含 bengali 数字的线程名),导致崩溃;根本原因是 `androidx.core` 的 `defaulttaskexecutor` 使用无本地化参数的 `string.format` 生成线程名,引发非 ascii 字符编码异常。
该问题表现为 Logcat 中出现如下典型错误:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0xa string: 'arch_disk_io_' input: '0x61 0x72 0x63 0x68 0x5f 0x64 0x69 0x73 0x6b 0x5f 0x69 0x6f 0x5f 0xe0 0xa7 <0x0a>' in call to NewStringUTF
关键线索在于 input 字节序列末尾的 (即换行符 \n)紧随 0xe0 0xa7(Bengali 数字“৭”的 UTF-8 编码前两字节)之后——而 0xe0 0xa7 0x0a 并非合法 UTF-8 序列(缺少第三字节),导致 JVM 在将线程名转为 JNI String 时校验失败。
问题根源:String.format 的隐式本地化行为
androidx.arch.core:core-runtime 中 DefaultTaskExecutor 的线程工厂代码如下(已简化):
private static final String THREAD_NAME_STEM = "arch_disk_io_%d"; // ⚠️ 未指定 Locale,依赖系统默认 Locale(如 Bengali 环境下 %d → "৭") t.setName(String.format(THREAD_NAME_STEM, mThreadId.getAndIncrement()));
当设备语言设为孟加拉语(bn_BD)、印地语(hi_IN)等支持本地化数字的区域设置时,%d 格式化会输出 Unicode 本地数字(如 ৭),其 UTF-8 编码为 0xe0 0xa7 0x97。但若 mThreadId 值较大或存在并发竞争,可能触发字符串截断或拼接异常,最终传入 NewStringUTF 的字节流中混入孤立的多字节 UTF-8 起始字节(如 0xe0)与后续非法字节(如 0x0a),违反 JNI 对 Modified UTF-8 的严格要求(JVM 要求 NewStringUTF 输入必须是合法 Modified UTF-8,不支持任意 UTF-8)。
? 补充说明:JNI 的 NewStringUTF 并非接受标准 UTF-8,而是 Modified UTF-8 ——它要求:
- ASCII 字符(U+0000–U+007F)按原样编码;
- 其他字符使用标准 UTF-8 编码,但 U+0000 必须编码为 0xc0 0x80;
- 禁止使用高位字节 0xc0–0xc1 作为起始字节(用于代理 U+0000);
- 所有 UTF-8 多字节序列必须完整且合法。
因此,任何包含截断、乱码或非 ASCII 本地化数字的字符串,都可能在 JNI 层被拒绝。
解决方案:强制使用 Locale.US 进行格式化
通过自定义 TaskExecutor 替换默认实现,确保线程名始终由 ASCII 数字构成,彻底规避非 ASCII 字符编码风险:
@SuppressLint("RestrictedApi")
public class CustomTaskExecutor extends DefaultTaskExecutor {
private static final String CUSTOM_THREAD_NAME = "fix_arch_disk_io_%d";
private final ExecutorService mCustomDiskIO = Executors.newFixedThreadPool(4,
new ThreadFactory() {
private final AtomicInteger mThreadId = new AtomicInteger(0);
@Override
public Thread newThread(@NonNull Runnable r) {
Thread thread = new Thread(r);
// ✅ 强制使用 Locale.US,确保 %d 输出 ASCII 数字(如 "7" 而非 "৭")
thread.setName(String.format(Locale.US, CUSTOM_THREAD_NAME,
mThreadId.getAndIncrement()));
return thread;
}
});
@Override
public void executeOnDiskIO(@NonNull Runnable runnable) {
mCustomDiskIO.execute(runnable);
}
}随后在 Application 初始化阶段(务必早于任何 ArchTaskExecutor 使用点,推荐在 Application.onCreate())替换全局委托:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// ⚠️ 必须在首个 ViewModel/WorkManager 等组件初始化前执行
ArchTaskExecutor.getInstance().setDelegate(new CustomTaskExecutor());
}
}注意事项与最佳实践
- 时机至关重要:setDelegate() 必须在 ArchTaskExecutor 首次被内部调用前完成(例如在 ViewModel 创建、WorkManager 初始化之前),否则默认 DefaultTaskExecutor 已启动含非法名称的线程,无法挽回。
- 避免全局污染:此修复仅影响 executeOnDiskIO 调度的线程名,不影响应用其他 String.format 行为;若需全局统一,可封装 SafeFormatter 工具类,但本场景无需。
- 兼容性验证:该方案兼容所有 Android 版本(API 14+),且不依赖反射或私有 API,符合 Google Play 政策。
- 延伸排查:若仍有类似崩溃,建议检查所有 Thread.setName() 调用点,尤其涉及动态字符串拼接或 Locale 敏感格式化的场景,统一添加 Locale.US 参数。
通过这一精准干预,既绕过了 androidx 库的固有缺陷,又保持了架构兼容性,是解决 JNI Modified UTF-8 编码异常的稳健实践方案。










