HarmonyOS 7 / API 26 AppFreeze 怎么提前拦:生命周期耗时、主线程任务和兜底超时怎么查
HarmonyOS 应用卡住,最麻烦的地方不是“慢”,而是开发时看起来只是偶发卡顿,到了线上才变成用户眼里的页面没反应。尤其是启动、回到前台、页面切换这几段,如果把耗时任务、网络等待、数据预热都压在主线程或生命周期里,最后很容易出现 AppFreeze 类问题。
这篇不把问题讲成一堆概念,直接拆一个检查办法:把启动链路里的任务先分层,再用一个小脚本把风险挡在提交前。它解决的不是所有性能问题,而是先把最容易被忽略的三类问题拎出来:
- 生命周期方法里做了太长的同步工作;
- 主线程上跑了本该拆出去的重活;
- 异步任务没有超时兜底,失败时页面一直等。
问题一般是怎么发生的
很多页面刚开始写的时候都很简单:进页面读配置、拉接口、查缓存、准备首屏数据。功能少的时候没问题,后面需求一多,就容易变成这样:
asynconWindowStageCreate(windowStage:window.WindowStage){awaitloadUserConfig()awaitrestoreCache()awaitpreloadFirstPageData()awaitinitAnalytics()windowStage.loadContent('pages/Index')}这段代码的问题不是语法,而是职责全挤在一起了。只要其中一步慢,页面就慢;只要其中一步卡住,后面的内容都跟着等。开发机网络好、数据少的时候不明显,换到低端设备、弱网、后台恢复场景,问题就会放大。
我的处理习惯是先把任务分成三类:
| 类型 | 应该怎么处理 | 原因 |
|---|---|---|
| 必须阻塞首屏的任务 | 只保留最小集合 | 首屏越短,冻结风险越低 |
| 可以延后的任务 | 页面出来后再跑 | 用户先看到内容,再补数据 |
| 可能卡住的任务 | 必须加超时和降级 | 不能让一个请求拖死整个页面 |
先做一个能复现问题的检查脚本
下面这个脚本不是替代系统诊断工具,而是用于提交前把明显风险拦住。它模拟三种情况:一个坏例子、一个正常启动例子、一个后台恢复边界例子。
constCASES=[{name:'bad-ui-heavy-work',taskMs:6200,lifecycleMs:1300,runsOnUiThread:true,hasTimeoutFallback:false,reportTag:'',},{name:'good-split-work',taskMs:900,lifecycleMs:260,runsOnUiThread:false,hasTimeoutFallback:true,reportTag:'appfreeze:startup-check',},{name:'edge-background-resume',taskMs:1800,lifecycleMs:420,runsOnUiThread:false,hasTimeoutFallback:true,reportTag:'appfreeze:resume-check',},];functioninspect(item){consterrors=[];constwarnings=[];if(item.runsOnUiThread&&item.taskMs>1000){errors.push('UI thread has long running work, split it before entering Ability lifecycle.');}if(item.lifecycleMs>1000){errors.push('Ability lifecycle section is too long, move IO/network/preload out of the critical path.');}if(!item.hasTimeoutFallback){errors.push('No timeout fallback. Once an async step is blocked, the page can stay frozen.');}if(!item.reportTag){warnings.push('No stable report tag. FaultLog/AppFreeze analysis will be hard to group later.');}return{...item,passed:errors.length===0,errors,warnings};}constresult=CASES.map(inspect);constsummary={total:result.length,passed:result.filter((item)=>item.passed).length,failed:result.filter((item)=>!item.passed).length,result,};console.log(JSON.stringify(summary,null,2));if(summary.failed>0)process.exitCode=1;本地跑出来的结果是这样的:
{"total":3,"passed":2,"failed":1}失败的就是bad-ui-heavy-work。它同时踩了三个点:主线程重活、生命周期耗时过长、没有超时兜底。这个结果比单纯说“注意性能优化”有用,因为它能告诉你到底是哪条规则不合格。
方案一:只把耗时任务挪出去,还不够
第一反应通常是把任务放到异步里:
aboutToAppear(){this.loadDataAsync()}asyncloadDataAsync(){constdata=awaitrequestFirstPage()this.items=data}这能减少同步阻塞,但问题还没彻底解决。因为请求如果一直不返回,页面仍然可能停在加载态。用户看到的不是“异步任务”,而是“这个页面是不是坏了”。
所以只做异步拆分不够,还要有超时、降级和可观测标记。
方案二:首屏最小化,耗时任务延后
更稳的写法是先让页面可见,再补充非关键数据:
@Stateloading:boolean=true@Stateitems:string[]=[]@StateerrorText:string=''aboutToAppear(){this.showSkeleton()this.loadFirstScreenWithTimeout()this.preloadLater()}showSkeleton(){this.loading=truethis.items=[]}asyncloadFirstScreenWithTimeout(){try{constdata=awaitwithTimeout(requestFirstPage(),1500)this.items=data}catch(err){this.errorText='数据暂时没回来,先展示本地兜底内容'this.items=getLocalFallback()}finally{this.loading=false}}asyncpreloadLater(){setTimeout(async()=>{awaitwarmupSecondPageCache()},300)}这里有几个关键点:
showSkeleton()先把页面状态落下来,不让用户面对空白;loadFirstScreenWithTimeout()只处理首屏必须的数据;preloadLater()延后做预热,别抢启动关键路径;- 请求失败时走本地兜底,不让页面无限等待。
方案三:把规则封装成提交前检查
如果只靠人记,很快就会漏。更实际的做法是把规则放进提交前检查或 CI。
我一般会把规则拆成这几项:
{"maxLifecycleMs":1000,"maxUiThreadTaskMs":1000,"requireTimeoutFallback":true,"requireReportTag":true}检查报告里不要只写“失败”,要写清楚哪一项失败:
bad-ui-heavy-work - UI thread has long running work - Ability lifecycle section is too long - No timeout fallback这样代码评审时就不会变成口水仗。谁超过阈值,谁补拆分;谁没有兜底,谁补超时;谁没有上报标记,谁补可观测字段。
为什么我更推荐第二种加第三种
| 方案 | 优点 | 问题 |
|---|---|---|
| 只异步拆分 | 改动小,见效快 | 请求卡住时仍然可能长时间等待 |
| 首屏最小化加超时兜底 | 用户先看到页面,失败也有退路 | 需要把状态拆清楚 |
| 提交前规则检查 | 能持续避免同类问题 | 需要维护阈值和报告 |
如果是要长期维护的 HarmonyOS 项目,我会选“首屏最小化 + 超时兜底 + 提交前检查”。它不是最省事的,但最能减少后面反复排查 AppFreeze、启动慢、回前台卡住这类问题。
最后怎么验收
我会用四个结果判断这次改动算不算稳:
- 首屏能在可接受时间内显示骨架或兜底内容;
- 慢请求不会让页面一直卡在加载态;
- 后台恢复时不会重复跑完整初始化;
- 检查脚本能稳定拦住主线程重活和无超时任务。
这类问题不要等线上日志出现后才处理。只要把生命周期、主线程任务和超时兜底这三件事提前拆清楚,大部分冻结类问题都能在开发阶段先压下去。