第34篇|首次启动异常白屏:用启动账本和恢复页兜底
摘要:首次启动异常白屏,最怕没有页面、没有错误、没有日志。很多项目把配置读取、用户恢复、缓存迁移、远程开关都塞进首屏前,一步失败就停在白屏。更稳的做法是 WindowStage 先加载安全壳页面,启动账本记录阶段,必需任务短超时执行,失败时进入恢复页,非必需任务放到首屏后补。
我遇到过一个新安装首次启动白屏:开发机复现不了,用户清数据后必现。后来发现启动页等待本地配置迁移和远程开关,两者任何一个失败都没有兜底页面。白屏不是一个页面问题,而是启动链路没有安全出口。

这篇文章解决四个实际问题:


先把首屏前任务减到最少
| 加载本地默认配置 | 是 | 失败进入恢复页 |
| 用户会话恢复 | 可降级 | 首屏后补或进入未登录态 |
| 远程开关 | 否 | 使用本地默认值 |
| 缓存预热 | 否 | 首屏后后台执行 |
| 日志初始化 | 是但要轻量 | 失败不阻断页面 |
WindowStage 先加载安全壳
export class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/BootShellPage')
}
}
安全壳页面要足够简单,不依赖复杂业务数据。它负责显示启动中、恢复入口或跳转主页面。
启动账本记录阶段
export interface BootStepRecord {
name: string
startedAt: number
finishedAt: number
success: boolean
message: string
}
export class BootLedger {
private records: BootStepRecord[] = []
finish(name: string, startedAt: number, success: boolean, message: string): void {
this.records.push({ name, startedAt, finishedAt: Date.now(), success, message })
}
}
白屏排查最缺上下文。账本至少要知道卡在哪一步、耗时多久、失败原因是什么。
必需任务短超时执行
export interface StartupTask {
name: string
required: boolean
run(): Promise<void>
}
export class StartupService {
constructor(private readonly ledger: BootLedger) {}
async runRequiredTasks(tasks: StartupTask[]): Promise<boolean> {
for (const task of tasks.filter(item => item.required)) {
const startedAt = Date.now()
try {
await task.run()
this.ledger.finish(task.name, startedAt, true, '')
} catch (err) {
this.ledger.finish(task.name, startedAt, false, '启动任务失败')
return false
}
}
return true
}
}
首屏前任务失败要有明确结果,不要让 Promise 悬挂在启动页里。
恢复页给重试和清理入口
@Component
struct BootRecoveryView {
@State message: string = '启动失败,请重试'
retryAction?: () => void
clearAction?: () => void
build() {
Column({ space: 12 }) {
Text(this.message)
Button('重试启动').onClick(() => this.retryAction?.())
Button('清理本地缓存后重试').onClick(() => this.clearAction?.())
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
}
恢复页不是装饰,它给用户和排障人员一个明确出口。
首屏后任务不要阻断可用性
export class AfterFirstFrameTasks {
async run(tasks: StartupTask[]): Promise<void> {
for (const task of tasks.filter(item => !item.required)) {
try {
await task.run()
} catch (err) {
// 记录即可,不阻断主页面
}
}
}
}
远程开关、缓存预热、诊断上报都可以首屏后补。用户先看到可用页面,比卡在启动页更重要。
验证清单
启动失败要区分可恢复和不可恢复
恢复页也不能只有一个“重试”。不同失败需要不同出口。
| 配置读取失败 | 重试、清缓存 |
| 数据迁移失败 | 查看修复提示、备份后重试 |
| 远程开关失败 | 使用默认配置进入 |
| 账号恢复失败 | 进入未登录态 |
如果所有失败都只给“重试”,用户可能一直卡在同一个坏状态里。恢复页要把失败类型转成可执行动作。
启动账本要能落到日志里
export class BootLedgerReporter {
report(records: BootStepRecord[]): void {
records.forEach((item: BootStepRecord) => {
const cost = item.finishedAt – item.startedAt
console.info(`[boot] ${item.name} success=${item.success} cost=${cost} message=${item.message}`)
})
}
}
发布态不需要输出大量 debug,但启动失败时至少要有阶段、耗时和结果。否则白屏问题只能靠猜。
安全壳页面要少依赖资源和业务
BootShellPage 的目标是“最坏情况下也能显示”。它不应该依赖远程配置、复杂主题、用户资料或业务列表。图片也不要放太重,避免资源读取异常时连恢复页都显示不出来。最稳的安全壳只需要标题、状态文案、重试按钮和清理入口。等必需任务完成后,再进入真正首页。
安全壳自检可以按这三项做:
启动任务要有超时边界
有些白屏不是抛错,而是 Promise 一直不返回。必需任务也要有超时。
export async function withTimeout(task: Promise<void>, timeoutMs: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('startup task timeout')), timeoutMs)
task.then(() => {
clearTimeout(timer)
resolve()
}).catch((err) => {
clearTimeout(timer)
reject(err)
})
})
}
超时后进入恢复页,比一直停在启动动画里更容易排查。超时时间不要随便设很长,首屏前任务应该短而确定。
首次启动回归表
这些路径都要能看到页面和日志。只验证“正常启动一次”无法证明白屏问题已经修好。
交付前验收表
| 安全壳 | WindowStage 后先显示可见页面 |
| 必需任务失败 | 进入恢复页,不白屏 |
| 非必需任务失败 | 不阻断首页 |
| 启动日志 | 有阶段、耗时和失败原因 |
启动类问题要用清数据、新安装、异常迁移三类路径验证。只跑正常启动一次没有意义。
白屏修复后要保留失败样本
启动白屏最难的是复现。修复后建议保留三类失败样本:损坏配置、迁移抛错、远程配置超时。每类样本都应该能稳定进入恢复页,并在日志里看到对应阶段。这样下次启动链路再改时,可以把这些样本当回归用例,而不是重新等用户反馈。
boot regression samples:
– broken_local_config -> RecoveryPage
– migration_throw_error -> RecoveryPage with repair message
– remote_flag_timeout -> HomePage with default flags
这些样本不需要进入发布包,但要在开发或测试说明里留下构造方式。否则“白屏已修复”很容易退化成只验证了一次正常启动。
小结:首屏必须有可见出口
首次启动白屏要从启动链路修。WindowStage 先加载安全壳,启动账本记录阶段,必需任务短链路执行,失败进入恢复页,非必需任务首屏后补。这样即使失败,也不是无上下文白屏。
pair message
- remote_flag_timeout -> HomePage with default flags
这些样本不需要进入发布包,但要在开发或测试说明里留下构造方式。否则“白屏已修复”很容易退化成只验证了一次正常启动。
## 小结:首屏必须有可见出口
首次启动白屏要从启动链路修。WindowStage 先加载安全壳,启动账本记录阶段,必需任务短链路执行,失败进入恢复页,非必需任务首屏后补。这样即使失败,也不是无上下文白屏。






