系列文章《鸿蒙开发从入门到专家》第 38 篇 · 专家
代码能写好只是开始。线上才是真相:用户卡在哪个页面、为什么 ANR、为什么崩溃、上个月改的功能到底有没有人用。这些问题要靠可观测体系回答——日志、Crash 监控、性能采集、用户行为分析,都要规划到位。
这一篇把鸿蒙应用的可观测性分四个层次拆开讲:日志、Crash、性能、用户行为。每个层次有独立的工具和落地动作,但数据上报要走统一的协议。
一、什么是可观测性
可观测性(Observability)源自控制系统理论,指从系统外部输出推断系统内部状态的能力。映射到鸿蒙应用:
日志(Logs):发生了什么,一条条离散事件。
指标(Metrics):数值随时间变化,如 FPS、CPU、内存。
链路(Traces):一次请求的完整路径。
鸿蒙生态的工具集:
下面按层次逐个讲。
二、日志体系:把打印变成数据
hilog 是鸿蒙的官方日志 API,开发者随手就调,但日志要被当成"可观测性数据",不能只是 console.log。
1. 结构化日志
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0001;
const TAG = 'LoginFlow';
export function logLoginStart() {
hilog.info(DOMAIN, TAG, 'login.start', JSON.stringify({ source: 'wechat' }));
}
export function logLoginError(err: Error) {
hilog.error(DOMAIN, TAG, 'login.error', JSON.stringify({
name: err.name,
message: err.message,
stack: err.stack,
}));
}
要点:
日志格式用 JSON,利于服务端解析。
DOMAIN按业务线划分。必须有
event name,便于检索。
2. 上报到服务器
import { http } from '@kit.NetworkKit';
class LogUploader {
private buffer: LogEntry[] = [];
add(entry: LogEntry) {
this.buffer.push(entry);
if (this.buffer.length >= 50) this.flush();
}
async flush() {
if (!this.buffer.length) return;
const entries = this.buffer;
this.buffer = [];
await http.request('https://log.example.com/v1/ingest', {
method: 'POST',
body: JSON.stringify(entries),
}).catch(() => {
// 上传失败,先写本地,后续重试
entries.forEach(e => LocalStorage.save(e));
});
}
}
要点:
批量上报,节省网络。
失败要落本地,下次启动重试。
上报频率随网络场景调整。
3. 采样策略
不是所有日志都全量上报。debug 阶段可以全开,生产环境采样:
function shouldSample(): boolean {
return Math.random() < 0.1; // 10% 采样
}
风险高的链路(如支付)采样 100%,长尾路径采样 10%。
三、Crash 监控:捕获用户侧的"硬错误"
Crash 是用户感知最强的。鸿蒙的 hiAppEvent 已经能捕获到崩溃事件,但要做得更多。
1. 订阅崩溃事件
import { hiAppEvent } from '@kit.PerformanceAnalysisKit';
hiAppEvent.addWatcher({
watchId: 'crashWatcher',
appEventFilters: [
{ domain: 'app_crash', eventTypes: [hiAppEvent.EventType.CRASH, hiAppEvent.EventType.ANR] },
],
onReceive: (events) => {
events.forEach((evt) => {
uploadCrash({
type: evt.name,
stack: evt.params['stack'] as string,
fingerprint: hash(evt.params['stack'] as string),
device: deviceInfo(),
});
});
},
});
注意:崩溃事件在 onReceive 回调里可能没有足够上下文(比如当前页面、业务参数)。可以加一层"前置缓存":
class CrashContext {
static current = '';
static set(action: string) { this.current = action; }
static clear() { this.current = ''; }
}
// Wrap 在路由拦截器里
router.beforeEach((to) => CrashContext.set(`nav.to.${to.name}`));
// 崩溃时附加上
hiAppEvent.on('app_crash', () => upload({ ...event, scene: CrashContext.current }));
2. 堆栈还原
上线包混淆过,原始堆栈是不可读的,需要符号化:
CI 构建产物时同步生成
mapping.txt。上传 mapping 到崩溃后台。
客户端上传崩溃堆栈时附
version + buildId。后台用 mapping 文件做符号化。
DevEco 的混淆工具会输出 .map 文件:
obfuscator/transformer --config build-profile.json5 --out map/
CI 里:
after_script:
- ./tools/upload_mapping.sh build/r8-map/ $BUILD_ID
3. 多端崩溃聚合
后端要做 stack fingerprint 聚合:
def fingerprint(stack: str) -> str:
# 取每帧的前 3 行
lines = [l for l in stack.split('\n') if 'at ' in l][:3]
return hash('\n'.join(lines))
聚合后展示 TOP 崩溃 + 影响用户数。
四、性能监控:FPS、卡顿、冷启动
性能监控比日志精细、要求更高。
1. 帧率(FPS)
@kit.ArkUI 提供 Profiler API,可以用 Frame 类监控:
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct MonitorPage {
private fpsSamples: number[] = [];
aboutToAppear() {
setInterval(() => {
hiTraceMeter.startTrace('fps.measure', 1);
// 计算当前帧率(具体实现可参考 Profiler Sample)
hiTraceMeter.finishTrace('fps.measure', 1);
}, 1000);
}
build() { Column() { Text('帧率监控').fontSize(18) } }
}
更简洁的做法是订阅 Profiler 的实时数据流(生产用第三方封装)。
2. 冷启动时间
冷启动测量有一个标准锚点:从系统创建 Ability 到第一帧渲染完。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
const start = Date.now();
this.startTimestamp = start;
}
onWindowStageCreate(windowStage: WindowStage) {
windowStage.on('firstFrameDrawn', () => {
const cost = Date.now() - this.startTimestamp;
uploadMetric({ event: 'cold_start', duration: cost });
});
}
}
冷启动时间超过 2 秒就要优化,4 秒以上用户基本就要卸载了。
3. 卡顿(ANR/Freeze)
第 31 篇已经讲了 Profiler 工具,线上用 hiAppEvent 订阅 appfreeze:
hiAppEvent.addWatcher({
watchId: 'freezeWatcher',
appEventFilters: [{ domain: 'appfreeze', eventTypes: [hiAppEvent.EventType.CARD, hiAppEvent.EventType.TIMEOUT] }],
onReceive: (events) => uploadMetrics(events),
});
阈值通常设 5s,超过就上报。
五、用户行为:从点击到漏斗
业务最关心的不是技术问题,是"用了没"“转化怎么”。这部分通常用商业 SDK,但埋点设计要规范。
1. 事件结构
type Event = {
event: string; // 'order.submit'
properties: object; // { price: 99, itemCount: 3 }
user: string; // unionId
appVersion: string;
osVersion: string;
ts: number;
};
export function track(event: string, props?: object) {
Bus.emit(event, { ...props, event, ts: Date.now() });
}
页面浏览:
aboutToAppear() {
track('page.view', { page: 'product_detail', id: this.productId });
}
按钮点击:
Button('立即购买').onClick(() => {
track('click.buy_now', { id: this.productId });
});
2. 漏斗与归因
服务端按 event + userId + ts 排序,构造漏斗:
register 1000 人
↓
open_app 900 人
↓
search 600 人
↓
product_view 200 人
↓
order_submit 50 人
每一段的转化率看一眼就知道问题在哪。
3. 隐私合规
埋点涉及隐私,用户协议和隐私弹窗一定要先做。业务部门要委托法务审。常见的合规要点:
收集前给用户知情同意。
不采集敏感数据(身份证、银行卡 CVV、生物特征原值)。
提供"取消埋点"的开关。
数据保留期限合规(一般 6 个月到 1 年)。
六、可观测性的整体架构
上面四块分开看,实际建设时要打通:
客户端 服务端
┌──────────────────────┐ ┌──────────────┐
│ 日志 / Crash / 性能 │ ─────► │ 接入层 │
│ 行为 / 链路 │ HTTPS │ Kafka / Pulsar │
│ 统一 SDK 上报 │ │ 实时分发 │
└──────────────────────┘ └────┬─────────┘
│
┌────────┼────────┐
▼ ▼ ▼
日志检索 监控告警 数据仓库
(ES) (Grafana) (ClickHouse)
数据双写:冷数据落仓、热数据进流处理系统。
七、案例:电商 App 可观测体系演进
V1:凑活用
只用 console.log + Bugly。前期勉强够用,一上量开始崩:Bugly 上崩溃率从 0.5% 涨到 2%,客服根本不知道发生什么。
V2:分层接入
hilog结构化日志 + 自建上报系统。hiAppEvent订阅崩溃 + ANR。性能指标接入 DataBuddy。
埋点 SDK 用自研,统一协议。
V3:一体化平台
所有指标进同一个 Grafana。开发、QA、产品都能拉数据看趋势。崩溃率异常、性能下降、漏斗变化都有报警。
收益:
线上 bug 排查时间从"3 小时 + 多次提单"压缩到"30 分钟内"。
漏斗诊断后,"搜索→商品详情"转化率从 38% 提升到 56%。
灰度期间通过性能监控拦截掉一次内存泄漏。
小结
可观测性是"上线以后的事",但要在"上线之前"想清楚:
日志:结构化、批量上报、失败本地重试。
Crash:订阅 + 堆栈还原 + 聚合。
性能:FPS、ANR、冷启动三件套。
行为:统一埋点协议 + 隐私合规。
统一:所有数据走一份协议、同一接入层。
下一篇我们要从工程化角度收束——聊聊鸿蒙应用架构模式从 MVC 到 MVI 的演进,把过去几十篇讲的代码组织思路串成一条主线。
参考阅读
鸿蒙开发者文档《hilog 使用指南》
鸿蒙开发者文档《hiAppEvent 上报》
《Observability Engineering》(Honeycomb 团队著作)
《数据驱动:从方法到实践》(业务分析)