查看: 122|回复: 3

鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
上一篇文章聊了生命周期和启动框架,这次来说说应用怎么结束。别小看这个问题:用户按返回键该退出还是挂后台?应用崩溃怎么查原因?有些场景需要应用自己重启怎么搞?处理不好用户真的会骂人。

我之前踩过一个坑:用户反馈“改了语言,重启应用又变回去”。查了半天发现应用退出时数据没持久化,下次启动又从默认配置读。后来才搞清楚onDestroy在某些场景下根本不走,代码白写了。

这篇就把应用退出、异常原因排查、应用重启三块串起来讲,都是实战一定会遇到的东西。

一、应用退出:不止一种方式

鸿蒙应用的退出机制分三大类,每一类处理方式不同。

1. 用户主动退出

最常见的是返回键退出。默认按返回键只是把UIAbility挂到后台,不销毁。如果想直接退出,可以重写onBackPressed方法:
  1. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  2. import { hilog } from '@kit.PerformanceAnalysisKit';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. export default class EntryAbility extends UIAbility {
  5.   onBackPressed(): boolean {
  6.     this.context.terminateSelf().then(() => {
  7.       hilog.info(0x0000, 'Demo', 'terminateSelf成功');
  8.     }).catch((err: BusinessError) => {
  9.       hilog.error(0x0000, 'Demo', `terminateSelf失败: ${err.code}, ${err.message}`);
  10.     });
  11.     return true; // 返回true表示消费事件,不再默认挂后台
  12.   }
  13. }
复制代码
注意:onBackPressed返回true表示事件被消费,系统不做默认处理;返回false则交给系统挂后台。

多任务清理(上滑卡片或点清除全部)时,onDestroy是保证触发的,可以在这里做数据保存。但在Dock栏关闭应用(PC或平板)时,onDestroy不保证触发,千万别依赖它保存关键数据。

2. 应用主动退出

通过代码调用terminateSelf,场景包括检测到致命错误主动终止、完成核心任务后自动关闭等。系统会触发onDestroy,走正常生命周期销毁。
  1. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. export default class EntryAbility extends UIAbility {
  4.   private doExit(): void {
  5.     this.context.terminateSelf().then(() => {
  6.       console.info('应用主动退出成功');
  7.     }).catch((err: BusinessError) => {
  8.       console.error(`退出失败: ${err.code}, ${err.message}`);
  9.     });
  10.   }
  11. }
复制代码

3. 系统强制终止

这类情况最坑爹——不会触发onDestroy。包括JavaScript崩溃、C++空指针异常、系统内存紧张回收进程、温度过高保护、应用升级时清理旧进程等。你的关键数据保存逻辑如果写在onDestroy里,碰上系统强杀等于白写。

那怎么办?关键数据要实时持久化,别等到退出时才存。

二、获取异常退出原因

既然系统强杀不通知,那至少得知道上次为什么挂了吧。鸿蒙从API 9开始就在LaunchParam里提供了lastExitReason,从API 18开始还能拿到更详细的内存信息。

在UIAbility的onCreate里读取:
  1. import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
  2. export default class MyAbility extends UIAbility {
  3.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  4.     const reason = launchParam.lastExitReason;
  5.     const exitMsg = launchParam.lastExitMessage;
  6.     console.info(`上次退出原因: ${reason}, 详情: ${exitMsg}`);
  7.     if (reason === AbilityConstant.LastExitReason.JS_ERROR) {
  8.       // 触发崩溃上报
  9.     } else if (reason === AbilityConstant.LastExitReason.RESOURCE_CONTROL) {
  10.       // 检查是否需要清理缓存
  11.     }
  12.   }
  13. }
复制代码

注意:PERFORMANCE_CONTROL(值6)已废弃,官方建议用RESOURCE_CONTROL替代。

LastExitDetailInfo(API 18+)可以获取退出时的进程状态:
  1. import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
  2. export default class MyAbility extends UIAbility {
  3.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  4.     if (launchParam.lastExitDetailInfo) {
  5.       const info = launchParam.lastExitDetailInfo;
  6.       console.info(`PID: ${info.pid}, 进程名: ${info.processName}`);
  7.       console.info(`RSS: ${info.rss} KB, PSS: ${info.pss} KB`);
  8.       // 如果RSS或PSS过高,大概率是被内存管控杀的
  9.       if (info.rss > 500000) { // 500MB
  10.         console.warn('内存占用过高,建议优化');
  11.       }
  12.     }
  13.   }
  14. }
复制代码
exitSubReason也有很多门道:100~102是户外模式相关,103是后台CPU高负载,107/108是内存超限。看到107基本可以确定是PSS内存超限杀的。

实战中我们可以做一个崩溃检测和上报:
  1. import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
  2. export default class EntryAbility extends UIAbility {
  3.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  4.     this.checkLastExit(launchParam);
  5.   }
  6.   private checkLastExit(launchParam: AbilityConstant.LaunchParam): void {
  7.     const reason = launchParam.lastExitReason;
  8.     if (reason === AbilityConstant.LastExitReason.NORMAL || reason === AbilityConstant.LastExitReason.UNKNOWN) {
  9.       return;
  10.     }
  11.     console.warn(`异常退出,原因: ${reason}, 消息: ${launchParam.lastExitMessage}`);
  12.     if (launchParam.lastExitDetailInfo) {
  13.       const info = launchParam.lastExitDetailInfo;
  14.       if (info.rss > 400000 || info.pss > 300000) {
  15.         console.warn(`内存异常: RSS=${info.rss}KB, PSS=${info.pss}KB`);
  16.       }
  17.     }
  18.     this.reportExitReason(reason, launchParam.lastExitMessage);
  19.   }
  20.   private reportExitReason(reason: number, message: string): void {
  21.     // 上报到统计分析平台
  22.   }
  23. }
复制代码

三、应用重启:什么时候需要自己重启

有些场景需要主动重启:下载新资源需重新加载、关键配置变化需从初始状态开始、应用状态出问题想刷新。鸿蒙从API 12开始提供两种主动重启方式。

不保留窗口的重启:用ApplicationContext.restartApp,API 12+。重启过程中当前窗口关闭,用户会看到桌面再拉起。适合不介意闪一下的场景。
  1. import { common, Want } from '@kit.AbilityKit';
  2. function restartApp(context: common.ApplicationContext): void {
  3.   let want: Want = {
  4.     bundleName: 'com.example.myapp',
  5.     abilityName: 'EntryAbility'
  6.   };
  7.   try {
  8.     context.restartApp(want);
  9.   } catch (err) {
  10.     console.error(`重启失败: ${err.code}, ${err.message}`);
  11.   }
  12. }
复制代码

保留窗口的重启:用UIAbilityContext.restartApp,API 22+。重启过程中窗口保留,体验更连贯。
  1. import { common, Want } from '@kit.AbilityKit';
  2. async function restartWithWindow(context: common.UIAbilityContext): Promise<void> {
  3.   let want: Want = {
  4.     bundleName: 'com.example.myapp',
  5.     abilityName: 'EntryAbility'
  6.   };
  7.   try {
  8.     await context.restartApp(want);
  9.   } catch (err) {
  10.     console.error(`重启失败: ${err.code}, ${err.message}`);
  11.   }
  12. }
复制代码
如果重启后想跳转到另一个页面,把abilityName改成其他Ability即可。

两种方式共同的限制:只能在主线程调用,应用必须处于焦点状态,3秒内不能重复调用。后台应用不能自己重启,我的做法是先切到前台再调,或者给用户弹提示。

元服务重启:用abilityManager.restartSelfAtomicService,API 20+,支持元服务的免安装更新。

此外还有被动重启:appRecovery模块。开启后应用挂了系统自动恢复并重启Ability,甚至能恢复到崩溃前的页面状态。
  1. import { appRecovery } from '@kit.AbilityKit';
  2. export default class EntryAbility extends UIAbility {
  3.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  4.     appRecovery.enableAppRecovery(
  5.       appRecovery.RestartFlag.ALWAYS_RESTART,
  6.       appRecovery.SaveOccasionFlag.SAVE_OCCASION_UI_STATE
  7.     );
  8.   }
  9. }
复制代码
注意:如果崩溃是脏数据导致的,自动恢复后会再次崩溃,可能死循环,不适合所有场景。

四、实际踩过的坑

坑1:onDestroy做保存,系统强杀全丢了
我之前把主题色保存逻辑写在onDestroy里,结果系统内存紧张直接杀进程,用户改完颜色再打开还是旧的。正确做法是数据改了立刻存,不依赖onDestroy。

坑2:restartApp在子线程调用直接报错
异步任务里调restartApp,报错:非主线程无权限。后来改用EventHub发消息到主线程调。

坑3:LastExitDetailInfo为空
这个字段从API 18开始,而且是可选的,只有异常退出时才可能有。取值时一定要做空判断。

坑4:SIGNAL退出的迷惑行为
看到大量lastExitReason === SIGNAL(值10)不要慌,这表示系统发kill信号,但不一定是坏事(如系统整理内存、应用升级)。要结合exitSubReason或时间戳判断。

坑5:3秒内重复调用restartApp被忽略
解决方案是UI层做防抖,保存上次调用时间,小于4秒就忽略。

总结

应用退出这件事坑不少,几个要点:用户主动退出走onDestroy可放心做清理;系统强制终止不触发onDestroy,关键数据要实时持久化;lastExitReason+lastExitDetailInfo是排查崩溃的利器,但注意空判断;主动重启分两种:流畅体验用UIAbilityContext.restartApp(API 22+),不在意闪一下用ApplicationContext.restartApp(API 12+);重启接口需在主线程调用,3秒内不能重复,应用必须在前台。

最后,建议定期查看异常退出数据。我们上线上报功能后才发现,大量退出是因为内存问题,直接推动了内存优化工作。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas

这篇文章非常实用,尤其是关于 `onDestroy` 不保证触发的提醒,确实是个容易踩的坑。我之前也遇到过类似问题,不过是把用户设置存到了本地数据库,但由于依赖 `onDestroy` 写入,结果系统强杀时数据就丢了,后来改成了每次修改配置就立刻持久化才解决。 另外你提到的 `lastExitReason` 和 `lastExitDetailInfo` 在 API 18 后的增强很关键,对于线上排查崩溃和内存问题帮助很大。我之前没注意到 `PERFORMANCE_CONTROL` 已废弃,感谢提醒。 关于应用重启,你提到后面会讲,期待具体实现——比如 `terminateSelf` 后再调 `startAbility` 时需要注意什么,或者是否有更优雅的重启方式?
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas

楼主这篇干货很实在,尤其是 onDestroy 不保证触发那点,之前我也掉过类似的坑,后来把关键数据的存盘逻辑都改成了实时写,才彻底解决。关于 lastExitReason 和内存信息的获取,目前我在 API 18 的设备上还没测试过,想问下楼主:如果 JS_ERROR 触发后,应用下次启动时 lastExitReason 是必能获取到吗?还是说有概率丢失?另外,RESOURCE_CONTROL 这个值除了清理缓存,有没有更好的应对策略?
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas

这篇总结太及时了,正好最近被系统强杀搞得焦头烂额,之前一直以为onDestroy是铁定会走的,结果线上丢了好几次用户配置才反应过来。特别是“关键数据要实时持久化”这句,点醒了我。还有lastExitReason那个API,之前不知道可以这样查崩溃原因,以后排查效率能高不少。感谢分享!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-24 20:47 , Processed in 0.028813 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部