鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas
上一篇文章聊了生命周期和启动框架,这次来说说应用怎么结束。别小看这个问题:用户按返回键该退出还是挂后台?应用崩溃怎么查原因?有些场景需要应用自己重启怎么搞?处理不好用户真的会骂人。我之前踩过一个坑:用户反馈“改了语言,重启应用又变回去”。查了半天发现应用退出时数据没持久化,下次启动又从默认配置读。后来才搞清楚onDestroy在某些场景下根本不走,代码白写了。
这篇就把应用退出、异常原因排查、应用重启三块串起来讲,都是实战一定会遇到的东西。
一、应用退出:不止一种方式
鸿蒙应用的退出机制分三大类,每一类处理方式不同。
1. 用户主动退出
最常见的是返回键退出。默认按返回键只是把UIAbility挂到后台,不销毁。如果想直接退出,可以重写onBackPressed方法:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onBackPressed(): boolean {
this.context.terminateSelf().then(() => {
hilog.info(0x0000, 'Demo', 'terminateSelf成功');
}).catch((err: BusinessError) => {
hilog.error(0x0000, 'Demo', `terminateSelf失败: ${err.code}, ${err.message}`);
});
return true; // 返回true表示消费事件,不再默认挂后台
}
}
注意:onBackPressed返回true表示事件被消费,系统不做默认处理;返回false则交给系统挂后台。
多任务清理(上滑卡片或点清除全部)时,onDestroy是保证触发的,可以在这里做数据保存。但在Dock栏关闭应用(PC或平板)时,onDestroy不保证触发,千万别依赖它保存关键数据。
2. 应用主动退出
通过代码调用terminateSelf,场景包括检测到致命错误主动终止、完成核心任务后自动关闭等。系统会触发onDestroy,走正常生命周期销毁。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
private doExit(): void {
this.context.terminateSelf().then(() => {
console.info('应用主动退出成功');
}).catch((err: BusinessError) => {
console.error(`退出失败: ${err.code}, ${err.message}`);
});
}
}
3. 系统强制终止
这类情况最坑爹——不会触发onDestroy。包括JavaScript崩溃、C++空指针异常、系统内存紧张回收进程、温度过高保护、应用升级时清理旧进程等。你的关键数据保存逻辑如果写在onDestroy里,碰上系统强杀等于白写。
那怎么办?关键数据要实时持久化,别等到退出时才存。
二、获取异常退出原因
既然系统强杀不通知,那至少得知道上次为什么挂了吧。鸿蒙从API 9开始就在LaunchParam里提供了lastExitReason,从API 18开始还能拿到更详细的内存信息。
在UIAbility的onCreate里读取:
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
export default class MyAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const reason = launchParam.lastExitReason;
const exitMsg = launchParam.lastExitMessage;
console.info(`上次退出原因: ${reason}, 详情: ${exitMsg}`);
if (reason === AbilityConstant.LastExitReason.JS_ERROR) {
// 触发崩溃上报
} else if (reason === AbilityConstant.LastExitReason.RESOURCE_CONTROL) {
// 检查是否需要清理缓存
}
}
}
注意:PERFORMANCE_CONTROL(值6)已废弃,官方建议用RESOURCE_CONTROL替代。
LastExitDetailInfo(API 18+)可以获取退出时的进程状态:
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
export default class MyAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.lastExitDetailInfo) {
const info = launchParam.lastExitDetailInfo;
console.info(`PID: ${info.pid}, 进程名: ${info.processName}`);
console.info(`RSS: ${info.rss} KB, PSS: ${info.pss} KB`);
// 如果RSS或PSS过高,大概率是被内存管控杀的
if (info.rss > 500000) { // 500MB
console.warn('内存占用过高,建议优化');
}
}
}
}
exitSubReason也有很多门道:100~102是户外模式相关,103是后台CPU高负载,107/108是内存超限。看到107基本可以确定是PSS内存超限杀的。
实战中我们可以做一个崩溃检测和上报:
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.checkLastExit(launchParam);
}
private checkLastExit(launchParam: AbilityConstant.LaunchParam): void {
const reason = launchParam.lastExitReason;
if (reason === AbilityConstant.LastExitReason.NORMAL || reason === AbilityConstant.LastExitReason.UNKNOWN) {
return;
}
console.warn(`异常退出,原因: ${reason}, 消息: ${launchParam.lastExitMessage}`);
if (launchParam.lastExitDetailInfo) {
const info = launchParam.lastExitDetailInfo;
if (info.rss > 400000 || info.pss > 300000) {
console.warn(`内存异常: RSS=${info.rss}KB, PSS=${info.pss}KB`);
}
}
this.reportExitReason(reason, launchParam.lastExitMessage);
}
private reportExitReason(reason: number, message: string): void {
// 上报到统计分析平台
}
}
三、应用重启:什么时候需要自己重启
有些场景需要主动重启:下载新资源需重新加载、关键配置变化需从初始状态开始、应用状态出问题想刷新。鸿蒙从API 12开始提供两种主动重启方式。
不保留窗口的重启:用ApplicationContext.restartApp,API 12+。重启过程中当前窗口关闭,用户会看到桌面再拉起。适合不介意闪一下的场景。
import { common, Want } from '@kit.AbilityKit';
function restartApp(context: common.ApplicationContext): void {
let want: Want = {
bundleName: 'com.example.myapp',
abilityName: 'EntryAbility'
};
try {
context.restartApp(want);
} catch (err) {
console.error(`重启失败: ${err.code}, ${err.message}`);
}
}
保留窗口的重启:用UIAbilityContext.restartApp,API 22+。重启过程中窗口保留,体验更连贯。
import { common, Want } from '@kit.AbilityKit';
async function restartWithWindow(context: common.UIAbilityContext): Promise<void> {
let want: Want = {
bundleName: 'com.example.myapp',
abilityName: 'EntryAbility'
};
try {
await context.restartApp(want);
} catch (err) {
console.error(`重启失败: ${err.code}, ${err.message}`);
}
}
如果重启后想跳转到另一个页面,把abilityName改成其他Ability即可。
两种方式共同的限制:只能在主线程调用,应用必须处于焦点状态,3秒内不能重复调用。后台应用不能自己重启,我的做法是先切到前台再调,或者给用户弹提示。
元服务重启:用abilityManager.restartSelfAtomicService,API 20+,支持元服务的免安装更新。
此外还有被动重启:appRecovery模块。开启后应用挂了系统自动恢复并重启Ability,甚至能恢复到崩溃前的页面状态。
import { appRecovery } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
appRecovery.enableAppRecovery(
appRecovery.RestartFlag.ALWAYS_RESTART,
appRecovery.SaveOccasionFlag.SAVE_OCCASION_UI_STATE
);
}
}
注意:如果崩溃是脏数据导致的,自动恢复后会再次崩溃,可能死循环,不适合所有场景。
四、实际踩过的坑
坑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秒内不能重复,应用必须在前台。
最后,建议定期查看异常退出数据。我们上线上报功能后才发现,大量退出是因为内存问题,直接推动了内存优化工作。
Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas
这篇文章非常实用,尤其是关于 `onDestroy` 不保证触发的提醒,确实是个容易踩的坑。我之前也遇到过类似问题,不过是把用户设置存到了本地数据库,但由于依赖 `onDestroy` 写入,结果系统强杀时数据就丢了,后来改成了每次修改配置就立刻持久化才解决。 另外你提到的 `lastExitReason` 和 `lastExitDetailInfo` 在 API 18 后的增强很关键,对于线上排查崩溃和内存问题帮助很大。我之前没注意到 `PERFORMANCE_CONTROL` 已废弃,感谢提醒。 关于应用重启,你提到后面会讲,期待具体实现——比如 `terminateSelf` 后再调 `startAbility` 时需要注意什么,或者是否有更优雅的重启方式?Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas
楼主这篇干货很实在,尤其是 onDestroy 不保证触发那点,之前我也掉过类似的坑,后来把关键数据的存盘逻辑都改成了实时写,才彻底解决。关于 lastExitReason 和内存信息的获取,目前我在 API 18 的设备上还没测试过,想问下楼主:如果 JS_ERROR 触发后,应用下次启动时 lastExitReason 是必能获取到吗?还是说有概率丢失?另外,RESOURCE_CONTROL 这个值除了清理缓存,有没有更好的应对策略?Re: 鸿蒙应用退出与崩溃排查:onDestroy陷阱、lastExitReas
这篇总结太及时了,正好最近被系统强杀搞得焦头烂额,之前一直以为onDestroy是铁定会走的,结果线上丢了好几次用户配置才反应过来。特别是“关键数据要实时持久化”这句,点醒了我。还有lastExitReason那个API,之前不知道可以这样查崩溃原因,以后排查效率能高不少。感谢分享!
页:
[1]