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

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 类型设计的考量

为什么 MemoryInfobytes 而非 GB

原始字节数是最精确的表示形式,在不同 UI 上下文可以灵活转换为 GBMBKB。本项目提供了 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 的全范围覆盖。

为什么 NetSpeedMB/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.DeviceUsageStatisticsKitstorageInfo 等底层 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);
  }
}

模拟策略的三大原则:

  1. 连续性:以上次值为基准做微小波动,模拟真实 CPU 的惯性。
  2. 真实性:基础值在 15-55% 之间,符合日常使用场景。
  3. 突发性: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
}

算法逻辑

  1. 取当前数据集的最大值。
  2. 留出 20% 的余量(max * 1.2),同时确保不低于配置值的 30%。
  3. 根据数值范围选择不同的取整步长(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() 方法的结构清晰,按区域分为:

  1. 标题栏(Row):显示标题、运行时间、操作按钮(重置/最小化/关闭)。
  2. 指标卡片区域 (Row × 2):每行两张 MetricCard,使用 layoutWeight(1) 等分宽度。
  3. 趋势图标题(Row):分隔线 + 标题 + 轮询间隔提示。
  4. 折线图滚动区域(Scroll > Column > LineChart × 4):四条折线图纵向排列,超出屏幕高度时滚动。
  5. 状态栏(Row):监控状态、轮询间隔、数据点计数。

其中操作按钮使用了 Unicode 符号:🔄(重置)、🗕(最小化)、🗙(关闭),与 emoji 图标风格统一。


九、状态管理策略深度解析

ArkTS 的状态管理模型基于装饰器系统。理解以下机制是本项目正确运行的基石。

9.1 装饰器对比

装饰器 本项目中用途 特点
@State stats, cpuHistory, memPct 组件内可变状态,变化触发本组件和子组件重渲染
@Prop LineChart.dataPoints, config 从父组件传入的只读数据,变化时子组件自动更新
@Watch onDataChanged() 监听 @Prop 变化,触发自定义回调
private collector, 定时器 ID 非响应式成员变量,变化不触发 UI 更新

9.2 为何不能用 getter?

在早期版本中,memPctdiskPct 通过 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/16margin: 4/6/8,基于 4px 的递增网格。

10.3 交互反馈

  • 重置按钮 🔄:清空所有历史数据和运行计时,重新开始采集。
  • 最小化按钮 🗕:调用 window.minimize() 将应用窗口最小化到任务栏/系统托盘。
  • 关闭按钮 🗙:同样调用 window.minimize()(非销毁),保持后台运行。
  • 状态栏绿点 🟢:表示采集正常运行中,视觉确认系统健康。

十一、性能优化

11.1 Canvas 渲染优化

  1. 条件渲染:数据点少于 2 时不绘制折线,仅显示"等待数据采集..."文字,避免空图表渲染浪费。

  2. 边界检查

    typescript 复制代码
    if (chartW <= 0 || chartH <= 0) return;

    防止 Canvas 尺寸未就绪时执行绘图操作。

  3. 增量更新:每次只重绘最新数据,不做全量 Canvas 动画帧循环。

  4. 批量绘图 :在一次 drawChart() 调用中完成所有路径操作,减少 Canvas context 的状态切换。

11.2 内存管理

  1. 定长历史数组MAX_DATA_POINTS = 60,无论运行多久,历史数据量恒定。
  2. 定时器清理aboutToDisappear() 中调用 clearInterval(),防止组件卸载后定时器继续运行导致内存泄漏和无效 UI 更新。
  3. 深拷贝控制[...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 后续演进方向

  1. 告警系统:当 CPU > 90% 或磁盘 > 95% 时弹出通知。
  2. 多语言支持 :使用资源文件 resources/base/element/string.json
  3. 主题切换:支持浅色/深色模式自动切换。
  4. 数据导出:导出 CSV 格式的性能日志。
  5. 浮动小窗:桌面画中画模式,始终置顶显示。
  6. 进程管理:展示占用资源最多的 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 和二次开发

相关推荐
nashane1 小时前
HarmonyOS 6学习:指南针“文图反向”Bug修复——从“北偏东”变“北偏西”的坐标系纠错
学习·华为·bug·harmonyos
慧海灵舟1 小时前
鸿蒙南向开发教程Day1:Hi3861 开发环境配置完全指南
华为·harmonyos·写文章,赢小鸿ai
禁默1 小时前
[鸿蒙PC命令行移植适配]移植rust三方库eza到鸿蒙PC的完整实践
华为·rust·harmonyos
不爱吃糖的程序媛2 小时前
React Native 应用适配鸿蒙PC 实战:从白屏到成功运行
react native·react.js·harmonyos
烛衔溟2 小时前
HarmonyOS 状态管理 V1 —— @State、@Prop、@Link 与 AppStorage
华为·harmonyos
李二。2 小时前
鸿蒙 PC 端文件搜索工具开发实战:从零构建桌面级搜索引擎
搜索引擎·华为·harmonyos
坚果派·白晓明3 小时前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成libhv鸿蒙化适配
c++·华为·ai编程·harmonyos·atomcode
hanlin033 小时前
基于OpenHarmony 5.0的CAN驱动移植步骤
linux·c语言·华为·can·openharmony·t527