HarmonyOS 7 API 26 AppFreeze 检查流程图

HarmonyOS 应用卡住,最麻烦的地方不是“慢”,而是开发时看起来只是偶发卡顿,到了线上才变成用户眼里的页面没反应。尤其是启动、回到前台、页面切换这几段,如果把耗时任务、网络等待、数据预热都压在主线程或生命周期里,最后很容易出现 AppFreeze 类问题。

这篇不把问题讲成一堆概念,直接拆一个检查办法:把启动链路里的任务先分层,再用一个小脚本把风险挡在提交前。它解决的不是所有性能问题,而是先把最容易被忽略的三类问题拎出来:

  • 生命周期方法里做了太长的同步工作;
  • 主线程上跑了本该拆出去的重活;
  • 异步任务没有超时兜底,失败时页面一直等。

问题一般是怎么发生的

很多页面刚开始写的时候都很简单:进页面读配置、拉接口、查缓存、准备首屏数据。功能少的时候没问题,后面需求一多,就容易变成这样:

async onWindowStageCreate(windowStage: window.WindowStage) {
  await loadUserConfig()
  await restoreCache()
  await preloadFirstPageData()
  await initAnalytics()
  windowStage.loadContent('pages/Index')
}

这段代码的问题不是语法,而是职责全挤在一起了。只要其中一步慢,页面就慢;只要其中一步卡住,后面的内容都跟着等。开发机网络好、数据少的时候不明显,换到低端设备、弱网、后台恢复场景,问题就会放大。

我的处理习惯是先把任务分成三类:

类型 应该怎么处理 原因
必须阻塞首屏的任务 只保留最小集合 首屏越短,冻结风险越低
可以延后的任务 页面出来后再跑 用户先看到内容,再补数据
可能卡住的任务 必须加超时和降级 不能让一个请求拖死整个页面

先做一个能复现问题的检查脚本

下面这个脚本不是替代系统诊断工具,而是用于提交前把明显风险拦住。它模拟三种情况:一个坏例子、一个正常启动例子、一个后台恢复边界例子。

const CASES = [
  {
    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',
  },
];

function inspect(item) {
  const errors = [];
  const warnings = [];
  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 };
}

const result = CASES.map(inspect);
const summary = {
  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()
}

async loadDataAsync() {
  const data = await requestFirstPage()
  this.items = data
}

这能减少同步阻塞,但问题还没彻底解决。因为请求如果一直不返回,页面仍然可能停在加载态。用户看到的不是“异步任务”,而是“这个页面是不是坏了”。

所以只做异步拆分不够,还要有超时、降级和可观测标记。

方案二:首屏最小化,耗时任务延后

更稳的写法是先让页面可见,再补充非关键数据:

@State loading: boolean = true
@State items: string[] = []
@State errorText: string = ''

aboutToAppear() {
  this.showSkeleton()
  this.loadFirstScreenWithTimeout()
  this.preloadLater()
}

showSkeleton() {
  this.loading = true
  this.items = []
}

async loadFirstScreenWithTimeout() {
  try {
    const data = await withTimeout(requestFirstPage(), 1500)
    this.items = data
  } catch (err) {
    this.errorText = '数据暂时没回来,先展示本地兜底内容'
    this.items = getLocalFallback()
  } finally {
    this.loading = false
  }
}

async preloadLater() {
  setTimeout(async () => {
    await warmupSecondPageCache()
  }, 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、启动慢、回前台卡住这类问题。

最后怎么验收

我会用四个结果判断这次改动算不算稳:

  • 首屏能在可接受时间内显示骨架或兜底内容;
  • 慢请求不会让页面一直卡在加载态;
  • 后台恢复时不会重复跑完整初始化;
  • 检查脚本能稳定拦住主线程重活和无超时任务。

这类问题不要等线上日志出现后才处理。只要把生命周期、主线程任务和超时兜底这三件事提前拆清楚,大部分冻结类问题都能在开发阶段先压下去。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐