在电商大促版本灰度期间,客诉群收到核心用户反馈:点击下单按钮后画面卡住不动,几秒钟后应用闪退。这类问题无法在本地测试机复现,属于典型的主线程阻塞导致的冻屏(AppFreeze)。它与常规 Crash 不同,不是内存越界或空指针,而是主线程长时间阻塞,UI 无法渲染、用户输入无法响应,最终被系统看门狗(Watchdog)强制终止。过去依赖系统默认崩溃日志,等到应用被杀后拿到墓碑日志,延迟较大,案发现场可能已经被覆盖。
HarmonyOS 7.0(API 26)在 Performance Analysis Kit 中提供两项能力:应用冻屏告警事件订阅 AppFreeze Warning,以及端云配合的应用灰度日志高保真采集,即 HiRetrieval 与 FaultLog 联动。目标是在主线程卡顿达到 3 秒时立即收到告警,并借助端云协同通道,把现场高保真堆栈和轨迹日志拉回云端 APM 监控平台。原文实战中,这套方案在灰度期捕获了超过 80% 难以复现的主线程超时问题。
一、AppFreeze 判定与 HiAppEvent 订阅
AppFreeze 本质是系统 Watchdog 对应用主线程健康度检查,是时间累积过程。主线程阻塞 3 秒触发 THREAD_BLOCK_3S 告警;到 6 秒抛 THREAD_BLOCK_6S 卡死并退出。应用层监控依赖 HiAppEvent 和 FaultLog。HarmonyOS 7.0 对底层进行了深度重构与接口扩充。7.0 之前只能监听 hiAppEvent.domain.OS 下的 APP_CRASH 或全量 APP_FREEZE;7.0 可精确捕获 warning 级事件,不再等进程即将死亡时才上报。
核心 API 是 @ohos.hiAppEvent 的 addWatcher。通过 appEventFilters 指定 domain 与事件类型,triggerCondition 控制回调时机,onReceive 接收事件。在 onReceive 中应尽量轻量,只提取 external_log、reason 等关键参数,把文件读取和上传交给后台任务。
二、HiRetrieval 与 FaultLog 端云配合
灰度日志采集关键是按需回捞。全量实时上报会消耗带宽和存储。端云配合机制分三步:端侧静默留存,将轨迹日志、网络请求写入本地环形缓冲区或轻量文件;条件触发采集,当 HiAppEvent 触发 THREAD_BLOCK_3S 等告警时,当前时间点前后日志成为高保真案发现场;指令下发或主动上报,端侧压缩日志包,并附带系统生成的 external_log(通常位于 /data/log/faultlog/),统一提交云端。
工程结构如下:- entry/src/main/ets/
- ├── entryability
- │ └── EntryAbility.ets
- ├── pages
- │ └── Index.ets
- ├── common
- │ ├── config
- │ │ └── MonitorConfig.ets
- │ └── utils
- │ ├── FileUtil.ets
- │ └── Logger.ets
- └── monitor
- ├── AppFreezeWatcher.ets
- ├── LogRetrievalTask.ets
- └── CloudUploader.ets
复制代码
三、核心实现
先定义监控阈值与云端模拟接口:- export class MonitorConfig {
- public static readonly UPLOAD_URL: string = 'https://apm-cloud.company.com/api/v1/fault/upload';
- public static readonly ENABLE_FREEZE_WARNING: boolean = true;
- }
复制代码
在 AppFreezeWatcher 中注册订阅器,利用过滤器拦截 OS 域下的故障事件。收到外部 faultLogPath 后,不在回调里做重活:- import { hiAppEvent } from '@kit.PerformanceAnalysisKit';
- import { LogRetrievalTask } from './LogRetrievalTask';
- export class AppFreezeWatcher {
- public static initWatcher(): void {
- try {
- let watcher: hiAppEvent.Watcher = {
- name: 'CloudSyncFreezeWatcher',
- appEventFilters: [
- {
- domain: hiAppEvent.domain.OS,
- eventTypes: [hiAppEvent.EventType.FAULT]
- }
- ],
- triggerCondition: {
- row: 1
- },
- onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
- for (let eventGroup of appEventGroups) {
- for (let eventInfo of eventGroup.appEventInfos) {
- let eventParams = eventInfo.params as Record<string, Object>;
- let faultLogPath = eventParams['external_log'] as string;
- let exceptionReason = eventParams['reason'] as string;
- if (faultLogPath) {
- LogRetrievalTask.executeTask(eventInfo.name, exceptionReason, faultLogPath);
- }
- }
- }
- }
- };
- hiAppEvent.addWatcher(watcher);
- } catch (error) {
- console.error('[AppFreezeWatcher] 注册监控器失败: ' + JSON.stringify(error));
- }
- }
- }
复制代码
为了验证告警,可在页面中主动制造长耗时同步计算,阻塞主线程 8 秒。该函数仅用于测试,实际业务中严禁这样写:- @Entry
- @Component
- struct Index {
- aboutToAppear() {
- AppFreezeWatcher.initWatcher();
- }
- private simulateAppFreeze() {
- const startTime = new Date().getTime();
- while (new Date().getTime() - startTime < 8000) {
- }
- }
- build() {
- Column({ space: 20 }) {
- Button('模拟主线程阻塞 (触发 3秒告警)')
- .onClick(() => {
- this.simulateAppFreeze();
- })
- }
- }
- }
复制代码
LogRetrievalTask 负责读取 external_log,并组装端侧业务轨迹与系统底层栈。读取时设置最大阈值,避免一次拉取过大文件:- import fileIo from '@ohos.file.fs';
- import { CloudUploader } from './CloudUploader';
- export class LogRetrievalTask {
- public static async executeTask(eventName: string, reason: string, faultLogPath: string): Promise<void> {
- try {
- let faultLogContent = '';
- let stat = await fileIo.stat(faultLogPath);
- const MAX_READ_SIZE = 2 * 1024 * 1024;
- if (stat.size > 0) {
- let readLen = stat.size > MAX_READ_SIZE ? MAX_READ_SIZE : stat.size;
- let buffer = new ArrayBuffer(readLen);
- let file = await fileIo.open(faultLogPath, fileIo.OpenMode.READ_ONLY);
- await fileIo.read(file.fd, buffer, { offset: 0, length: readLen });
- let decoder = new util.TextDecoder('utf-8');
- faultLogContent = decoder.decodeToString(new Uint8Array(buffer));
- await fileIo.close(file.fd);
- }
- let businessTrailLog = 'User clicked Order -> UI loading state -> Sync heavy task started';
- let payload = {
- appVersion: '1.0.0-gray',
- timestamp: new Date().getTime(),
- eventType: eventName,
- exceptionReason: reason,
- systemFaultStack: faultLogContent,
- businessTrail: businessTrailLog
- };
- await CloudUploader.uploadToCloud(payload);
- } catch (e) {
- console.error('[LogRetrievalTask] 日志回捞失败: ' + JSON.stringify(e));
- }
- }
- }
复制代码
上传层使用 @ohos.net.http,将高保真数据包以 POST JSON 方式发送到 APM 后台,并设置连接和读取超时:- import http from '@ohos.net.http';
- import { MonitorConfig } from '../common/config/MonitorConfig';
- export class CloudUploader {
- public static async uploadToCloud(payload: Object): Promise<void> {
- let httpRequest = http.createHttp();
- try {
- let response = await httpRequest.request(
- MonitorConfig.UPLOAD_URL,
- {
- method: http.RequestMethod.POST,
- header: { 'Content-Type': 'application/json' },
- extraData: JSON.stringify(payload),
- expectDataType: http.HttpDataType.STRING,
- connectTimeout: 5000,
- readTimeout: 5000
- }
- );
- if (response.responseCode === 200) {
- console.info('[CloudUploader] 端云协同上报成功');
- }
- } catch (err) {
- console.error('[CloudUploader] 网络请求异常: ' + JSON.stringify(err));
- } finally {
- httpRequest.destroy();
- }
- }
- }
复制代码
四、避坑指南
1. Release 与 Debug 模式差异。Watchdog 冻屏检测在 Debug 模式下默认静默,目的是防止断点调试时停顿几秒就被判定卡死强杀。验证 AppFreeze 逻辑时,应使用 Release 包,或配置开启测试选项,让应用运行在贴近真实用户的生命周期管理环境下。
2. 主线程二次灾害。THREAD_BLOCK_3S 触发时,主线程已经处于脆弱状态。如果回调中再做耗时字符串拼接、同步读写文件或大规模 JSON 序列化,就可能加速 3 秒告警向 6 秒死亡演变。获取 faultLogPath 后,应立即转移到独立并发模型或 TaskPool 中执行。
3. 日志配额与清理策略。频繁卡顿告警可能在短时间内生成大量 faultlog。系统层 external_log 会被操作系统按空间配额循环覆盖,Faultlogger 守护进程拥有这批文件的生命周期管理权。回传云端后一般无需物理删除;不要在代码中 unlink 不属于应用沙箱目录的底层文件,否则可能触发文件句柄权限异常和稳定性风险。
五、总结
对于规模庞大、交互复杂的应用,在实验室环境中做到 100% 无死锁、无卡顿并不现实。关键是在卡顿真的发生时,能否建立一套雷达网,把远端用户环境数字化定格。借助 HarmonyOS 7.0 Performance Analysis Kit 的应用冻屏告警机制与 HiRetrieval 端云配合架构,可以在 3 秒时捕获告警并保存高保真数据流,而不是等 6 秒后闪退才收墓碑日志。原文实践中,该方案在灰度期捕获超过 80% 难以复现的主线程超时问题,实现防微杜渐、未死先报。 |