Android 侧 AI 自修复崩溃方案

一、问题背景

在 IM 应用的日常运维中,我们经常面对一类"简单但高频"的崩溃:

  • NullPointerException:服务端返回了一个预期之外的 null 字段
  • IndexOutOfBoundsException:列表数据异常导致越界
  • ClassCastException:数据类型与预期不符
  • NumberFormatException:格式化解析失败

这类崩溃通常不涉及复杂的业务逻辑错误,本质上是防御性编程的缺失。传统流程中,从用户崩溃 → 日志上报 → 开发定位 → 修复发版 → 用户更新,链路漫长。用户在这段时间内可能反复触发同一个崩溃。

能不能让 App 自己修好自己?

二、整体架构

我们设计了一套端侧 AI 崩溃自修复系统,整体架构如下:

flowchart TB subgraph CRASH_PROCESS ["🔧 :crash 进程 --- 崩溃现场分析与修复"] direction TB CA["① CrashAnalyzer\n解析 stack trace · 白名单过滤\n生成 CrashRepairPlan"] subgraph AI_PIPELINE ["AiRepairOrchestrator --- 7 步 AI 修复流水线"] direction LR M1["② 检查模型\nModelManager"] M2["③ 加载模型\nMnnLlmEngine"] M3["④ 提取 smali\nSmaliExtractor"] M4["⑤ 构建 Prompt\nRepairPromptBuilder"] M5["⑥ 端侧推理\nMNN 1.5B"] M6["⑦ 解析策略\nRepairStrategyParser"] M7["⑧ 生成补丁\nSmaliTemplateEngine"] M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7 end CA --> AI_PIPELINE AI_PIPELINE -- "AI 不可用时" --> FALLBACK["规则引擎 Fallback\nCrashRepairEngine\n(try-catch 兜底)"] AI_PIPELINE --> STORE["⑨ CrashRepairStore\n写入 patch.dex + meta.json\nfilesDir/crash_repair/"] FALLBACK --> STORE end subgraph IPC ["📁 filesDir --- 跨进程通信"] PATCH[("patch.dex\nrepair_meta.json")] end subgraph MAIN_PROCESS ["🚀 主进程 --- 启动时加载补丁"] direction TB BG["① BootLoopGuard.check()\n连续 3 次快速崩溃?\n→ 清除所有补丁 · 裸启动"] LOADER["② CrashRepairLoader\n注入 patch dex → dexElements[0]\nClassLoader 优先加载修复类"] STABLE["③ postDelayed(30s)\nBootLoopGuard.markStable()\n重置崩溃计数器"] BG -->|"通过检查"| LOADER --> STABLE BG -->|"启动循环!"| CLEAR["清除所有补丁\n裸启动恢复"] end CRASH(("💥 Java 崩溃\nUncaughtExceptionHandler")) --> CRASH_PROCESS STORE --> PATCH PATCH --> BG STABLE --->|"App 正常运行\n崩溃已修复 ✅"| RUNNING(("✅ 稳定运行")) style CRASH fill:#ff6b6b,stroke:#c0392b,color:#fff style RUNNING fill:#2ecc71,stroke:#27ae60,color:#fff style CRASH_PROCESS fill:#1a1a2e11,stroke:#e94560 style MAIN_PROCESS fill:#1a1a2e11,stroke:#0f3460 style AI_PIPELINE fill:#16213e22,stroke:#533483 style IPC fill:#f8f9fa,stroke:#6c757d style PATCH fill:#fff3cd,stroke:#ffc107 style FALLBACK fill:#fff0f0,stroke:#e74c3c,stroke-dasharray: 5 style CLEAR fill:#ffe0e0,stroke:#e74c3c

关键设计决策:

  • 双进程隔离 :崩溃分析和 AI 推理在独立的 :crash 进程中执行,主进程已崩溃释放内存,为模型推理提供了足够的内存空间。
  • 跨进程通信 :通过 filesDir JSON 文件实现,: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. 生成修复计划
    }
}

设计要点:

  • 白名单机制 :只修复已知可安全处理的异常类型,不会尝试修复 SecurityExceptionOutOfMemoryError 等系统级错误。
  • 排除启动路径ApplicationContentProviderSplashActivity 等启动链上的类不做修复,因为修复失败会导致启动循环。
  • 只修复 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 引擎增加了两个维度的精准性:

  1. 更精确的异常类型:不是一股脑 catch 所有异常,而是只捕获特定类型
  2. 更智能的返回值: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 验证过的成熟方案

九、已知限制与后续演进

当前限制:

  1. 仅支持 Java/Kotlin 崩溃,不支持 Native crash
  2. 模型文件较大(~1.5GB),后续可以剪枝处理,是否能精简到 1G 以内
  3. 首次崩溃时如果模型未下载,只能使用规则引擎
  4. 不修复启动路径上的类(Application、ContentProvider 等)

后续规划:

  • 目前只是尝试还没有上线,后续把模型部署服务端来做。
  • 接入云端 API 作为端侧推理的补充(模型未就绪时)
  • 补丁效果验证:重启后自动验证修复是否有效
  • 修复策略的 A/B 测试和指标跟踪
  • 探索更小的模型(< 500MB)以降低下载成本

十、关于反射问题

Android 9+ 反射系统函数会被禁掉,参考 突破 Android 9.0 + 受限制系统 Api

十一、总结

这套端侧 AI 崩溃自修复系统的核心创新在于:

  1. 端侧推理:不依赖网络,在崩溃现场直接分析和修复
  2. AI + 规则引擎协同:AI 提供精准策略,规则引擎保证兜底
  3. 完整安全防护:BootLoopGuard 防止修复引入新问题
  4. 零侵入热修复:利用 ClassLoader 机制,无需重新编译或签名

从"用户崩溃 → 开发修复 → 发版更新"的时级响应,缩短到"崩溃 → 自修复 → 重启恢复"的秒级响应。

这个方案主要是解决崩溃导致用户行为阻断,结合 Ai 的能力,将问题尽可能的修复成功。

相关推荐
开开心心就好2 小时前
经典塔防游戏移植移动端随时畅玩
java·前端·科技·游戏·edge·django·pdf
We་ct2 小时前
前端包管理工具与Monorepo全面解析
前端·javascript·npm·pnpm·yarn·monorepo·包管理
ZPC82102 小时前
moveit servo 发指令给real arm
java·前端·数据库
油丶酸萝卜别吃2 小时前
高效处理数组差异:JS中新增、删除、交集的最优解(Set实现)
开发语言·前端·javascript
GISer_Jing2 小时前
前端动画技术全解析:从GIF到WebGPU
前端·ai·动画·webgl
LIO2 小时前
Vue3 + TS 企业级工程化项目全套实战(Vue3 + Vite + Pinia + VueRouter + Element Plus)
前端·vue.js
李昊哲小课2 小时前
安装 npm/pnpm/yarn 换国内镜像 统一目录管理全局包+缓存
前端·缓存·npm·pnpm·yarn
挖稀泥的工人2 小时前
AI 打字跟随优化
前端·javascript·vue.js
jiayong232 小时前
第 11 课:把筛选条件同步到 URL
开发语言·前端·javascript