HarmonyOS APP《画伴梦工厂》开发第47篇-错误处理与容错设计——鸿蒙应用健壮性
第6.5篇:错误处理与容错设计——鸿蒙应用健壮性
难度:⭐⭐⭐ 高级
前置知识:第 3.1 ~ 3.9 篇 图像识别、AI 服务与网络请求封装
涉及源文件:products/default/src/main/ets/services/AIGenerationService.ets、products/default/src/main/ets/services/ImageRecognitionService.ets、products/default/src/main/ets/pages/Index.ets
概述
企业级应用不仅要"能跑",更要"稳得住"。在"画伴梦工厂"中,涉及系统相机调用、HTTP 网络请求、AI 模型推理、图片编解码、文件读写等多种高失败率操作。任何一个环节出错,若未妥善处理,轻则功能异常,重则应用闪退,对儿童用户极不友好。
本文将从"画伴梦工厂"的实际源码出发,系统梳理在 HarmonyOS 应用开发中构建 纵深防御(Defense in Depth) 错误处理体系的最佳实践,涵盖三层保护屏障、异步错误捕获、降级策略、资源清理与 UI 状态守卫。
一、三层防护模型:Try-Catch 边界 + 降级默认值 + 用户提示
项目的错误处理遵循经典的三层防护模型:
| 层级 | 职责 | 典型实现 |
|---|---|---|
| 第一层:Try-Catch 边界 | 捕获异常,防止崩溃扩散 | try { ... } catch (error) { ... } finally { ... } |
| 第二层:降级默认值 | 返回合理的兜底数据 | getFallbackResult()、pickText()、use mock data |
| 第三层:用户友好提示 | 将错误转化为用户可理解的消息 | showNotice()、getErrorMessage() |
在 Index.ets 的 saveCurrentVideo 方法中,三层防护清晰可见:
private async saveCurrentVideo(): Promise<void> {
if (this.exportBusy) return; // UI 守卫(前置防护)
this.exportBusy = true;
this.showNotice('正在准备保存视频'); // 操作前提示
try {
await VideoExportService.saveToLocal(...);
this.downloadedActive = true;
this.showNotice('视频已保存'); // 成功提示
} catch (error) {
this.showNotice('保存失败:' + this.getErrorMessage(error as Error)); // 错误提示
} finally {
this.exportBusy = false; // 资源/状态清理
}
}
这套模型的核心思想是:每一层都有明确的职责,层与层之间互不替代。Try-Catch 保证不崩溃,降级保证有数据可用,用户提示保证体验不中断。
二、异步错误捕获:async/await 与 try-catch 的黄金组合
在鸿蒙应用中,网络请求、图片处理和 AI 任务全是异步操作。async/await 语法让异步代码看起来像同步代码,但错误处理也必须配套跟上。
2.1 包裹整个异步链路
在 sendChatPrompt 方法中,整个异步操作被一个 try-catch 块包裹:
private async sendChatPrompt() {
const prompt = this.chatInput.trim();
if (prompt === '' || this.chatBusy) return;
this.chatBusy = true;
this.chatInput = '';
// ... 构建消息
try {
const generatedImage: GeneratedImage = await AIGenerationService.generateImage(prompt);
// ... 处理成功结果
} catch (error) {
this.showNotice('图片生成失败,请稍后重试');
} finally {
this.chatBusy = false;
}
}
关键要点:
- Try 块要足够大:切勿只在
await行加 try-catch,应将整个请求+处理逻辑包裹在内,这样才能捕获从请求发起、结果解析到状态更新的所有异常。 - Catch 块要足够细:给用户一个可操作的提示(“请稍后重试”),而非技术堆栈。
- Finally 块必须执行:无论成功还是失败,
chatBusy状态都必须恢复为false,否则 UI 会永久卡住。
2.2 异步错误传播链
在 AIGenerationService 中,generateVideo 方法调用了 createImg2VideoTask、pollImg2VideoTask、downloadVideo 等多个异步方法。每个子方法内部都使用 try-catch-finally,但错误会重新抛出(throw),逐层向上传播:
private static async createImg2VideoTask(base64: string): Promise<GeneratedVideoTask> {
const request = http.createHttp();
try {
// ... 发起请求、解析响应
if (response.responseCode < 200 || response.responseCode >= 300) {
throw new Error('图片上传接口返回异常:' + response.responseCode.toString());
}
// ... 继续解析
} catch (error) {
const safeError = error as Error;
AIGenerationService.logError('createImg2VideoTask failed: ' + AIGenerationService.getErrorMessage(safeError));
throw safeError; // 重新抛出,让上层处理
} finally {
request.destroy();
}
}
这种模式的好处是:每层只处理自己关心的清理逻辑(finally),决策逻辑集中在顶层调用者。顶层调用者(如 sendChatPrompt)根据业务场景决定是重试、降级还是提示用户。
三、BusinessError 格式转换与安全错误消息提取
在调用系统 API 或第三方接口时,抛出的错误类型多种多样——可能是 Error 对象、可能带 message 字段、也可能是一个无法直接显示的 JSON 对象。
项目中通过 getErrorMessage 方法对所有错误进行统一的消息提取:
// ImageRecognitionService.ets / AIGenerationService.ets
static getErrorMessage(error: Error): string {
if (error && error.message && error.message !== '') {
return error.message;
}
return JSON.stringify(error);
}
3.1 为什么要统一格式化?
| 错误来源 | 原始格式 | getErrorMessage 输出 |
|---|---|---|
throw new Error('网络超时') |
Error: 网络超时 |
网络超时 |
| JSON parse 失败 | SyntaxError: Unexpected token |
Unexpected token |
| HTTP 返回 401 | Error: 401 Unauthorized |
401 Unauthorized |
| 框架抛出系统异常 | 无 message 的对象 |
{"code":201,...} |
统一格式化的核心价值在于:无论什么错误结构,前端 UI 都能拿到一个可显示的字符串,不会因为 error.message 为空而显示 undefined。
3.2 Index.ets 中的对称实现
Index 页面中的 getErrorMessage 与 Service 层的实现完全一致,确保 UI 层和 Service 层的错误显示风格统一:
private getErrorMessage(error: Error): string {
if (error && error.message && error.message !== '') return error.message;
return JSON.stringify(error);
}
四、降级策略:Fallback Result 与兜底数据
当识别或生成服务完全不可用时,应用不能白屏或报错——它应该返回一个合理的默认值,让流程可以继续。
4.1 getFallbackResult:完整的兜底识别结果
ImageRecognitionService 中定义了 DEFAULT_RESULT 常量和 getFallbackResult 静态方法:
const DEFAULT_RESULT: DrawingRecognitionResult = {
protagonist: '小恐龙',
scene: '草地和太阳',
emotion: '快乐探险',
animationSuggestion: '跳跃、摇尾、看向太阳',
summary: '识别到儿童手绘中的主角、自然场景和明亮情绪,适合生成轻快的探险动画草稿。',
elements: [
{ name: '小恐龙', confidence: 96, color: '#D8F7EA' },
{ name: '太阳', confidence: 91, color: '#FFF0DD' },
{ name: '草地', confidence: 88, color: '#EAF8F0' },
{ name: '树木', confidence: 84, color: '#F1EDFF' }
]
};
static getFallbackResult(): DrawingRecognitionResult {
return {
protagonist: DEFAULT_RESULT.protagonist,
scene: DEFAULT_RESULT.scene,
// ... 每个字段都从 DEFAULT_RESULT 获取
};
}
这个方法的使用场景包括:
- 图片 URI 为空时直接返回兜底结果,不发起网络请求
- AI 返回内容缺失(如
protagonist为空)时,用默认值填充 - 模型接口异常时,作为候选降级方案
4.2 pickText:单字段的降级守卫
normalizeModelResult 方法使用 pickText 对模型返回的每个字段进行空值检查:
private static pickText(value: string | undefined, fallback: string): string {
if (value && value.trim() !== '') {
return value.trim();
}
return fallback;
}
return {
protagonist: ImageRecognitionService.pickText(parsed.protagonist, fallback.protagonist),
scene: ImageRecognitionService.pickText(parsed.scene, fallback.scene),
emotion: ImageRecognitionService.pickText(parsed.emotion, fallback.emotion),
// ...
};
这个简单的函数解决了一个常见痛点:AI 大模型的输出具有不确定性,返回的字段可能是 undefined、空字符串或纯空白。通过 pickText 配合 getFallbackResult 的默认值,实现了字段级别的容错。
4.3 clampConfidence:数值范围的守卫
对于置信度等数值字段,clampConfidence 确保数值始终在合法范围内:
private static clampConfidence(value: number | undefined): number {
if (value === undefined || Number.isNaN(value)) {
return 86; // 默认置信度
}
return Math.max(0, Math.min(100, Math.round(value)));
}
这种"有界取值"模式确保了即使 AI 返回了异常数据(如 confidence: -5 或 confidence: 250),界面上显示的进度条也不会出界。
4.4 图片压缩失败的降级
在 prepareUploadBase64 方法中,图片压缩失败时直接使用原图:
try {
const compressedBuffer = await AIGenerationService.compressImageBuffer(sourceBuffer);
// ... 使用压缩后的 base64
} catch (error) {
// 压缩失败,降级使用原图
const sourceBase64 = AIGenerationService.arrayBufferToBase64(sourceBuffer);
return sourceBase64;
}
五、重试守卫:可重试错误识别
在长时间的 AI 动画生成任务中,网络抖动是常态。pollImg2VideoTask 方法实现了自动重试机制,但并非所有错误都值得重试——认证错误(401)和资源不存在(404)就应该立即失败。
private static canRetryTaskQuery(message: string): boolean {
return message.indexOf('400') < 0 && message.indexOf('401') < 0 &&
message.indexOf('403') < 0 && message.indexOf('404') < 0;
}
5.1 重试逻辑的完整闭环
结合 canRetryTaskQuery 的轮询重试模式如下:
while (Date.now() - startedAt < MAX_SEEDANCE_WAIT_MS) {
let responseBody;
try {
responseBody = await AIGenerationService.queryImg2VideoTask(taskId);
} catch (error) {
lastErrorMessage = AIGenerationService.getErrorMessage(error as Error);
if (!AIGenerationService.canRetryTaskQuery(lastErrorMessage)) {
throw new Error(lastErrorMessage); // 不可重试,立即抛出
}
// 可重试,等待后继续
await AIGenerationService.sleep(POLL_INTERVAL_MS);
continue;
}
// ... 检查任务状态
}
| HTTP 状态码 | canRetryTaskQuery |
行为 |
|---|---|---|
| 400 Bad Request | ❌ 不可重试 | 请求格式错误,重试无意义 |
| 401 Unauthorized | ❌ 不可重试 | API Key 过期,需重新配置 |
| 403 Forbidden | ❌ 不可重试 | 无权限访问,重试无意义 |
| 404 Not Found | ❌ 不可重试 | 资源不存在 |
| 500+ / 网络超时 | ✅ 可重试 | 服务端临时故障,重试可能成功 |
5.2 超时兜底
轮询还有一重安全网——超时保护:
throw new Error('视频生成超时,请稍后重试' + (lastErrorMessage !== '' ? ',' + lastErrorMessage : ''));
MAX_SEEDANCE_WAIT_MS 被设置为 6 分钟,超过这个时间无论任务是否完成都终止轮询。这防止了用户在极端情况下面临无限等待的困境。
六、资源清理:Finally 块中的确定性释放
HarmonyOS 中的系统资源(HTTP 请求、PixelMap、ImageSource、ImagePacker 等)必须显式释放,否则会造成内存泄漏。
6.1 HTTP 请求的销毁
每个 HTTP 请求方法都使用 try-catch-finally 模式,确保 request.destroy() 一定执行:
private static async createSeedanceTask(...): Promise<string> {
const request = http.createHttp();
try {
// ... 发起请求和处理
} finally {
request.destroy(); // 无论成功还是失败,都要销毁请求
}
}
6.2 图片资源的链式释放
图片压缩涉及多个资源对象,释放顺序有严格要求——必须先释放子对象,再释放父对象:
private static async compressImageBuffer(sourceBuffer: ArrayBuffer): Promise<ArrayBuffer> {
const source: image.ImageSource = image.createImageSource(sourceBuffer);
let pixelMap: image.PixelMap | null = null;
const packer: image.ImagePacker = image.createImagePacker();
try {
// ... 解码、缩放、编码
} finally {
if (pixelMap !== null) {
await pixelMap.release(); // 先释放 PixelMap
}
await packer.release(); // 再释放 Packer
await source.release(); // 最后释放 ImageSource
}
}
6.3 文件句柄的关闭
文件操作也遵循同样的模式——内层 finally 保证文件关闭,外层 finally 保证资源销毁:
const file = fileIo.openSync(path, fileIo.OpenMode.READ_ONLY);
try {
// ... 文件读取操作
} finally {
fileIo.closeSync(file); // 一定关闭文件
}
七、UI 状态守卫:重复操作的预防
在 UI 层面,最典型的错误就是重复点击导致的并发操作。项目中通过布尔状态变量实现了简单的互斥守卫:
// 视频导出守卫
private async saveCurrentVideo(): Promise<void> {
if (this.exportBusy) return; // 导出中,忽略重复点击
this.exportBusy = true;
// ...
}
// 语音识别守卫
private async startVoiceInput(): Promise<void> {
if (this.voiceListening) { // 正在听,执行停止逻辑
this.voiceListening = false;
await this.voiceRecognitionService.stop();
return;
}
// ...
}
// AI 图片生成守卫
if (this.chatBusy) return;
this.chatBusy = true;
// ...
7.1 守卫的可视化反馈
更进一步的优化是,在 chatBusy 为 true 时,UI 层也给出视觉反馈:
Button(this.chatBusy ? '生成中' : '发送')
.backgroundColor(this.chatBusy ? '#A9A0D8' : this.brandPurple)
用户在界面上一眼就能看出当前操作的状态,既防止了重复操作,又消除了用户的不确定性。
八、通知系统:用户友好的错误呈现
所有的 catch 块最终都通过 showNotice 方法向用户展示错误信息:
private showNotice(text: string) {
this.noticeText = text;
}
而在 UI 模板中,NoticeBar 组件会根据 noticeText 非空自动渲染:
@Builder
private NoticeBar() {
if (this.noticeText !== '') {
Row() {
Text(this.noticeText)
.fontSize(12)
.fontColor('#FFFFFF')
.layoutWeight(1)
Text('x')
.onClick(() => { this.noticeText = ''; })
}
.backgroundColor('#D94C3D') // 红色警示
.borderRadius(18)
}
}
项目中的错误提示遵循几个原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 以用户为中心 | 不说技术术语,只说影响和操作 | “保存失败”,而非 “TypeError: Cannot read properties” |
| 可操作性 | 提示用户能做什么 | “请稍后重试” |
| 正向引导 | 即使失败也提供替代路径 | “已载入示例画作便于继续流程” |
| 一致性 | 所有错误走同一个展示通道 | 统一通过 showNotice → NoticeBar |
九、容错策略全景图
将上述所有模式综合起来,可以看到"画伴梦工厂"中的容错体系覆盖了代码执行的每一个关键节点:
用户操作
│
├── UI 状态守卫 ─────── exportBusy / chatBusy / recording
│
└── 进入异步操作
│
├── try:
│ ├── 参数校验(空值检查、格式检查)
│ ├── 网络请求 / AI 调用 / 本地 IO
│ └── 结果解析与数据清洗
│
├── catch(error):
│ ├── getErrorMessage() 格式化
│ ├── canRetryTaskQuery() 判断是否可重试
│ │ ├── 可重试 → 休眠后继续
│ │ └── 不可重试 → 抛出给上层
│ ├── pickText() / clampConfidence() 字段降级
│ ├── getFallbackResult() 整体降级
│ └── showNotice() 通知用户
│
└── finally:
├── request.destroy() HTTP 请求销毁
├── pixelMap.release() 图片资源释放
├── fileIo.closeSync() 文件句柄关闭
└── chatBusy = false 状态守卫还原
这九个字概括了项目的容错哲学:防得住(guard)、抓得住(try)、兜得住(fallback)、说得清(notice)、收得净(finally)。
十、最佳实践总结
以下是本文所有容错模式的速查表,供实际开发中参考:
| 模式 | 代码位置 | 适用场景 |
|---|---|---|
| UI 状态守卫 | if (this.exportBusy) return |
防止按钮重复点击、并发操作 |
| 同步 try-catch-finally | readImageAsBase64 |
文件读取、JSON 解析等同步操作 |
| 异步 try-catch-finally | saveCurrentVideo |
网络请求、AI 服务等异步操作 |
| 统一错误格式化 | getErrorMessage() |
将各种 Error 类型转为可显示字符串 |
| 字段级降级 | pickText() / clampConfidence() |
AI 输出字段缺失或越界 |
| 整体降级 | getFallbackResult() |
服务完全不可用时返回默认 UI 数据 |
| 重试守卫 | canRetryTaskQuery() |
HTTP 请求失败的重试决策 |
| 超时保护 | MAX_SEEDANCE_WAIT_MS |
长时间轮询的超时兜底 |
| 资源释放 | finally { request.destroy() } |
HTTP、PixelMap、ImageSource、文件句柄 |
| 用户通知 | showNotice() + NoticeBar |
所有错误的对外呈现 |
总结
本文从"画伴梦工厂"的实际源码出发,系统总结了在鸿蒙应用中构建错误处理与容错设计的方法论:
| 维度 | 核心思路 | 关键代码 |
|---|---|---|
| 三层防护 | try-catch 捕获 → 降级兜底 → 用户提示 | saveCurrentVideo、sendChatPrompt |
| 错误格式化 | 统一提取 error.message |
getErrorMessage() |
| 降级策略 | 字段级 + 整体级兜底数据 | pickText、getFallbackResult、clampConfidence |
| 重试机制 | 根据错误类型判断是否可重试 | canRetryTaskQuery |
| 资源清理 | finally 块中对称释放 | request.destroy、pixelMap.release |
| UI 守卫 | 布尔状态互斥锁 | exportBusy、chatBusy、recording |
| 用户通知 | 统一通道 + 可关闭的 NotificationBar | showNotice + NoticeBar |
错误处理不是"锦上添花"的功能,而是应用健壮性的基石。一个好的错误处理体系,要做到"悄无声息地处理可恢复错误,不失体面地展示不可恢复错误"——这正是"画伴梦工厂"代码中透露的工程智慧。
在下一篇中,我们将探讨跨页面通信与全局状态管理的最佳实践。
参考源码
本文所有代码均来自项目文件:
products/default/src/main/ets/pages/Index.ets— 首页主组件,包含saveCurrentVideo、sendChatPrompt、saveCurrentVideo、getErrorMessage、showNotice等 UI 守卫和错误处理示例products/default/src/main/ets/services/ImageRecognitionService.ets— 图像识别服务,包含getFallbackResult、pickText、clampConfidence、getErrorMessage等降级策略products/default/src/main/ets/services/AIGenerationService.ets— AI 生成服务,包含canRetryTaskQuery、轮询重试、finally资源清理等高级容错模式
更多推荐


所有评论(0)