【口算王|13】HarmonyOS ArkTS 应用启动链路实战:从 EntryAbility 到首屏加载保持窗口与路由稳定

【口算王|13】HarmonyOS ArkTS 应用启动链路实战:从 EntryAbility 到首屏加载保持窗口与路由稳定

公开边界:本文仅保留 HarmonyOS Stage 模型、UIAbility、WindowStage、AppStorage 与 ArkUI 页面装载的通用工程方法。所有代码均为重新编写的公开化示例,不包含本地项目路径、包名、签名、业务数据或私有源码。未执行的构建、真机与性能测试不会写成已通过。

应用启动故障很少只表现为"打不开"。更常见的是首屏能出来,但窗口状态不稳定:底部按钮第一次绘制时贴住手势区,几百毫秒后又向上跳;折叠屏改变窗口宽度后,页面仍沿用旧断点;窗口销毁了,避让区监听还持有旧对象;页面加载失败只剩一块系统背景,日志里却没有可定位的错误。

这些问题的共同点是责任落在 UIAbilityWindowStage、窗口对象和 ArkUI 页面之间。页面只看到一个安全区高度,却不知道这个值来自哪个窗口;数据服务只需要 Context,却不应该持有页面实例;断点系统要覆盖整个 Ability 生命周期,又不能在每个页面重复注册。启动链路要稳定,关键不是把所有初始化都塞进 onCreate(),而是让每种资源由正确的生命周期所有者管理。

本文基于口算类示例应用 示例工程 的公开化示例代码,复核 EntryAbility.etsStartupDataService.etsAdaptiveLayoutRuntime.etsIndex.etsTopBar.etsPracticePage.etsmodule.json5 与应用配置。包名 com.example.mathapp 是本文草稿核验使用的唯一标记。项目采用 Stage 模型,SDK 版本应以实际工程与官方文档为准,设备范围包含 phone、tablet 和 2in1;本文讨论的 Ability、WindowStage、AppStorage 与避让区方法面向 HarmonyOS 5.0 及以上工程。

本文重点回答:

  • onCreate()onWindowStageCreate()onWindowStageDestroy()onDestroy() 分别拥有哪类资源;
  • 本地数据、Tab 状态、断点监听和窗口避让区为什么不能混在页面中初始化;
  • 如何把窗口像素值转换为页面可消费的共享状态;
  • avoidAreaChange 为什么既要监听,也要在两级销毁路径中解除;
  • loadContent() 的回调怎样成为首屏加载的第一条诊断证据;
  • 冷启动、窗口变化、前后台切换和销毁重建应该如何验证。

一、先建立 Ability 生命周期账本

Stage 模型里的 UIAbility 是一段业务窗口会话的运行时边界。口算王的 EntryAbility 同时拥有四类资源:应用 Context、本地共享状态、媒体查询监听器、主窗口与避让区回调。它们的创建时机和销毁时机并不相同。

可以先用资源账本明确边界:

资源 创建位置 使用范围 释放位置
Preferences 访问入口 onCreate() Ability 内业务数据 随进程与服务生命周期
AppStorage 初始键 onCreate() 全部 ArkUI 页面 由运行时管理
断点媒体查询 onCreate() phone/tablet/2in1 页面 onDestroy()
主窗口引用 onWindowStageCreate() 当前 WindowStage onWindowStageDestroy()
避让区监听回调 onWindowStageCreate() 当前主窗口 WindowStage 与 Ability 销毁路径
首屏内容 loadContent() 当前窗口页面树 WindowStage 销毁

这张表能避免两个常见错误。一是页面在 aboutToAppear() 中重复注册全局断点监听,导致每次进出页面都增加一组回调;二是 Ability 在 onCreate() 中读取尚未创建的主窗口,把窗口依赖提前到错误的阶段。

二、项目入口确实使用 Stage 模型

entry/build-profile.json5 明确声明:

"targets": [

]

模块配置把 EntryAbility 设置为入口:

"name": "entry",

"deviceTypes": [

"2in1"

"abilities": [

"srcEntry": "./ets/entryability/EntryAbility.ets",

]

这意味着启动诊断要沿 Stage 模型查找:系统创建 EntryAbility,Ability 创建 WindowStage,WindowStage 加载 ArkUI 页面。不能套用旧模型中 Page Ability 的生命周期假设,也不能把 EntryAbility.ets 当作普通工具类。

项目配置还表明兼容和目标版本都在 HarmonyOS 6.0 系列。文章中的 API 不是凭空拼接的兼容示例,而是来自该实际工程;如果迁移到 HarmonyOS 5.x 项目,应以本地 SDK 签名和官方文档为准复核具体重载与类型。

三、onCreate 只初始化不依赖窗口的能力

口算王在 onCreate() 中完成浅色模式、本地数据、共享索引和断点系统初始化:

launchParam: AbilityConstant.LaunchParam

this.context.getApplicationContext()

hilog.error(

'Failed to set colorMode. Cause: %{public}s',

}

AppStorage.setOrCreate ('currentTabIndex', 0)

AppStorage.setOrCreate ('navigationIndicatorHeightPx', 0)

这些动作有一个共同点:不需要 window.Window 实例。Context 在 Ability 创建后可用,Preferences 可以同步读取,AppStorage 可以先建立默认键,媒体查询也可以注册初始断点。

这种安排防止页面第一次绑定时遇到"键不存在"。例如 Index 声明:

@StorageLink('topAvoidAreaHeightPx') topAvoidAreaHeightPx: number = 0

即使窗口避让区尚未读取完成,页面也先拿到 0 作为可预测回退值。随后 Ability 写入真实高度,所有 @StorageLink 订阅者一起刷新,不需要页面各自调用窗口 API。

四、本地数据初始化为什么放在 Ability 层

StartupDataService.init() 使用 Ability Context 获取 Preferences,并把记录写入 AppStorage:

): void {

{ name: 'app_preferences' }

const favStr = StartupDataService.prefs

.getSync('wrongRecords', '\[\]') as string

JSON.parse(favStr) as FavoriteRecord\[\]

'wrongRecords',

}

如果每个页面自行读取 Preferences,会出现三类分歧:

  1. 首页计数、收藏列表和"我的"统计可能在不同时间拿到不同快照;
  2. 页面需要知道存储名称和字段名,UI 层被迫承担持久化协议;
  3. 页面销毁重建会重复解析同一份数据。

Ability 层只负责触发初始化,真正的存储细节仍封装在 StartupDataService。这符合"Context 由生命周期边界提供,业务页面只消费窄接口"的结构。页面不保存 Context,也不直接调用 preferences.getPreferencesSync()

当前实现使用同步读取,所以 onCreate() 返回前数据已经写入 AppStorage。若未来换成 RDB 迁移或异步文件读取,应该把数据管理器改为显式启动任务,并增加 startupState,而不是在 Ability 内调用异步函数后立即假定成功。

五、断点系统属于 Ability,而不是单个页面

口算王的 AdaptiveLayoutRuntime.register() 创建三组媒体查询:

mediaquery.matchMediaSync('(width<=600vp)')

AdaptiveLayoutRuntime.lgListener =

AdaptiveLayoutRuntime.smListener.on('change', result => {

AdaptiveLayoutRuntime.mdListener.on('change', result => {

AdaptiveLayoutRuntime.lgListener.on('change', result => {

}

初始匹配完成后,系统把断点广播到:

AppStorage.setOrCreate ('currentBreakpoint', bp)

首页、题库页、挑战页、详情页和统计页都通过 @StorageLink 消费同一个断点。因此注册一次即可覆盖整棵页面树。把注册动作放在某个首页组件中,会让二级页面直接启动或窗口重建时缺少断点来源。

对应的清理在 onDestroy()

this.mainWindow.off(

)

}

unregister() 分别对三组监听执行 off('change')。Ability 销毁后不再接收窗口宽度变化,避免静态监听器持有旧运行环境。

六、窗口相关操作必须等到 WindowStage 创建

主窗口在 onWindowStageCreate() 中获取:

(data: window.AvoidAreaOptions) => void = undefined

this.mainWindow = windowStage.getMainWindowSync()

this.avoidAreaCallback =

data.type ===

) {

}

this.avoidAreaCallback

AppStorage.setOrCreate (

)

0

}

这里先主动读取一次,再注册变化监听。如果只监听 avoidAreaChange,首次页面构建可能一直拿默认 0,直到设备姿态或系统栏状态发生变化才更新;如果只读取一次,旋转、分屏、窗口缩放或手势导航变化后又会过期。

"先拉取,再订阅"是窗口状态同步的通用模式:

  1. 获取当前主窗口;
  2. 立即计算当前值;
  3. 保存回调引用;
  4. 监听后续变化;
  5. 销毁时用同一回调解除。

回调被保存成字段非常关键。解除监听通常需要事件名与原回调匹配;如果注册时直接写匿名函数,销毁阶段没有同一个函数引用可用。

七、避让区要同时看导航指示区和系统区域

updateNavigationIndicatorHeight() 读取两种避让区:

AppStorage.setOrCreate (

)

0

}

window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR

this.mainWindow.getWindowAvoidArea(

const topHeight =

navigationArea.visible

const systemHeight =

AppStorage.setOrCreate (

)

Math.max(navigationHeight, systemHeight)

顶部使用系统区域的 topRect.height,底部取导航指示区与系统区域底部高度的最大值。取最大值而不是相加,是因为两类区域可能描述重叠空间;直接相加会把页面内容推得过高。

这里保存的是像素值,因为窗口 API 返回 px。页面真正计算布局时再通过当前 UIContext 转成 vp:

Sizes.BOTTOM_NAV_MIN_PADDING,

)

这个转换位置是合理的:Ability 只广播窗口原始事实,组件根据自己的 UIContext 解释密度,并叠加业务最小间距。不要在 Ability 中硬编码一个假定密度,把所有设备都换算成同一个 vp。

八、一个安全区值如何驱动多个页面

navigationIndicatorHeightPx 不只服务首页。公开化示例代码中,以下页面都会消费它:

页面 使用方式
Index.ets 增加底部 Tab 容器高度与 padding
BankDetailPage.ets 保证底部操作区离开系统手势区
CategoryPage.ets 在列表末尾增加可滚动空白
ExamResultPage.ets 提高底部按钮区域
PracticePage.ets 调整答题操作栏与状态页面底部
SearchPage.ets 为搜索结果尾部留出安全空间
SettingsPage.ets 调整清理按钮和页面底部 padding

这种共享方式比每页读取窗口更稳。窗口监听只有一份,转换和最小间距由组件按场景决定。页面不会互相覆盖窗口回调,也不会因为路由切换漏掉监听。

顶部值则由公共 TopBar.ets 和练习、搜索等页面订阅。当前 Index.topSafePadding() 返回 0,说明首页根布局并没有直接使用保存的顶部高度;顶部适配由具体页面或公共顶栏负责。文章只能描述真实消费路径,不能因为存在 topAvoidAreaHeightPx 就声称所有页面都已自动适配状态栏。

九、异常回退不能让首屏失去布局

窗口获取、监听注册和避让区读取都放在 try/catch 中。失败时写回 0:

'topAvoidAreaHeightPx',

AppStorage.setOrCreate (

)

'testTag',

)

回退为 0 不代表忽略安全区。页面的 Math.max(Sizes.BOTTOM_NAV_MIN_PADDING, ...) 仍保留至少 28vp 的底部距离。这样窗口 API 异常时,布局会退回保守间距,而不是直接把操作按钮压到最底边。

稳定启动需要区分"可降级"和"不可继续":

失败点 当前处理 是否可继续
设置浅色模式失败 记录 error 可以,系统模式接管
Preferences 解析失败 使用空记录与默认设置 可以
获取避让区失败 高度回退 0,页面保留最小间距 可以
注册窗口监听失败 记录 warn 可以,但窗口变化不会动态更新
loadContent() 失败 记录 error 并返回 不可进入 ArkUI 首屏

最后一项属于硬失败,因为没有页面树可显示。它的日志必须比普通 warn 更醒目,也应在发布包冒烟测试中作为阻断项。

十、loadContent 是首屏加载的明确边界

窗口准备完成后,EntryAbility 加载 Splash:

(err) => {

DOMAIN,

JSON.stringify(err)

}

'testTag',

}

这个回调把"窗口已经创建"和"ArkUI 内容已经加载"分开。启动日志建议按以下顺序查看:

Succeeded in loading the content.

如果第二条都没有,问题在 Ability 或 WindowStage 创建前;如果第二条存在但第三条失败,重点检查页面注册、资源、构建产物和 loadContent 目标;如果第三条成功但品牌页不跳首页,才进入 Splash 和路由层排查。

不要用 Splash 的 aboutToAppear() 日志替代 loadContent 证据。前者只有在组件成功构建后才可能执行,无法解释页面树为什么没加载。

十一、窗口销毁与 Ability 销毁是两级清理

当前工程在 onWindowStageDestroy() 中解除窗口监听并清空字段:

this.mainWindow.off(

)

this.avoidAreaCallback = undefined

onDestroy() 也有一次防御性解除,并注销断点系统。这种两级结构对应两个不同事实:

  • WindowStage 销毁后,旧窗口不能再被访问;
  • Ability 销毁后,应用级媒体查询也必须停止。

重复执行 off() 是否安全要以实际 API 行为为准。当前代码通过 mainWindow && avoidAreaCallback 判断引用存在;WindowStage 销毁路径清空字段后,后续 onDestroy() 不会再次调用窗口解除,因此避免了重复操作。

更完整的封装可以把窗口清理提取为一个幂等方法:

this.mainWindow.off(

)

this.avoidAreaCallback = undefined

onWindowStageDestroy(): void {

onDestroy(): void {

}

这属于维护性增强,不是对示例实现已有方法的描述。它减少两处清理逻辑未来发生差异的风险。

十二、前后台回调当前只记录日志

口算王实现:

DOMAIN,

'Ability onForeground'

onBackground(): void {

'testTag',

)

当前应用是本地口算训练,没有后台网络、定位、音频常驻或定时同步,因此前后台切换不需要启动额外任务。只记录生命周期日志与实际能力一致。

如果以后增加语音朗读,页面或语音服务应在后台时停止正在播放的会话;如果增加在线同步,也要根据官方后台任务机制设计,不能把长时间请求直接塞进 onBackground()。本文不把当前空回调描述成已实现后台恢复能力。

十三、窗口监听中的数据类型边界

Ability 将安全区存成 number,但这个 number 代表 px;页面使用时转成 vp。单靠类型系统无法表达单位,因此命名承担了契约:

后缀 Px 很重要。若把字段命名成 bottomPadding,其他开发者可能直接当 vp 使用,在高密度设备上产生明显误差。

可以进一步用接口集中表示窗口事实:

bottom: number

function selectBottomInsetPx(

): number {

? navigation.bottomRect.height

system.visible

return Math.max(navigationHeight, systemHeight)

纯函数便于输入构造和边界测试,窗口 API 调用仍留在 Ability。这样能验证"不可见区域返回 0""两个区域取最大值""负值或异常值如何处理"等规则,而不必每次依赖真实设备姿态。

十四、多设备启动要同时验证断点与避让区

项目声明 phone、tablet 和 2in1,说明启动链路不能只在手机竖屏验一次。建议覆盖:

场景 断点预期 避让区预期 首屏检查
手机竖屏 sm 底部手势区有效 Tab 不贴底
展开折叠屏 md 根据系统栏变化 内容重新排布
大平板 lg 可能无手机式手势区 不额外抬高过多
2in1 窗口缩小 lg -> md -> sm 随窗口变化 无跳变和裁切
横竖屏切换 按宽度重算 顶底区域更新 顶栏与按钮可达

还要注意当前 Index 判断:

this.currentBp === 'md' ||

// 底部导航

}

断点系统只产生这三个值,因此 else 侧栏分支不可达。Ability 的断点广播本身会更新,但 Index 的消费条件没有区分设备形态。测试时如果只看到 currentBreakpoint = lg 就认为平板适配完成,会漏掉这个真实逻辑问题。

十五、启动链路的诊断顺序

遇到白屏、布局跳动或监听异常时,可以按以下顺序排查:

  1. module.json5 是否指向正确 EntryAbility;
  2. onCreate() 是否完成数据与 AppStorage 初始化;
  3. AdaptiveLayoutRuntime.register() 是否写入初始断点;
  4. onWindowStageCreate() 是否拿到主窗口;
  5. 首次避让区读取是否成功;
  6. avoidAreaChange 是否只注册一次;
  7. loadContent() 是否回调成功;
  8. Splash 是否替换到 Index;
  9. 页面是否把 px 转成 vp;
  10. WindowStage 与 Ability 销毁时监听是否解除。

每一步都有独立证据。不要在看到底部按钮错位时立即给所有页面加 30vp 固定 padding,那会掩盖窗口监听或单位转换的真正问题。

十六、实机与发布包验证清单

生命周期

  • 冷启动日志顺序正确;
  • 进入后台和回到前台各触发一次对应回调;
  • 旋转、分屏、窗口缩放不会重复注册监听;
  • WindowStage 销毁后字段被清空;
  • Ability 销毁后断点监听注销。

页面布局

  • 首页底部 Tab 至少保留业务最小间距;
  • 练习页操作栏不进入系统手势区;
  • 搜索、分类、设置等长页面最后一个操作可滚动到安全区域;
  • 顶部公共栏在状态栏下方可读;
  • px 到 vp 的转换只在 UIContext 可用的组件侧执行。

异常路径

  • 模拟窗口避让区读取失败,页面仍可显示;
  • Preferences 内容损坏时回退为空记录和默认设置;
  • loadContent() 失败能在 hilog 中定位;
  • 首屏资源丢失时构建或运行日志明确;
  • 快速创建销毁窗口不会留下旧回调。

发布门槛

  • 使用签名 release 包完成安装;
  • 从桌面冷启动并走到首页;
  • 完成一次题库选择与答题;
  • 切换横竖屏或调整窗口;
  • 回到桌面后重新进入;
  • 正常卸载且无第三方安装依赖。

这些验证对应 AppGallery 对安装、启动、运行、稳定性和多设备布局的基本要求。只有编辑器预览正常,不足以证明 WindowStage 生命周期在发布包中可靠。

十七、常见问题与修复方向

现象 根因候选 优先修复
首屏底部栏先贴底后上跳 首次避让区读取晚于页面构建 获取窗口后先主动更新,再监听变化
横屏后仍用旧间距 只读取一次,没有监听变化 注册 avoidAreaChange
多次进出后回调重复 页面或窗口重复注册 Ability 单点注册并保存回调引用
WindowStage 销毁后报窗口错误 仍持有旧 mainWindow 销毁时解除监听并清空字段
平板断点为 lg 但仍是底部栏 Index 条件覆盖全部断点 修正断点消费分支
某些设备间距过大 两类底部区域被相加 取最大值并按可见性判断
不同密度设备布局不一致 把 px 直接当 vp 在页面 UIContext 中转换
Splash 前出现白屏 loadContent() 或资源失败 先读 WindowStage 加载回调
数据页首次显示空列表 数据初始化晚于页面 在 Ability 边界建立 ready 状态

十八、总结:让资源跟着生命周期走

口算王的 EntryAbility 把启动链路拆得比较清楚:onCreate() 处理不依赖窗口的应用状态,onWindowStageCreate() 获取窗口、读取并监听避让区,然后加载首屏;页面通过 AppStorage 消费断点和安全区;onWindowStageDestroy() 释放窗口引用,onDestroy() 注销应用级断点监听。

这套结构最值得复用的不是某个 API,而是资源所有权:

  • Context 驱动的数据初始化归 Ability 触发;
  • Preferences 细节归数据服务;
  • 媒体查询归应用级断点系统;
  • 主窗口和避让区回调归 WindowStage 生命周期;
  • px 到 vp 的解释归页面 UIContext;
  • 页面布局只消费共享事实,不重复监听窗口。

示例实现仍有可完善点:窗口清理可以抽成幂等方法,前后台回调暂时只有日志,Index 的断点消费让侧栏分支不可达,顶部安全区也不是所有根页面都直接使用。这些边界被明确记录后,后续优化才能基于证据推进,而不是把启动问题笼统归因于"设备兼容性"。

本文部分内容由 AI 辅助整理,所有实现边界、版本信息、代码片段与结论均依据上述本地源码复核。

当前启动链路:从桌面入口到业务首屏

源码显示,系统先依据模块清单创建 EntryAbility;onCreate 初始化用户数据、共享状态和断点系统;窗口阶段创建后注册避让区监听,并装载 SplashPage。SplashPage 到 Index 的实际跳转属于后续页面职责,不能仅凭 EntryAbility 推断为已经成功。

启动职责分层:配置、生命周期、状态与页面

启动稳定性不是一个回调的责任。配置层决定入口和页面注册,Ability 层管理上下文、窗口与监听,状态层准备用户数据和 AppStorage,页面层负责 SplashPage 与 Index 的可见切换。分层检查可以把"白屏"进一步定位为入口、窗口、数据或页面问题。

建议实现:把启动过程变成可观察状态机

示例实现已把日志、用户数据、共享状态、断点系统、避让区和页面装载放在正确的生命周期附近,但日志并不等于启动状态。更稳的做法是定义 idle、preparing、windowReady、contentReady、degraded 和 failed 六类状态,并记录每个状态的进入时间、错误类型和恢复动作。StartupDataService 初始化失败时,首屏可以使用明确默认值进入降级状态;SplashPage 跳转失败时,应保留可见页面并提供重试,而不是只留下后台日志。

状态机需要坚持单向推进和幂等初始化。onCreate 可能只执行一次,但页面恢复、窗口重建和测试桩会让初始化路径重复出现;StartupDataService、AdaptiveLayoutRuntime 和 avoidAreaChange 监听都应能安全地判断"已初始化"或"已注册"。释放路径同样要与注册路径成对,避免窗口销毁后继续持有回调。这里给出的是建议设计,不能据此宣称当前工程已经实现。

启动回归不能只验证"看见首页"

验收时至少覆盖首次安装冷启动、已有用户数据冷启动、后台恢复、进程被系统回收后的重建、窗口尺寸变化和底部导航区域变化。每个场景分别观察启动窗口连续性、SplashPage 是否可交互、Index 是否只进入一次、五个 Tab 状态是否稳定、避让区是否刷新,以及退出后监听和断点系统是否完成释放。

性能数据必须来自真实测量。可以记录 onCreate 起点、StartupDataService 完成、onWindowStageCreate、loadContent 回调、SplashPage 首帧和 Index 首次可交互的时间戳,但在没有真机采样之前,不应写出冷启动毫秒数、通过率或设备覆盖结论。本文没有执行这些测试,因此只提供验证方法,不报告未发生的结果。

排查表:按启动阶段寻找第一处真实失败

现象 首查位置 可验证证据 不应直接推断
点击图标后无页面 module.json5、EntryAbility 日志 mainElement、srcEntry、onCreate 是否到达 不能直接归因于 Index
启动页出现后停住 SplashPage 跳转逻辑 路由 Promise、错误分支、目标页注册 不能仅凭截图断言数据服务失败
首屏内容为空 StartupDataService、AppStorage 初始化结果、默认值、页面读取时机 空数据不等于读取异常
底部内容被遮挡 avoidAreaChange、px2vp 转换 避让区类型、可见性、高度更新 不应固定写死某个设备高度
返回前台状态错乱 onForeground、页面状态恢复 生命周期顺序和状态快照 日志出现不代表恢复完成

排查时先找到第一个没有满足契约的阶段,再向下游追踪。这样可以避免把所有启动问题都归结为"路由不稳定",也能防止为了修复一个页面问题而修改入口清单、权限或签名配置。

可迁移示例:用公开接口组织启动链路

下面代码用于说明职责分配,名称和数据均为通用示例,不是任何本地项目源码。

复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "pages": "$profile:main_pages"
  }
}

模块清单只负责声明入口和页面清单,不应承载数据初始化逻辑。

复制代码
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}enum StartupPhase {
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  Idle = 'idle',
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  Preparing = 'preparing',
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  WindowReady = 'windowReady',
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  ContentReady = 'contentReady',
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  Degraded = 'degraded',
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}  Failed = 'failed'
#enum StartupPhase {
  Idle = 'idle',
  Preparing = 'preparing',
  WindowReady = 'windowReady',
  ContentReady = 'contentReady',
  Degraded = 'degraded',
  Failed = 'failed'
}}

显式阶段比单个布尔值更容易定位卡在数据、窗口还是页面装载阶段。

复制代码
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}export default class EntryAbility extends UIAbility {
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}  onCreate(): void {
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}    StartupDataService.initialize(this.context)
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}  }
#export default class EntryAbility extends UIAbility {
  onCreate(): void {
    AppStorage.setOrCreate<StartupPhase>('startupPhase', StartupPhase.Preparing)
    AppStorage.setOrCreate<number>('bottomAvoidHeightPx', 0)
    StartupDataService.initialize(this.context)
  }
}}

onCreate 只触发不依赖窗口的初始化,并先建立页面可消费的默认状态。

复制代码
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}onWindowStageCreate(stage: window.WindowStage): void {
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}  stage.loadContent('pages/SplashPage', (error) => {
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}    AppStorage.set('startupPhase',
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}  })
#onWindowStageCreate(stage: window.WindowStage): void {
  stage.loadContent('pages/SplashPage', (error) => {
    AppStorage.set('startupPhase',
      error.code === 0 ? StartupPhase.ContentReady : StartupPhase.Failed)
  })
}}

loadContent 的回调必须进入状态模型,失败时才能显示可恢复界面。

复制代码
#function normalizeAvoidHeight(px: number, ui: UIContext): number {
  if (!Number.isFinite(px) || px <= 0) return 0
  return Math.max(0, ui.px2vp(px))
}function normalizeAvoidHeight(px: number, ui: UIContext): number {
#function normalizeAvoidHeight(px: number, ui: UIContext): number {
  if (!Number.isFinite(px) || px <= 0) return 0
  return Math.max(0, ui.px2vp(px))
}  if (!Number.isFinite(px) || px <= 0) return 0
#function normalizeAvoidHeight(px: number, ui: UIContext): number {
  if (!Number.isFinite(px) || px <= 0) return 0
  return Math.max(0, ui.px2vp(px))
}  return Math.max(0, ui.px2vp(px))
#function normalizeAvoidHeight(px: number, ui: UIContext): number {
  if (!Number.isFinite(px) || px <= 0) return 0
  return Math.max(0, ui.px2vp(px))
}}

窗口 API 返回像素值,页面布局使用前要校验并转换为 vp。

复制代码
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}function openHomeAfterReady(): void {
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}  const phase = AppStorage.get<StartupPhase>('startupPhase')
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}    router.replaceUrl({ url: 'pages/Index' })
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}  }
#function openHomeAfterReady(): void {
  const phase = AppStorage.get<StartupPhase>('startupPhase')
  if (phase === StartupPhase.ContentReady || phase === StartupPhase.Degraded) {
    router.replaceUrl({ url: 'pages/Index' })
  }
}}

启动页只在内容就绪或明确降级后替换路由,避免重复压栈。

AI 辅助声明

本文在人工复核公开接口与通用启动职责后/,使用 AI 辅助整理结构、润色表达并生成配图;未执行的构建、真机、性能与异常恢复测试均未写成已通过。

相关推荐
万物智能信息科技4 小时前
LVDS屏幕输出桌面—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
知潮网5 小时前
HarmonyOS 7正式发布:华为分享远程直传无距离限制,还能和iPhone、Apple Watch互联
华为·iphone·harmonyos
OH_TPC7 小时前
HarmonyOS APP开发---“好物优选“电商导购App,需要用到这个库
华为·harmonyos·鸿蒙
lqj_本人18 小时前
Flutter 三方库「flutter_ble_peripheral」的鸿蒙化适配指南
flutter·华为·harmonyos
lqj_本人19 小时前
Flutter 三方库「flutter-dualscreen」的鸿蒙化适配指南
flutter·华为·harmonyos
熊猫钓鱼>_>21 小时前
【SenseNova U1.5 Lite实战】鸿蒙校园工具开发者适配原生统一多模态大模型全记录
人工智能·华为·ai·harmonyos·媒体·sensenova
在人间耕耘21 小时前
鸿蒙7「互动卡片」实测:桌面上多了个“记一笔“按钮 快的很
华为·harmonyos
RUNIONE1 天前
合亿RUNIONE 三防平板电脑|手持工业平板 UA810 开源鸿蒙系统,与国产生态深度融合
harmonyos·手持工业平板·手持工业平板厂家
●VON1 天前
鸿蒙跨平台框架怎么选?从真实需求比较 Flutter、React Native、KMP/CMP 与 Web 路线
flutter·react native·harmonyos