稳定性案例

本文档对典型稳定性问题进行匿名化整理,重点说明故障现象、归类方式、分析路径和修复思路。

1. 应用异常退出案例

案例 1:实例销毁后异步回调继续执行导致 SIGSEGV

现象

  • 应用在页面切换或实例销毁后偶现闪退。
  • FaultLog 显示 SIGSEGV,崩溃点位于实例相关回调链路。

分析

排查发现,异步任务提交时直接捕获了对象自身引用,后续任务执行时对象已经销毁,最终在回调中访问失效实例,形成典型的 UAF 或悬空引用访问。

修复思路

  • 不在异步回调中直接持有失效风险高的强引用。
  • 将回调改为弱引用或先校验实例有效性后再执行。
  • 在实例销毁阶段及时解绑回调和后续任务。

案例 2:释放顺序不当导致原生对象使用已失效资源

现象

  • 应用在退出、重建或清理阶段出现 SIGSEGV。
  • 日志无法直接指向业务逻辑,更像底层对象状态异常。

分析

进一步对调用栈和内存行为分析后,发现问题出在对象释放顺序错误。上层实例销毁后,下层调度器或运行时仍被访问,导致在反向依赖尚未清理完成时触发非法访问。

修复思路

  • 梳理依赖链,先释放依赖上游对象,再释放被依赖对象。
  • 对销毁顺序增加断言和空值保护。
  • 避免在析构过程中继续触发外部回调。

案例 3:接口返回路径不完整导致 SIGTRAP

现象

  • 应用启动阶段即闪退。
  • 崩溃信号为 SIGTRAP,调用栈落在某个原生接口调用后。

分析

问题最终定位为接口声明要求返回对象自身或状态值,但实际实现遗漏了返回路径。调用方继续基于异常返回结果执行,最终触发断言或陷阱信号。

修复思路

  • 保证接口声明与实现一致。
  • 补齐所有分支的返回路径。
  • 对调用方增加异常状态保护,避免继续沿错误结果执行。

案例 4:异常穿透 noexcept 边界导致 SIGABRT

现象

  • 运行过程中出现 SIGABRT。
  • 崩溃栈表面落在通用运行时或标准库终止逻辑中。

分析

分析后发现,真实根因并不在当前栈顶函数本身,而是某个声明为不抛异常的函数内部调用了可能抛出异常的逻辑,异常穿透到 noexcept 边界后触发 terminate,最终表现为 SIGABRT。

修复思路

  • 清理不合理的 noexcept 声明。
  • 在边界函数内捕获并转换异常。
  • 重新确认真实业务异常来源,而不是只看崩溃栈最上层。

案例 5:JS 对象访问缺少判空导致 JS Crash

现象

  • 应用退出,日志提示 TypeError。
  • 报错信息为读取 undefined 或 null 对象属性失败。

分析

问题通常发生在页面测量、布局回调或异步数据回填场景中。代码假设对象必然存在,但实际在边界情况下对象为空,导致运行时抛出未处理异常。

修复思路

  • 在对象属性访问前增加判空保护。
  • 对异步数据和可选字段建立默认值。
  • 将高风险渲染逻辑拆分为可降级分支。

案例 6:初始化阶段参数类型错误触发 JS Crash

现象

  • 页面初始化失败,首屏空白或应用退出。
  • 日志显示文件操作或模块初始化阶段抛出 JS 异常。

分析

分析发现,代码把目录路径、非法对象或不符合约定的参数传入了只接受文件对象或特定结构的接口,导致初始化阶段直接报错。

修复思路

  • 在初始化前校验输入参数类型。
  • 对目录、文件、对象句柄等不同输入做显式分流。
  • 将初始化逻辑中的异常改为可观测、可降级的失败路径。

2. 应用冻屏案例

案例 7:锁顺序不一致导致线程死锁

现象

  • 应用不退出,但界面完全无响应。
  • 冻结日志显示多个线程长期等待,主线程无法继续推进。

分析

主线程和业务线程对同一组共享资源采用了不同的加锁顺序,最终形成循环等待。由于进程仍然存在,因此表面上是“卡死”,本质上是典型死锁。

修复思路

  • 统一锁顺序。
  • 缩小锁作用域,避免持锁执行回调。
  • 对高风险同步路径改成无锁或异步串行化模型。

案例 8:主线程执行耗时创建逻辑导致页面卡死

现象

  • 页面打开时明显卡顿,严重时被判定为应用无响应。
  • trace 中可见主线程持续停留在某个页面创建或节点构建过程。

分析

问题出在页面首次渲染阶段存在大量同步构建和高频回调,导致主线程长时间被占用,输入事件和生命周期推进都无法及时完成。

修复思路

  • 将重计算和重构建逻辑移出主线程关键路径。
  • 避免在页面创建阶段触发异常高频的组件生命周期回调。
  • 对大型组件做分段创建或惰性初始化。

3. 内存异常案例

案例 9:内存持续增长最终触发 OOM

现象

  • 应用运行一段时间后内存持续升高,最终崩溃或被系统回收。
  • 堆分析显示对象总量和占用持续增长,没有回落到基线。

分析

问题不一定来自单次大对象分配,更常见的是页面切换、组件重建或长期运行过程中对象不断累积,但销毁路径没有真正释放,最终放大为 OOM。

修复思路

  • 建立内存基线并做阶段性对比。
  • 抓取前后堆快照,识别持续存活对象。
  • 优先检查页面退出后仍存活的上下文、缓存和实例对象。

4. 资源泄漏案例

案例 10:ArkTS 侧监听未注销导致对象长期存活

现象

  • 页面退出后内存没有回落。
  • 快照中仍能看到与页面相关的上下文对象和回调链路存活。

分析

根因是模块注册了环境或生命周期回调,但在销毁阶段没有执行 off 或反注册,导致回调闭包持续持有上下文对象,形成稳定的泄漏链。

修复思路

  • 所有 on 注册都必须有成对的 off。
  • 在模块销毁阶段集中清理回调和监听器。
  • 对长期对象持有链做快照回归检查。

案例 11:Native 实例未释放导致长期内存泄漏

现象

  • 多次进入退出同一功能后,Native 内存持续增长。
  • Allocation 视图中可见多个同类实例残留。

分析

调用栈分析表明,实例创建流程被多次执行,但调度器、组件注册表或运行时关联对象没有在退出时同步释放,最终导致 Native 侧对象累计。

修复思路

  • 建立实例创建与销毁的对账关系。
  • 对调度器、注册表、上下文和任务队列逐项检查释放闭环。
  • 在同步等待路径中避免因为线程阻塞而跳过清理流程。

5. 从案例中提炼的通用经验

综合这些案例,可以提炼出几个高频判断规律:

  1. 销毁后崩溃,优先检查回调、弱引用和对象释放顺序。
  2. 无响应不等于没日志,重点看主线程、关键线程和 trace。
  3. 首屏或初始化异常,优先检查参数契约、返回路径和异常边界。
  4. 长时间运行后问题放大,优先检查监听器、实例、线程和缓存是否真正释放。
  5. 看到表面栈顶并不等于找到根因,很多稳定性问题需要回到生命周期和线程模型才能解释清楚。