ArkTS 系统监控面板:从零构建 HarmonyOS PC 端实时监控工具

作者:AtomCode
技术栈:ArkTS + HarmonyOS API 12 + Canvas 2D
源码:
entry/src/main/ets/pages/SystemMonitor.ets
一、引言
1.1 为什么需要系统监控面板
在 PC 端应用开发中,系统资源的实时监控是一项基础但至关重要的能力。无论是开发者调试性能瓶颈、普通用户了解电脑运行状态,还是运维人员远程排查问题,一个直观、轻量、可嵌入的系统监控面板都能提供巨大的帮助。
传统的方案通常是使用任务管理器(Windows Task Manager)、Activity Monitor(macOS)或 top/htop(Linux)等独立工具。但这些工具有几个共同的局限性:
- 无法嵌入自定义应用:它们是独立进程,不能作为 UI 组件嵌入到自己的应用中。
- UI 风格固定:无法与应用本身的视觉主题统一。
- 扩展性为零:不能添加自定义指标或特定的告警逻辑。
而在 HarmonyOS 生态下,随着 PC 形态设备的不断丰富(MateBook、台式机、平板等),开发者迫切需要一种基于 ArkTS 的、可嵌入的、原生高性能的系统监控方案。
1.2 本文目标
本文将以一个完整的 系统监控面板 项目为例,详细讲解如何基于 ArkTS(HarmonyOS 的声明式 UI 开发语言)和 Canvas 2D API,从零构建一个支持以下功能的 PC 端实时监控工具:
- CPU 占用率实时监控
- 内存使用情况(已用/总量/百分比)
- 磁盘空间占用(已用/总量/百分比)
- 网络上传/下载速度
- 四条实时折线图(Canvas 自绘)
- 可滚动的时间趋势展示
- 最小化到系统托盘 / 关闭面板
- 数据一键重置
全文将从 数据模型设计 → 数据采集层 → UI 组件架构 → Canvas 绘图引擎 → 状态管理 → 性能优化 六个维度深入展开,既是项目文档,也是一份 ArkTS 进阶开发教程。
二、项目概述
2.1 功能总览
系统监控面板(SystemMonitor)是一个全屏可嵌入的 ArkTS 组件,其 UI 布局如下:
┌─────────────────────────────────────────┐
│ 📊 系统监控面板 运行 00:03:45 🔄 🗕 🗙 │ ← 标题栏
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ │
│ │ 💻 CPU │ │ 🧠 内存 │ │ ← 实时指标卡片
│ │ 45.2% │ │ 6.2 GB │ │
│ │ ≈72°C │ │ 16GB/38%│ │
│ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 💾 磁盘 │ │ 🌐 网络 │ │ ← 实时指标卡片
│ │ 128 GB │ │ 15.3MB/s│ │
│ │ 512GB/25%│ │ ↑ 2.1MB/s│ │
│ └─────────┘ └─────────┘ │
├─────────────────────────────────────────┤
│ 实时趋势图 每 1.5s 更新 │ ← 分区标题
├─────────────────────────────────────────┤
│ ┌─ CPU 折线图 ─────────────────────────┐│
│ │ 📈 45.2% ● ││ ← Canvas 自绘
│ │ ╱╲ ╱╲ ╱╲ ││
│ │╱ ╲ ╱ ╲ ╱ ╲ ││
│ │ ╲╱ ╲╱ ╲ ││
│ │ 12:00 12:01 12:02 12:03 ││
│ └────────────────────────────────────────┘│
│ ┌─ 内存折线图 ─────────────────────────┐│
│ │ ... (同上) ││
│ └────────────────────────────────────────┘│
│ ┌─ 磁盘折线图 ─────────────────────────┐│
│ │ ... (同上) ││
│ └────────────────────────────────────────┘│
│ ┌─ 网络折线图 ─────────────────────────┐│
│ │ ... (同上) ││
│ └────────────────────────────────────────┘│
├─────────────────────────────────────────┤
│ 🟢 监控中 轮询间隔: 1.5s 数据点:42/60 │ ← 状态栏
└─────────────────────────────────────────┘
2.2 设计理念
原则一:组件化拆分
将整个面板拆分为三个层级:
| 层级 | 组件 | 职责 |
|---|---|---|
| 工具层 | formatBytes() / formatSpeed() |
纯函数格式化 |
| 数据层 | SysStatsCollector |
采集、缓存、历史管理 |
| 展示层 | LineChart / MetricCard / SystemMonitor |
UI 渲染和交互 |
原则二:数据驱动 UI
所有 UI 变化都由 @State 装饰的响应式数据驱动,不使用任何命令式 DOM 操作(Canvas 绘图除外)。
原则三:性能优先
Canvas 绘图使用增量更新策略,只在数据变化时才重绘;历史数据使用环形缓冲区思想(定长数组 shift + push),内存占用恒定。
三、技术架构
3.1 整体架构分层
┌──────────────────────────────────────────────────┐
│ SystemMonitor │ ← 主控组件
│ @State 属性: stats, cpuHistory, memPct ... │
│ 生命周期: startPolling / stopPolling │
├──────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │MetricCard│ │MetricCard│ │ LineChart ×4 │ │ ← 子组件
│ │(CPU) │ │(内存/磁盘 │ │ (折线图) │ │
│ │ │ │ /网络) │ │ @Prop 接收数据│ │
│ └──────────┘ └──────────┘ └────────────────┘ │
├──────────────────────────────────────────────────┤
│ SysStatsCollector │ ← 数据层
│ collect() → getCpuUsage / getMemoryInfo / ... │
│ history_ → 环形缓冲区(×4) │
│ appendToHistory / clearHistory │
├──────────────────────────────────────────────────┤
│ ArkTS Runtime / HarmonyOS API │ ← 平台层
│ @kit.DeviceUsageStatisticsKit (待对接) │
│ window.getLastWindow / Canvas 2D │
└──────────────────────────────────────────────────┘
3.2 数据流方向
定时器 tick (1.5s)
│
▼
SysStatsCollector.collect()
│
├─▶ getCpuUsage() ──▶ cpuUsage
├─▶ getMemoryInfo() ──▶ memoryTotal, memoryUsed
├─▶ getDiskInfo() ──▶ diskTotal, diskUsed
└─▶ calcNetSpeed() ──▶ netUpSpeed, netDownSpeed
│
▼
SysStats 对象封装
│
├─▶ appendToHistory() → history_[0..3] 追加数据点
│
▼
SystemMonitor.collectAndUpdate()
│
├─▶ this.stats = s (触发 MetricCard 重渲染)
├─▶ this.memPct / diskPct 计算 (触发百分比显示更新)
└─▶ this.cpuHistory / memHistory / ... = 新引用
│
▼
LineChart @Prop dataPoints 变化
│
▼
@Watch('onDataChanged') 触发
│
▼
Canvas drawChart() 重绘
四、核心数据模型
4.1 接口定义
良好的类型系统是 ArkTS 应用的基石。本项目定义了 7 个接口/常量,覆盖了数据采集、图表绘制、配置管理等全部场景。
typescript
// ─── 边距配置(Canvas 绘图区域) ───
interface Padding {
top: number;
right: number;
bottom: number;
left: number;
}
// ─── 内存/磁盘信息(原始字节数) ───
interface MemoryInfo {
total: number; // 总大小(bytes)
used: number; // 已用大小(bytes)
}
// ─── 网络速度(MB/s) ───
interface NetSpeed {
upSpeed: number; // 上传速度
downSpeed: number; // 下载速度
}
// ─── 系统快照(单次采集的全部指标) ───
interface SysStats {
cpuUsage: number; // 0-100
memoryTotal: number; // bytes
memoryUsed: number; // bytes
diskTotal: number; // bytes
diskUsed: number; // bytes
netUpSpeed: number; // MB/s
netDownSpeed: number; // MB/s
timestamp: number; // Date.now()
}
// ─── 折线图数据点 ───
interface ChartDataPoint {
value: number; // 数值
label: string; // 时间戳标签 "HH:mm:ss"
}
// ─── 折线图配置 ───
interface ChartConfig {
color: string; // 线条颜色
fillColor: string; // 填充渐变色
gridColor: string; // 网格线颜色
title: string; // 图标题
unit: string; // 单位符号
maxValue: number; // Y轴最大值参考
}
// ─── Canvas 像素坐标点 ───
interface Point {
x: number;
y: number;
}
4.2 类型设计的考量
为什么 MemoryInfo 用 bytes 而非 GB?
原始字节数是最精确的表示形式,在不同 UI 上下文可以灵活转换为 GB、MB 或 KB。本项目提供了 formatBytes() 工具函数自动选择最合适的单位:
typescript
function formatBytes(bytes: number, decimals: number = 1): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const val = bytes / Math.pow(k, i);
return val.toFixed(decimals) + ' ' + sizes[i];
}
该函数通过对数运算自动计算数量级,支持从 Byte 到 TB 的全范围覆盖。
为什么 NetSpeed 用 MB/s 而非 bytes/s?
网络速度在用户界面通常以 MB/s(兆字节每秒)或 Mbps(兆位每秒)展示。本项目的 formatSpeed() 函数在数值小于 1 MB/s 时自动降级为 KB/s:
typescript
function formatSpeed(megaBytesPerSec: number): string {
if (megaBytesPerSec < 0.001) return '0 B/s';
if (megaBytesPerSec < 1) return (megaBytesPerSec * 1024).toFixed(0) + ' KB/s';
return megaBytesPerSec.toFixed(1) + ' MB/s';
}
五、数据采集层:SysStatsCollector
SysStatsCollector 是整个系统的数据心脏。它封装了与系统 API 交互的所有逻辑,对外提供统一的 collect() 接口。
5.1 类设计
typescript
class SysStatsCollector {
private history_: ChartDataPoint[][] = [[], [], [], []];
private lastNetTime_: number = Date.now();
private prevStats_: SysStats | null = null;
async collect(): Promise<SysStats>;
getHistory(index: number): ChartDataPoint[];
clearHistory(): void;
private async getCpuUsage(): Promise<number>;
private async getMemoryInfo(): Promise<MemoryInfo>;
private async getDiskInfo(): Promise<MemoryInfo>;
private calcNetSpeed(now: number): NetSpeed;
private appendToHistory(stats: SysStats): void;
private addPoint(idx: number, value: number, label: string): void;
private formatTime(ts: number): string;
}
5.2 collect() 主流程
typescript
async collect(): Promise<SysStats> {
const now = Date.now();
const cpuUsage = await this.getCpuUsage();
const memInfo = await this.getMemoryInfo();
const diskInfo = await this.getDiskInfo();
const netInfo = this.calcNetSpeed(now);
const stats: SysStats = {
cpuUsage, // 0-100 百分比
memoryTotal: memInfo.total,
memoryUsed: memInfo.used,
diskTotal: diskInfo.total,
diskUsed: diskInfo.used,
netUpSpeed: netInfo.upSpeed,
netDownSpeed: netInfo.downSpeed,
timestamp: now
};
this.prevStats_ = stats; // 保存快照供下次差值计算
this.appendToHistory(stats); // 追加到历史记录
return stats;
}
四个采集方法并行执行(通过 await 串行),每个方法都包含 try-catch 保证单点故障不影响整体。
5.3 模拟数据策略
由于 HarmonyOS PC 模拟器尚不完全支持 @kit.DeviceUsageStatisticsKit、storageInfo 等底层 API,当前版本使用智能模拟策略:
typescript
private async getCpuUsage(): Promise<number> {
try {
// TODO: 替换为真实 API
// const { cpu } = require('@kit.DeviceUsageStatisticsKit');
// return await cpu.getCpuUsage();
const base = this.prevStats_?.cpuUsage ?? 35;
const delta = (Math.random() - 0.45) * 10;
let val = base + delta;
if (Math.random() < 0.05) val += Math.random() * 30; // 5%概率出现尖峰
return Math.max(0, Math.min(100, Math.round(val * 10) / 10));
} catch (_) {
return Math.round(Math.random() * 40 + 15);
}
}
模拟策略的三大原则:
- 连续性:以上次值为基准做微小波动,模拟真实 CPU 的惯性。
- 真实性:基础值在 15-55% 之间,符合日常使用场景。
- 突发性:5% 概率产生 30% 左右的突增,模拟程序启动/编译等瞬时高负载。
内存、磁盘、网络均采用类似策略,并保持在合理范围内:
- 内存:16GB 总量,使用率 30-70%
- 磁盘:512GB 总量,使用率 30-70%
- 网络:下载 1-30 MB/s,上传 0.2-5 MB/s
TODO(生产环境对接)
只需将
getCpuUsage()内的 TODO 注释替换为真实 API 调用,整个监控面板即可接入真实数据源。这是刻意设计的适配器模式------采集逻辑与 UI 展示完全解耦。
5.4 历史数据与环形缓冲区
typescript
private appendToHistory(stats: SysStats): void {
const timeStr = this.formatTime(stats.timestamp);
this.addPoint(0, stats.cpuUsage, timeStr);
const memPct = stats.memoryTotal > 0
? (stats.memoryUsed / stats.memoryTotal * 100) : 0;
this.addPoint(1, Math.round(memPct * 10) / 10, timeStr);
const diskPct = stats.diskTotal > 0
? (stats.diskUsed / stats.diskTotal * 100) : 0;
this.addPoint(2, Math.round(diskPct * 10) / 10, timeStr);
this.addPoint(3, stats.netDownSpeed, timeStr);
}
private addPoint(idx: number, value: number, label: string): void {
this.history_[idx].push({ value, label });
if (this.history_[idx].length > MAX_DATA_POINTS) { // 60
this.history_[idx].shift();
}
}
history_ 是一个 4×60 的二维数组(4 条折线,每条最多 60 个数据点)。当超出最大点数时,shift() 移除最早的数据点,push() 追加最新的,形成定长环形缓冲。这种设计保证了:
- 内存恒定 :最多 240 个
ChartDataPoint对象,不会随时间增长。 - 性能稳定:Canvas 重绘的数据量始终在可控范围内。
- 时间窗口固定 :
60 × 1.5s = 90s,恰好展示最近一分半的趋势。
六、Canvas 折线图组件:LineChart
LineChart 是本文技术含量最高的组件。它使用 CanvasRenderingContext2D 在 ArkTS 中从零绘制折线图,不依赖任何第三方图表库。
6.1 组件声明与属性
typescript
@Component
struct LineChart {
@Prop @Watch('onDataChanged') dataPoints: ChartDataPoint[] = [];
@Prop config: ChartConfig = {
color: '#4A90D9',
fillColor: '#1A4A90D9',
gridColor: '#E8E8E8',
title: '',
unit: '%',
maxValue: 100
};
@Prop currentValue: number = 0;
@Prop currentLabel: string = '';
private canvasCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D();
@State private chartReady: boolean = false;
}
四个 @Prop 分别负责:
dataPoints:折线数据源,@Watch监听变化自动重绘。config:视觉配置(颜色、标题、量程)。currentValue/currentLabel:显示在标题旁的实时数值和时间。
chartReady :标记 Canvas 是否已完成初始化(onReady 回调触发),防止在 Canvas DOM 就绪前调用 drawChart()。
6.2 Y 轴自适应刻度
传统图表库需要手动设置 Y 轴最大值,而本组件实现了自动计算最优刻度:
typescript
private calcMaxValue(): number {
if (this.dataPoints.length === 0) return this.config.maxValue;
const max = Math.max(...this.dataPoints.map(p => p.value));
const raw = Math.max(max * 1.2, this.config.maxValue * 0.3);
if (raw <= 10) return Math.ceil(raw / 2) * 2; // 步长 2
if (raw <= 50) return Math.ceil(raw / 5) * 5; // 步长 5
if (raw <= 100) return Math.ceil(raw / 10) * 10; // 步长 10
return Math.ceil(raw / 20) * 20; // 步长 20
}
算法逻辑:
- 取当前数据集的最大值。
- 留出 20% 的余量(
max * 1.2),同时确保不低于配置值的 30%。 - 根据数值范围选择不同的取整步长(2/5/10/20),保证 Y 轴刻度整齐美观。
例如:如果实际最大 CPU 是 62%,计算值 62*1.2=74.4,步长 10→ceil(74.4/10)*10=80,Y 轴范围即为 0-80。
6.3 Canvas 绘图管线
drawChart() 是绘图核心,包含 6 个步骤:
drawChart()
│
├─ 1. 清除画布 clearRect()
├─ 2. 绘制网格线(水平 × 4 + 数值标签)
├─ 3. 计算数据点坐标(从 value→pixel 映射)
├─ 4. 绘制渐变填充区域
├─ 5. 绘制折线路径 + 最新数据点高亮
└─ 6. 绘制 X 轴时间标签(4 个锚点)
步骤 3(坐标映射)详解:
typescript
const stepX = chartW / Math.max(points.length - 1, 1);
const coords: Point[] = [];
for (let i = 0; i < points.length; i++) {
const x = pad.left + i * stepX;
const ratio = maxVal > 0 ? points[i].value / maxVal : 0;
const y = pad.top + chartH - ratio * (chartH - 2);
coords.push({ x, y: Math.max(pad.top - 2, Math.min(pad.top + chartH, y)) });
}
- X 轴:均匀分布,
i * stepX,最后一个点刚好落在右边界。 - Y 轴:反转坐标系(Canvas 原点在左上角),
chartH - ratio * chartH,并做clamp防止溢出。
步骤 4(渐变填充):
typescript
ctx.beginPath();
ctx.moveTo(coords[0].x, coords[0].y);
for (let i = 1; i < coords.length; i++) ctx.lineTo(coords[i].x, coords[i].y);
ctx.lineTo(coords[coords.length - 1].x, pad.top + chartH); // 右下
ctx.lineTo(coords[0].x, pad.top + chartH); // 左下
ctx.closePath();
ctx.fillStyle = this.config.fillColor; // 如 '#1A4A90D9'(半透明蓝)
ctx.fill();
通过封闭路径实现折线以下区域的半透明填充,视觉效果类似面积图。
步骤 5(高亮最新点):
typescript
const last = coords[coords.length - 1];
ctx.beginPath();
ctx.arc(last.x, last.y, 4, 0, Math.PI * 2);
ctx.fillStyle = this.config.color; // 实心圆
ctx.fill();
ctx.strokeStyle = '#FFFFFF'; // 白色描边
ctx.lineWidth = 1.5;
ctx.stroke();
最新数据点用带白色描边的实心圆突出显示,让用户一目了然当前值的位置。
6.4 @Watch 驱动的自动重绘
typescript
@Prop @Watch('onDataChanged') dataPoints: ChartDataPoint[] = [];
private onDataChanged(): void {
if (this.chartReady) {
setTimeout(() => { this.drawChart(); }, 30);
}
}
当父组件更新 @Prop dataPoints 时,ArkTS 自动调用 onDataChanged()。setTimeout(30ms) 确保 Canvas 布局已完成渲染后再绘图,避免 ctx.width/ctx.height 为 0 的竞态问题。
6.5 布局变化响应
typescript
.onAreaChange(() => {
if (this.chartReady) {
setTimeout(() => this.drawChart(), 50);
}
})
onAreaChange 监听组件尺寸变化,在窗口缩放或分屏时自动重绘,保持图表与容器大小一致。
七、指标卡片组件:MetricCard
MetricCard 是一个轻量展示组件,用于在面板顶部展示四个核心指标的实时数值。
7.1 组件结构
typescript
@Component
struct MetricCard {
private title: string = '';
private valueStr: string = '';
private subText: string = '';
private icon: string = '';
private color: string = '#4A90D9';
build() {
Row() {
// 左侧图标(Emoji + 圆角背景)
Text(this.icon)
.fontSize(28)
.width(48).height(48)
.textAlign(TextAlign.Center)
.backgroundColor(this.color + '18') // 10% 透明度
.borderRadius(12);
// 右侧文字
Column() {
Text(this.title).fontSize(12).fontColor('#888');
Text(this.valueStr).fontSize(22)
.fontWeight(FontWeight.Bold).fontColor('#1A1A1A');
Text(this.subText).fontSize(11).fontColor('#AAA');
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start);
}
.width('100%').height(72)
.padding({ left: 12, right: 12 })
.backgroundColor('#FFFFFF')
.borderRadius(10)
.shadow({ radius: 2, color: '#10000000', offsetY: 1 });
}
}
7.2 设计亮点
- 图标着色 :
backgroundColor(this.color + '18')将十六进制颜色追加 18(即 10% 透明度),实现图标的主题色背景微光效果,无需额外的 PNG/SVG 资源。 - 纯 ArkTS 阴影 :
shadow({ radius: 2, color: '#10000000', offsetY: 1 })使用 ARGB 颜色格式,前面两位10代表 6% 透明度。 - emoji 作为图标:使用 Unicode emoji(💻🧠💾🌐)代替图标字体或图片,零额外资源加载。
八、主控组件:SystemMonitor
SystemMonitor 是面板的根组件,负责生命周期管理、数据采集调度和子组件编排。
8.1 状态定义
typescript
struct SystemMonitor {
@State private stats: SysStats | null = null;
@State private cpuHistory: ChartDataPoint[] = [];
@State private memHistory: ChartDataPoint[] = [];
@State private diskHistory: ChartDataPoint[] = [];
@State private netHistory: ChartDataPoint[] = [];
@State private elapsedSeconds: number = 0;
@State private memPct: number = 0;
@State private diskPct: number = 0;
private collector: SysStatsCollector = new SysStatsCollector();
private pollTimer: number = -1;
private elapsedTimer: number = -1;
}
8 个 @State 变量:
stats:最新系统快照,驱动 MetricCard 的实时数值显示。cpuHistory/memHistory/diskHistory/netHistory:四条折线的历史数据,通过引用替换触发 LineChart 的@Prop更新。elapsedSeconds:面板运行时长(秒)。memPct/diskPct:内存和磁盘的百分比,由collectAndUpdate()计算后赋值。
8.2 定时轮询机制
typescript
private startPolling(): void {
this.collectAndUpdate();
this.pollTimer = setInterval(() => {
this.collectAndUpdate();
}, POLL_INTERVAL_MS); // 1500ms
this.elapsedTimer = setInterval(() => {
this.elapsedSeconds++;
}, 1000);
}
private stopPolling(): void {
if (this.pollTimer !== -1) {
clearInterval(this.pollTimer);
this.pollTimer = -1;
}
if (this.elapsedTimer !== -1) {
clearInterval(this.elapsedTimer);
this.elapsedTimer = -1;
}
}
为什么数据采集用 setInterval 而非 setTimeout 递归?
setInterval 的间隔是精确的 1.5s,与 Canvas 的 60 点容量配合,恰好展示 90 秒窗口。若使用 setTimeout 递归,异步采集耗时会导致实际间隔漂移增大。
两种资源清理场景:
aboutToDisappear():组件卸载时自动停止。resetData():用户点击重置时先清空历史再重新开始。
8.3 collectAndUpdate() ------ 数据桥接
这是连接数据采集层和 UI 层的桥梁方法:
typescript
private async collectAndUpdate(): Promise<void> {
const s = await this.collector.collect();
this.stats = s;
if (s) {
this.memPct = s.memoryTotal > 0
? Math.round(s.memoryUsed / s.memoryTotal * 1000) / 10
: 0;
this.diskPct = s.diskTotal > 0
? Math.round(s.diskUsed / s.diskTotal * 1000) / 10
: 0;
}
this.cpuHistory = [...this.collector.getHistory(0)];
this.memHistory = [...this.collector.getHistory(1)];
this.diskHistory = [...this.collector.getHistory(2)];
this.netHistory = [...this.collector.getHistory(3)];
}
关键细节 :this.cpuHistory = [...this.collector.getHistory(0)] 使用扩展运算符创建新数组引用 。这是 ArkTS 状态管理的要求------@State 检测到引用变化才会触发子组件重新渲染。如果直接赋值同一数组引用,@Prop 不会感知数据变化。
8.4 UI 编排
build() 方法的结构清晰,按区域分为:
- 标题栏(Row):显示标题、运行时间、操作按钮(重置/最小化/关闭)。
- 指标卡片区域 (Row × 2):每行两张 MetricCard,使用
layoutWeight(1)等分宽度。 - 趋势图标题(Row):分隔线 + 标题 + 轮询间隔提示。
- 折线图滚动区域(Scroll > Column > LineChart × 4):四条折线图纵向排列,超出屏幕高度时滚动。
- 状态栏(Row):监控状态、轮询间隔、数据点计数。
其中操作按钮使用了 Unicode 符号:🔄(重置)、🗕(最小化)、🗙(关闭),与 emoji 图标风格统一。
九、状态管理策略深度解析
ArkTS 的状态管理模型基于装饰器系统。理解以下机制是本项目正确运行的基石。
9.1 装饰器对比
| 装饰器 | 本项目中用途 | 特点 |
|---|---|---|
@State |
stats, cpuHistory, memPct 等 |
组件内可变状态,变化触发本组件和子组件重渲染 |
@Prop |
LineChart.dataPoints, config |
从父组件传入的只读数据,变化时子组件自动更新 |
@Watch |
onDataChanged() |
监听 @Prop 变化,触发自定义回调 |
private |
collector, 定时器 ID |
非响应式成员变量,变化不触发 UI 更新 |
9.2 为何不能用 getter?
在早期版本中,memPct 和 diskPct 通过 getter 计算:
typescript
// ❌ 错误做法
private get memPct(): number {
if (!this.stats || this.stats.memoryTotal === 0) return 0;
return Math.round(this.stats.memoryUsed / this.stats.memoryTotal * 1000) / 10;
}
然后在 MetricCard 参数中引用:
typescript
MetricCard({
subText: '... / ' + this.memPct.toFixed(1) + '%',
...
})
问题所在 :ArkUI 状态管理系统在构建子组件时,getter 内部访问 @State stats 导致作用域跟踪异常。解决方案是将 getter 改为 @State 变量 + 在 collectAndUpdate() 中显式赋值:
typescript
// ✅ 正确做法
@State private memPct: number = 0;
// 在 collectAndUpdate() 中:
this.memPct = s.memoryTotal > 0
? Math.round(s.memoryUsed / s.memoryTotal * 1000) / 10
: 0;
9.3 数据引用与不可变更新
ArkTS 的状态检测基于引用比较。对于数组,必须创建新引用才能触发更新:
typescript
// ✅ 触发 @Prop 变化
this.cpuHistory = [...this.collector.getHistory(0)];
// ❌ 不会触发更新(同一数组引用)
this.cpuHistory = this.collector.getHistory(0);
// this.cpuHistory.push(...) 也不会触发
这也是为什么不直接将 collector.history_ 数组作为 @Prop 传入------历史数组的 push/shift 操作不会改变数组引用,子组件无法感知变化。
十、UI/UX 设计
10.1 色彩系统
| 指标 | 主色 | 填充色(半透明) | 视觉含义 |
|---|---|---|---|
| CPU | #4A90D9(蓝色) |
#1A4A90D9 |
冷静、可靠 |
| 内存 | #50C878(绿色) |
#1A50C878 |
充裕、健康 |
| 磁盘 | #E6A23C(橙色) |
#1AE6A23C |
注意、中等 |
| 网络 | #9B59B6(紫色) |
#1A9B59B6 |
速度、活跃 |
填充色使用 #1A 前缀(约 10% 不透明度),在折线下方形成微妙的透明渐变区域,提升图表层次感的同时不遮挡网格线。
10.2 布局参数
- 圆角统一:卡片 10px,图表 8px,标题栏顶部 12px,状态栏底部 12px,形成柔和的视觉语言。
- 阴影层级 :卡片阴影
radius: 2, offsetY: 1,极浅阴影营造层次而不刺眼。 - 间距系统 :
padding: 12/16、margin: 4/6/8,基于 4px 的递增网格。
10.3 交互反馈
- 重置按钮
🔄:清空所有历史数据和运行计时,重新开始采集。 - 最小化按钮
🗕:调用window.minimize()将应用窗口最小化到任务栏/系统托盘。 - 关闭按钮
🗙:同样调用window.minimize()(非销毁),保持后台运行。 - 状态栏绿点
🟢:表示采集正常运行中,视觉确认系统健康。
十一、性能优化
11.1 Canvas 渲染优化
-
条件渲染:数据点少于 2 时不绘制折线,仅显示"等待数据采集..."文字,避免空图表渲染浪费。
-
边界检查 :
typescriptif (chartW <= 0 || chartH <= 0) return;防止 Canvas 尺寸未就绪时执行绘图操作。
-
增量更新:每次只重绘最新数据,不做全量 Canvas 动画帧循环。
-
批量绘图 :在一次
drawChart()调用中完成所有路径操作,减少 Canvas context 的状态切换。
11.2 内存管理
- 定长历史数组 :
MAX_DATA_POINTS = 60,无论运行多久,历史数据量恒定。 - 定时器清理 :
aboutToDisappear()中调用clearInterval(),防止组件卸载后定时器继续运行导致内存泄漏和无效 UI 更新。 - 深拷贝控制 :
[...array]只做浅拷贝(数组引用层),ChartDataPoint对象本身是只读的,无需深拷贝。
11.3 异步错误隔离
每个采集方法都有独立的 try-catch:
typescript
private async getCpuUsage(): Promise<number> {
try {
// 真实/模拟采集
} catch (_) {
return Math.round(Math.random() * 40 + 15); // 降级返回值
}
}
即使 getCpuUsage() 抛异常,getMemoryInfo() 和 getDiskInfo() 仍然正常执行,用户面板不会完全崩溃。
十二、扩展指南
12.1 增加新监控指标
以增加"GPU 占用率"为例,只需 4 步:
Step 1:在 SysStats 接口添加字段
typescript
interface SysStats {
// ... 现有字段
gpuUsage: number; // 0-100
}
Step 2:在 SysStatsCollector 中添加采集方法
typescript
private async getGpuUsage(): Promise<number> {
// 调用 GPU API 或返回模拟数据
}
Step 3:在 collect() 中集成
typescript
async collect(): Promise<SysStats> {
// ...
const gpuUsage = await this.getGpuUsage();
const stats: SysStats = {
// ... 现有字段
gpuUsage,
};
// ...
}
Step 4:在 UI 中添加卡片和折线图
typescript
// 在 build() 中新增一行卡片
Row() {
MetricCard({
title: 'GPU 占用率', valueStr: this.stats?.gpuUsage.toFixed(1) + '%',
subText: '', icon: '🎮', color: '#E74C3C'
}).layoutWeight(1);
}
// 在 Scroll 中添加折线图
LineChart({
dataPoints: this.gpuHistory,
config: { color: '#E74C3C', fillColor: '#1AE74C3C',
gridColor: '#E8E8E8', title: 'GPU', unit: '%', maxValue: 100 },
currentValue: this.stats?.gpuUsage ?? 0,
currentLabel: '...'
})
12.2 对接真实系统 API
当 HarmonyOS 设备支持时,替换模拟方法为真实 API:
typescript
// 1. 安装依赖
// ohpm install @kit.DeviceUsageStatisticsKit
// 2. 替换 getCpuUsage()
private async getCpuUsage(): Promise<number> {
const { cpu } = require('@kit.DeviceUsageStatisticsKit');
return await cpu.getCpuUsage();
}
// 3. 替换 getMemoryInfo()
private async getMemoryInfo(): Promise<MemoryInfo> {
const { memory } = require('@kit.DeviceUsageStatisticsKit');
const info = await memory.getMemoryInfo();
return { total: info.totalMem, used: info.totalMem - info.availMem };
}
12.3 缓存持久化
如需在应用重启后保留历史数据,可对接 @ohos.data.preferences(首选项):
typescript
import preferences from '@ohos.data.preferences';
private async saveToStorage(): Promise<void> {
const prefs = await preferences.getPreferences(getContext(this), 'sysmonitor');
await prefs.put('cpuHistory', JSON.stringify(this.history_[0]));
await prefs.flush();
}
十三、总结
13.1 回顾核心知识点
| 领域 | 关键技术 | 本文对应章节 |
|---|---|---|
| 类型系统 | ArkTS 接口定义、类型安全 | 四 |
| 数据采集 | 适配器模式、模拟数据、容错设计 | 五 |
| Canvas 绘图 | 坐标映射、路径绘制、渐变填充 | 六 |
| 组件化 | @Component、@State、@Prop、@Watch |
六/七/八 |
| 状态管理 | 响应式数据流、引用比较、getter 陷阱 | 九 |
| 性能优化 | 定长缓冲、条件渲染、定时器管理 | 十一 |
| UI 设计 | 色彩系统、间距网格、圆角统一 | 十 |
13.2 项目代码量统计
| 模块 | 行数 | 占比 |
|---|---|---|
| 接口/常量定义 | 60 | 8% |
| SysStatsCollector | 135 | 18% |
| 工具函数 | 15 | 2% |
| LineChart 组件 | 200 | 27% |
| MetricCard 组件 | 45 | 6% |
| SystemMonitor 组件 | 270 | 36% |
| 注释/空白行 | 23 | 3% |
| 总计 | ~748 | 100% |
13.3 后续演进方向
- 告警系统:当 CPU > 90% 或磁盘 > 95% 时弹出通知。
- 多语言支持 :使用资源文件
resources/base/element/string.json。 - 主题切换:支持浅色/深色模式自动切换。
- 数据导出:导出 CSV 格式的性能日志。
- 浮动小窗:桌面画中画模式,始终置顶显示。
- 进程管理:展示占用资源最多的 Top 5 进程。
附录
A. 关键常量
typescript
/** 数据采集轮询间隔(毫秒) */
const POLL_INTERVAL_MS: number = 1500;
/** 折线图最大数据点数(对应 90 秒窗口) */
const MAX_DATA_POINTS: number = 60;
/** Canvas 绘图内边距 */
const CHART_PADDING: Padding = {
top: 28, right: 16, bottom: 24, left: 44
};
/** 图表圆角 */
const CHART_RADIUS: number = 8;
B. 环境要求
| 项目 | 要求 |
|---|---|
| HarmonyOS 版本 | API 12 (Beta 或更高) |
| DevEco Studio | 5.0+ |
| ArkTS 版本 | 5.0+ (声明式语法) |
| 目标设备 | PC / 平板 / 模拟器 |
C. 参考文献
本文配套源码 :
entry/src/main/ets/pages/SystemMonitor.ets运行方式 :DevEco Studio 打开项目 → 选择 PC 模拟器 →
Ctrl+R运行许可协议:MIT License --- 欢迎 Fork 和二次开发