一、问题背景
在 IM 应用的日常运维中,我们经常面对一类"简单但高频"的崩溃:
NullPointerException:服务端返回了一个预期之外的 null 字段IndexOutOfBoundsException:列表数据异常导致越界ClassCastException:数据类型与预期不符NumberFormatException:格式化解析失败
这类崩溃通常不涉及复杂的业务逻辑错误,本质上是防御性编程的缺失。传统流程中,从用户崩溃 → 日志上报 → 开发定位 → 修复发版 → 用户更新,链路漫长。用户在这段时间内可能反复触发同一个崩溃。
能不能让 App 自己修好自己?
二、整体架构
我们设计了一套端侧 AI 崩溃自修复系统,整体架构如下:
关键设计决策:
- 双进程隔离 :崩溃分析和 AI 推理在独立的
:crash进程中执行,主进程已崩溃释放内存,为模型推理提供了足够的内存空间。 - 跨进程通信 :通过
filesDirJSON 文件实现,:crash进程写入补丁元数据,主进程启动时读取并加载。 - AI + 规则引擎双保险:AI 不可用时(模型未下载、推理失败等),自动 fallback 到确定性的规则引擎。
三、核心模块详解
3.1 崩溃分析器 ------ CrashAnalyzer
崩溃分析器是整个流程的"守门人",决定一个崩溃是否值得自动修复。
kotlin
object CrashAnalyzer {
// 可自动修复的异常类型白名单
private val FIXABLE_EXCEPTIONS = mapOf(
"java.lang.NullPointerException" to CrashType.NPE,
"java.lang.IndexOutOfBoundsException" to CrashType.INDEX_OUT_OF_BOUNDS,
"java.lang.ArrayIndexOutOfBoundsException" to CrashType.INDEX_OUT_OF_BOUNDS,
"java.lang.ClassCastException" to CrashType.CLASS_CAST,
"java.lang.NumberFormatException" to CrashType.NUMBER_FORMAT,
)
// 系统类和三方库前缀 --- 不修复
private val EXCLUDE_PREFIXES = listOf(
"android.", "androidx.", "java.", "kotlin.",
"com.google.", "com.facebook.", "okhttp3.", ...
)
fun analyze(crashMessage: String?, stackTrace: String?): CrashRepairPlan? {
// 1. 解析异常类型 → 白名单过滤
// 2. 提取第一个 app 代码栈帧(跳过系统类和三方库)
// 3. 排除启动路径上的类(防止 Application/ContentProvider 被修改导致启动循环)
// 4. 生成修复计划
}
}
设计要点:
- 白名单机制 :只修复已知可安全处理的异常类型,不会尝试修复
SecurityException、OutOfMemoryError等系统级错误。 - 排除启动路径 :
Application、ContentProvider、SplashActivity等启动链上的类不做修复,因为修复失败会导致启动循环。 - 只修复 App 代码:系统框架和三方库的崩溃不处理。
3.2 Smali 提取器 ------ SmaliExtractor
这是连接 "Java 崩溃" 和 "端侧 AI" 的桥梁。我们需要把崩溃方法的字节码转化为 AI 能理解的文本。
kotlin
object SmaliExtractor {
private const val MAX_SMALI_LENGTH = 1500 // 控制 prompt 长度
fun extract(context: Context, plan: CrashRepairPlan): String? {
val apkPath = context.applicationInfo.sourceDir
val opcodes = Opcodes.forApi(Build.VERSION.SDK_INT)
// 1. 从 base.apk 中查找目标类(遍历所有 classes*.dex)
val classDef = findClass(apkPath, plan.dexClassName, opcodes)
// 2. 定位目标方法
val method = findMethod(classDef, plan.methodName)
// 3. 格式化为 smali 文本
return formatMethod(method, method.implementation)
}
}
生成的 smali 文本示例:
smali
.method public parseUserName()Ljava/lang/String;
.registers 3
#0 sget-object v0, Lcom/example/Foo;->userData:Ljava/lang/String;
#1 invoke-virtual {v0}, Ljava/lang/String;->trim()Ljava/lang/String;
#2 move-result-object v0
#3 invoke-virtual {v0}, Ljava/lang/String;->uppercase()Ljava/lang/String;
#4 move-result-object v0
#5 return-object v0
.end method
指令 #1 处,如果 v0 为 null,调用 trim() 就会 NPE。AI 需要识别这个位置,并在此前插入 null 检查。
3.3 端侧推理 ------ MNN-LLM 引擎
我们选择了阿里巴巴的 MNN (Mobile Neural Network) 作为端侧推理框架,集成了一个 1.5B 参数的代码模型。
css
app-mnn/
├── src/main/cpp/
│ ├── mnn_llm_bridge.cpp ← JNI 桥接层
│ └── include/
│ ├── MNN/ ← MNN 推理框架头文件
│ └── llm/llm.hpp ← LLM 推理接口
├── src/main/jniLibs/arm64-v8a/
│ ├── libMNN.so ← MNN 核心 (2.8MB)
│ ├── libMNN_Express.so ← MNN 表达式引擎 (749KB)
│ └── libllm.so ← LLM 推理 (1.8MB)
└── src/main/java/com/example/app/mnn/
├── MnnLlmEngine.kt ← Kotlin 推理接口
└── ModelManager.kt ← 模型文件管理
JNI 桥接层核心代码:
cpp
JNIEXPORT jlong JNICALL
Java_com_example_app_mnn_MnnLlmEngine_nativeCreate(JNIEnv *env, jobject thiz, jstring modelDir) {
std::string config_path = model_dir_str + "/config.json";
Llm *llm = Llm::createLLM(config_path);
llm->load();
return reinterpret_cast<jlong>(llm);
}
JNIEXPORT jstring JNICALL
Java_com_example_app_mnn_MnnLlmEngine_nativeGenerate(JNIEnv *env, jobject thiz,
jlong handle, jstring prompt, jint maxTokens) {
Llm *llm = reinterpret_cast<Llm *>(handle);
// 设置最大生成长度
llm->set_config("{\"max_new_tokens\":" + std::to_string(maxTokens) + "}");
// 执行推理,收集输出
std::ostringstream oss;
auto callback = [&oss](const char* token) { oss << token; };
llm->response(prompt_str, &oss);
return env->NewStringUTF(oss.str().c_str());
}
关键参数:
- 推理超时:60 秒(手机端 1.5B 模型推理较慢)
- 最大 token 数:128(修复策略 JSON 约 50-100 tokens,无需更多)
- 模型加载时机 :仅在
:crash进程中加载,主进程已崩溃释放了内存
3.4 Prompt 工程 ------ RepairPromptBuilder
我们使用 ChatML 格式构建 prompt,引导模型输出结构化的 JSON 修复策略:
kotlin
private const val SYSTEM_PROMPT = """You are an Android crash repair expert.
Analyze the crash and smali code, output a JSON repair strategy.
Strategy types: null_check, bounds_check, type_check, try_catch_wrap, return_early
You MUST also provide "suggested_return_value" - a safe default return value
based on the method's business logic:
- For String methods: use "empty_string"
- For numeric methods: use "0" or "-1"
- For boolean methods: use "false"
- For List/Collection: use "empty_list"
- For void: use "void"
Output ONLY JSON:
{"strategy":"<type>","target_register":<reg>,
"insert_before_index":<idx>,"confidence":<0-1>,
"suggested_return_value":"<value>"}"""
实际发送给模型的完整 prompt:
vbnet
<|im_start|>system
You are an Android crash repair expert...
<|im_end|>
<|im_start|>user
Crash: NPE
Method: Lcom/example/app/crash/repair/CrashSimulator;->parseUserName
Return type: String
Message: Attempt to invoke virtual method 'java.lang.String.trim()' on null
Smali:
.method public parseUserName()Ljava/lang/String;
.registers 3
#0 sget-object v0, ...
#1 invoke-virtual {v0}, Ljava/lang/String;->trim()...
...
.end method
<|im_end|>
<|im_start|>assistant
模型的期望输出:
json
{
"strategy": "null_check",
"target_register": 0,
"insert_before_index": 1,
"confidence": 0.92,
"suggested_return_value": "empty_string"
}
3.5 策略解析与校验 ------ RepairStrategyParser
模型输出不可盲信。我们对 AI 输出进行了严格的校验:
kotlin
object RepairStrategyParser {
private const val MIN_CONFIDENCE = 0.3f // 最小置信度阈值
fun parse(response: String): RepairStrategy? {
// 1. 从模型输出中提取 JSON(可能混杂其他文本)
val jsonStr = extractJson(response) ?: return null
// 2. 解析 JSON
val json = JSONObject(jsonStr)
// 3. 策略类型白名单校验
val strategyType = STRATEGY_MAP[json.optString("strategy")] ?: return null
// 4. 置信度阈值过滤
if (confidence < MIN_CONFIDENCE) return null
// 5. 参数合法性校验(寄存器号 >= 0,指令索引 >= -1)
if (!validateStrategy(strategyType, targetRegister, insertBeforeIndex))
return null
return RepairStrategy(...)
}
}
6 种修复策略类型:
| 策略 | 含义 | 适用场景 |
|---|---|---|
NULL_CHECK |
在目标指令前插入 null 检查 | NPE |
BOUNDS_CHECK |
插入越界检查 | IndexOutOfBoundsException |
TYPE_CHECK |
插入 instance-of 检查 | ClassCastException |
TRY_CATCH_WRAP |
整方法 try-catch 包裹 | 兜底方案 |
REPLACE_METHOD_CALL |
替换方法调用 (如 getInt → optInt) | 数据解析 |
RETURN_EARLY |
方法入口直接返回 | 功能级降级 |
3.6 Dex 字节码修补引擎 ------ CrashRepairEngine & SmaliTemplateEngine
这是整个系统最底层的核心------操作 Dex 字节码。
核心流程:
scss
base.apk → DexFileFactory.loadDexContainer()
→ 遍历 classes*.dex 查找目标类
→ 定位目标方法
→ MutableMethodImplementation 修改字节码
→ 构建只含修补类的新 DexFile
→ DexPool.writeTo() 写出 patch.dex
AI 策略 vs 规则引擎的关键差异:
kotlin
// SmaliTemplateEngine --- AI 驱动
fun resolveExceptionType(strategy: RepairStrategy, plan: CrashRepairPlan): String {
// AI 会根据上下文推断更精确的异常类型
// 例如:NPE 场景中,AI 可能建议只捕获 NullPointerException
// 而非笼统的 Exception
return when (strategy.type) {
NULL_CHECK -> "Ljava/lang/NullPointerException;" // 精确捕获
BOUNDS_CHECK -> "Ljava/lang/IndexOutOfBoundsException;" // 精确捕获
TYPE_CHECK -> "Ljava/lang/ClassCastException;" // 精确捕获
TRY_CATCH_WRAP -> plan.exceptionDexType // 使用原始异常
else -> plan.exceptionDexType
}
}
规则引擎(CrashRepairEngine)和 AI 模板引擎(SmaliTemplateEngine)底层共用 wrapWithTryCatch 方法生成字节码,但 AI 引擎增加了两个维度的精准性:
- 更精确的异常类型:不是一股脑 catch 所有异常,而是只捕获特定类型
- 更智能的返回值:AI 根据方法的业务语义建议安全返回值(如返回空字符串而非 null)
3.7 补丁加载器 ------ CrashRepairLoader
采用与 Tinker 相同的原理,在 attachBaseContext() 中将 patch dex 注入到 ClassLoader 的 dexElements 数组最前面:
kotlin
object CrashRepairLoader {
fun loadIfExists(context: Context) {
val patches = CrashRepairStore.readPatchMetas(context)
if (patches.isEmpty()) return
// Android 9+ 绕过 hidden API 限制
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
HiddenApiBypass.addHiddenApiExemptions("")
}
injectDexFiles(context.classLoader, patchFiles)
}
private fun injectDexFiles(classLoader: ClassLoader, patchFiles: List<File>) {
// 1. 反射获取 BaseDexClassLoader.pathList
// 2. 反射获取 DexPathList.dexElements
// 3. 用 patch 文件创建新的 Element[]
// 4. 合并:patch elements + original elements
// → patch 在前,ClassLoader 优先加载补丁类
// 5. 反射写回 dexElements
}
}
原理解释:
ini
ClassLoader 类查找顺序:
dexElements[0] → patch.dex (修复后的 CrashSimulator) ✓ 优先命中
dexElements[1] → classes.dex (原始 CrashSimulator) ✗ 被跳过
dexElements[2] → classes2.dex
...
四、安全防护:BootLoopGuard
最危险的场景:修复补丁本身导致了崩溃。如果不加防护,App 会陷入"启动 → 加载补丁 → 崩溃 → 重启 → 加载补丁 → 崩溃"的死循环。
kotlin
object BootLoopGuard {
private const val FAST_CRASH_THRESHOLD_MS = 5_000L // 5 秒内再次启动 = 崩溃
private const val MAX_FAST_CRASH_COUNT = 3 // 连续 3 次快速崩溃
private const val STABLE_THRESHOLD_MS = 30_000L // 运行超 30 秒 = 稳定
fun check(context: Context): Boolean {
val timeSinceLastBoot = now - lastBoot
if (timeSinceLastBoot < FAST_CRASH_THRESHOLD_MS) {
val newCount = crashCount + 1
if (newCount >= MAX_FAST_CRASH_COUNT) {
// 连续快速崩溃 → 清除所有补丁 → 裸启动
CrashRepairStore.clearAll(context)
return true // 需要裸启动
}
} else {
// 正常间隔启动,重置计数器
resetCounter()
}
return false
}
fun markStable(context: Context) {
// 运行超过 30 秒后调用,重置崩溃计数器
}
}
调用时序图:
scss
attachBaseContext()
│
├── BootLoopGuard.check() ← 最先执行
│ └── 连续 3 次快速崩溃? → clearAll() → 裸启动
│
└── CrashRepairLoader.loadIfExists() ← 只有通过 guard 才执行
└── inject patch dex
onCreate()
│
└── postDelayed(30s)
└── BootLoopGuard.markStable() ← 运行稳定,重置计数
五、AI 修复 vs 规则引擎:实际效果对比
以 CrashSimulator.parseUserName() 为例:
kotlin
fun parseUserName(): String? {
val userData: String? = null
val name = userData!!.trim() // ← NPE 发生在这里
return name.uppercase()
}
规则引擎修复(try-catch 兜底)
kotlin
整个方法体包裹 try-catch:
try {
val userData: String? = null
val name = userData!!.trim() // NPE 被 catch
return name.uppercase()
} catch (e: NullPointerException) {
return null // ← 返回 null
}
问题 :调用方拿到 null,如果直接使用会触发连环 NPE:
kotlin
val name = CrashSimulator.parseUserName() // 返回 null
textView.text = "Hi, ${name.length}" // 又 NPE!
AI 修复(精准 null 检查 + 安全返回值)
AI 分析 smali 后识别出:
- 目标寄存器
v0在指令#1处可能为 null - 方法返回类型是
String,建议返回空字符串而非 null
csharp
指令 #0: sget-object v0, ...
── AI 插入 ── if-eqz v0, :safe_return ← null 检查
指令 #1: invoke-virtual {v0}, trim()
指令 #2: move-result-object v0
...
:safe_return
const-string v0, "" ← 返回空字符串
return-object v0
效果 :调用方拿到空字符串 "","Hi, ${name.length}" 正常显示为 "Hi, 0",不会崩溃。
六、数据持久化与补丁管理
kotlin
object CrashRepairStore {
const val MAX_PATCH_SIZE_BYTES = 1024 * 1024L // 1MB
private const val MAX_ATTEMPTS_PER_CRASH = 3 // 同一 crash 最多修 3 次
private const val MAX_PATCH_COUNT = 5 // 最多 5 个补丁共存
private const val PATCH_TTL_MS = 48 * 60 * 60 * 1000L // 48 小时过期
// 文件结构:
// filesDir/crash_repair/
// ├── patch_1234567890.dex ← 补丁 dex
// ├── patch_ai_1234567891.dex ← AI 生成的补丁 dex
// ├── repair_meta.json ← 待应用的补丁元数据
// └── repair_history.json ← 历史修复记录
}
安全约束:
- 补丁文件大小限制 1MB(防止异常 dex)
- 同一个崩溃签名最多修复 3 次(防止无限重试)
- 最多 5 个补丁同时生效(控制内存影响)
- 补丁 48 小时后自动过期清除(等待正式修复版本发布)
七、完整调用链路
scss
用户操作 → Java 崩溃
↓
UncaughtExceptionHandler 捕获
↓
启动 CrashActivity (:crash 进程)
↓
┌─ CrashAnalyzer.analyze()
│ ├── 解析 stack trace → 提取崩溃位置
│ ├── 白名单过滤 → NPE/IOOB/CCE/NFE
│ ├── 排除系统类/三方库/启动路径
│ └── 生成 CrashRepairPlan
│
├─ AiRepairOrchestrator.attemptAiRepair()
│ ├── [1] ModelManager.ensureModel() 检查模型文件
│ ├── [2] MnnLlmEngine.create() 加载 1.5B 模型
│ ├── [3] SmaliExtractor.extract() 从 APK 提取 smali
│ ├── [4] RepairPromptBuilder.build() 构建 ChatML prompt
│ ├── [5] MnnLlmEngine.generate() 端侧推理 (≤60s)
│ ├── [6] RepairStrategyParser.parse() 解析+校验 JSON
│ └── [7] SmaliTemplateEngine.apply() 字节码修补 → patch.dex
│
├─ CrashRepairStore.savePatchMeta()
│ └── 写入 filesDir/crash_repair/
│
└─ 重启 App
↓
MyApplication.attachBaseContext()
├── BootLoopGuard.check() 启动循环检测
└── CrashRepairLoader.loadIfExists() 注入 patch dex
↓
App 正常运行,崩溃方法已被修复版本替换
↓
30 秒后 → BootLoopGuard.markStable() 标记启动稳定
八、技术选型与 Trade-off
| 决策点 | 选择 | 原因 |
|---|---|---|
| 推理框架 | MNN (Mobile Neural Network) | 阿里出品,对移动端优化深入,支持 ARM NEON/GPU |
| 模型规模 | 1.5B 参数 | 手机端可运行,且对代码理解能力足够 |
| 字节码操作 | dexlib2 (smali/baksmali) | 成熟稳定,Apktool 底层库 |
| Hidden API 绕过 | LSPosed HiddenApiBypass | 无需 root,运行时绕过 |
| 跨进程通信 | filesDir JSON 文件 | 简单可靠,无需 AIDL/ContentProvider |
| 补丁注入 | dexElements 数组前插 | Tinker 验证过的成熟方案 |
九、已知限制与后续演进
当前限制:
- 仅支持 Java/Kotlin 崩溃,不支持 Native crash
- 模型文件较大(~1.5GB),后续可以剪枝处理,是否能精简到 1G 以内
- 首次崩溃时如果模型未下载,只能使用规则引擎
- 不修复启动路径上的类(Application、ContentProvider 等)
后续规划:
- 目前只是尝试还没有上线,后续把模型部署服务端来做。
- 接入云端 API 作为端侧推理的补充(模型未就绪时)
- 补丁效果验证:重启后自动验证修复是否有效
- 修复策略的 A/B 测试和指标跟踪
- 探索更小的模型(< 500MB)以降低下载成本
十、关于反射问题
Android 9+ 反射系统函数会被禁掉,参考 突破 Android 9.0 + 受限制系统 Api。
十一、总结
这套端侧 AI 崩溃自修复系统的核心创新在于:
- 端侧推理:不依赖网络,在崩溃现场直接分析和修复
- AI + 规则引擎协同:AI 提供精准策略,规则引擎保证兜底
- 完整安全防护:BootLoopGuard 防止修复引入新问题
- 零侵入热修复:利用 ClassLoader 机制,无需重新编译或签名
从"用户崩溃 → 开发修复 → 发版更新"的时级响应,缩短到"崩溃 → 自修复 → 重启恢复"的秒级响应。
这个方案主要是解决崩溃导致用户行为阻断,结合 Ai 的能力,将问题尽可能的修复成功。