HarmonyOS开发:Canvas性能优化与高效绘制策略

举报
Jack20 发表于 2026/06/22 22:17:54 2026/06/22
【摘要】 HarmonyOS开发:Canvas性能优化与高效绘制策略📌 核心要点:从绘制调用优化、路径合并批处理、脏区域重绘到硬件加速,系统掌握Canvas性能优化的核心策略,让复杂绘制场景的帧率从30fps提升到稳定60fps。 一、背景与动机Canvas是HarmonyOS图形开发中最灵活也最容易"翻车"的组件。灵活在于——你可以用它在屏幕上画任何东西;容易翻车在于——稍不留神,性能就崩了。我...

HarmonyOS开发:Canvas性能优化与高效绘制策略

📌 核心要点:从绘制调用优化、路径合并批处理、脏区域重绘到硬件加速,系统掌握Canvas性能优化的核心策略,让复杂绘制场景的帧率从30fps提升到稳定60fps。


一、背景与动机

Canvas是HarmonyOS图形开发中最灵活也最容易"翻车"的组件。灵活在于——你可以用它在屏幕上画任何东西;容易翻车在于——稍不留神,性能就崩了。

我见过一个真实的案例:一个天气应用用Canvas绘制24小时温度曲线,数据点不多,才288个(每5分钟一个),但帧率只有25fps。开发者百思不得其解——“就这么点数据,怎么就卡了?”

打开代码一看,问题一目了然:每个数据点都单独创建了一个Path对象、设置了一次strokeStyle、调用了一次stroke()。288个点就是288次绘制调用,每次调用都要从CPU向GPU提交一次绘制命令,GPU在"接收命令→执行→等待下一个命令"之间反复切换,效率极低。

这就像去超市购物——如果你买100样东西,每拿一样就去收银台结账一次,那收银员(GPU)得崩溃。正确的做法是把100样东西都装进购物车,最后一次性结账。这就是**批处理(Batching)**的核心思想。

Canvas性能优化的本质,就是减少不必要的绘制工作,让每一次绘制调用都"物尽其用"。今天我们就来系统性地拆解Canvas性能优化的方方面面。


二、核心原理

2.1 Canvas性能瓶颈分析

Canvas的性能瓶颈通常出现在以下几个环节:

graph TD
    A[Canvas绘制请求]:::primary --> B[CPU生成绘制命令]:::info
    B --> C{命令提交瓶颈?}:::warning
    C -->|绘制调用过多| D[CPU-GPU通信瓶颈]:::error
    C -->|命令正常| E[GPU执行渲染]:::info
    E --> F{渲染计算瓶颈?}:::warning
    F -->|像素填充过多| G[Overdraw瓶颈]:::error
    F -->|计算正常| H[帧缓冲输出]:::primary
    D --> I[掉帧]:::error
    G --> I
    
    J[优化策略]:::primary
    J --> K[减少绘制调用]:::info
    J --> L[路径合并批处理]:::info
    J --> M[脏区域重绘]:::info
    J --> N[硬件加速]:::info
    K -.-> D
    L -.-> D
    M -.-> G
    N -.-> E

    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff

性能瓶颈的量化指标:

瓶颈类型 症状 典型原因 优化方向
绘制调用过多 CPU占用高、GPU利用率低 每个元素单独绘制 批处理合并
路径操作过多 stroke/fill耗时高 复杂Path未优化 路径简化
Overdraw严重 GPU填充率高 不必要的重叠绘制 裁剪和分层
全量重绘 每帧都重绘整个Canvas 未使用脏区域 局部重绘
状态切换频繁 上下文状态设置耗时 频繁切换fillStyle等 状态排序

2.2 绘制调用优化原理

每一次Canvas的绘制调用(如fill()stroke()drawImage()),都要经历以下流程:

ArkTS调用 → 命令序列化 → 跨进程传输 → GPU命令解析 → GPU执行

这个流程的开销不在于GPU执行本身,而在于"命令提交"的过程。一次fill()可能只需要GPU 0.01ms来执行,但命令提交可能需要0.1ms。如果你调用1000次fill(),GPU执行时间只有10ms,但命令提交就要100ms——严重失衡。

优化核心:减少绘制调用次数,增加每次调用的"工作量"。

2.3 脏区域重绘原理

脏区域(Dirty Region)重绘是Canvas性能优化的利器。它的核心思想是:只重绘发生变化的区域,而不是每帧重绘整个Canvas。

全量重绘:每帧重绘 100% 的区域
脏区域重绘:每帧只重绘 5%-20% 的区域

性能提升:5-20

脏区域重绘的实现需要:

  1. 脏区域标记:记录哪些区域发生了变化
  2. 区域合并:将多个小脏区域合并为大区域(减少clip操作)
  3. 裁剪绘制:只在脏区域内执行绘制操作

三、代码实战

3.1 基础用法——绘制调用优化与路径合并

/**
 * Canvas绘制调用优化示例
 * 对比:逐个绘制 vs 批量绘制
 */
@Entry
@Component
struct DrawCallOptimization {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  
  @State drawMode: string = 'batch'; // 'single' | 'batch'
  @State drawTime: number = 0;
  
  build() {
    Column() {
      Canvas(this.context)
        .width(360)
        .height(360)
        .backgroundColor('#F5F5F5')
        .onReady(() => {
          this.redraw();
        })
      
      Row() {
        Button('逐个绘制')
          .fontSize(14)
          .height(36)
          .backgroundColor(this.drawMode === 'single' ? '#F44336' : '#E0E0E0')
          .fontColor(this.drawMode === 'single' ? Color.White : '#333333')
          .onClick(() => {
            this.drawMode = 'single';
            this.redraw();
          })
        
        Button('批量绘制')
          .fontSize(14)
          .height(36)
          .margin({ left: 8 })
          .backgroundColor(this.drawMode === 'batch' ? '#4CAF50' : '#E0E0E0')
          .fontColor(this.drawMode === 'batch' ? Color.White : '#333333')
          .onClick(() => {
            this.drawMode = 'batch';
            this.redraw();
          })
      }
      .margin({ top: 12 })
      
      Text(`绘制耗时: ${this.drawTime.toFixed(2)}ms`)
        .fontSize(16)
        .margin({ top: 8 })
        .fontColor(this.drawTime > 16 ? '#F44336' : '#4CAF50')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
  
  /**
   * 重新绘制
   */
  private redraw(): void {
    const startTime = Date.now();
    const ctx = this.context;
    const width = 360;
    const height = 360;
    
    ctx.clearRect(0, 0, width, height);
    
    if (this.drawMode === 'single') {
      this.drawSingleMode(ctx, width, height);
    } else {
      this.drawBatchMode(ctx, width, height);
    }
    
    this.drawTime = Date.now() - startTime;
  }
  
  /**
   * 逐个绘制(低效)
   * 每个圆单独一个Path、单独设置样式、单独fill
   */
  private drawSingleMode(ctx: CanvasRenderingContext2D, width: number, height: number): void {
    const count = 500;
    
    for (let i = 0; i < count; i++) {
      const x = Math.random() * width;
      const y = Math.random() * height;
      const r = Math.random() * 10 + 3;
      
      // ❌ 每个圆都是独立的绘制调用
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.7)`;
      ctx.fill(); // 500次fill()调用!
    }
  }
  
  /**
   * 批量绘制(高效)
   * 按颜色分组,相同颜色的圆合并到一个Path中
   */
  private drawBatchMode(ctx: CanvasRenderingContext2D, width: number, height: number): void {
    const count = 500;
    const colorGroups: Map<string, Array<{ x: number; y: number; r: number }>> = new Map();
    
    // 第一步:按颜色分组
    const colors = ['#e94560', '#0f3460', '#533483', '#00b4d8', '#4CAF50'];
    for (let i = 0; i < count; i++) {
      const color = colors[i % colors.length];
      if (!colorGroups.has(color)) {
        colorGroups.set(color, []);
      }
      colorGroups.get(color)!.push({
        x: Math.random() * width,
        y: Math.random() * height,
        r: Math.random() * 10 + 3,
      });
    }
    
    // 第二步:每种颜色只设置一次样式,合并到一个Path中
    for (const [color, circles] of colorGroups) {
      ctx.fillStyle = color; // 每种颜色只设置一次
      ctx.beginPath();
      
      for (const circle of circles) {
        // ✅ 使用moveTo避免路径自动连接
        ctx.moveTo(circle.x + circle.r, circle.y);
        ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
      }
      
      ctx.fill(); // 每种颜色只调用一次fill(),总共5次!
    }
  }
}

性能对比:

模式 fill()调用次数 fillStyle设置次数 典型耗时
逐个绘制 500 500 25-40ms
批量绘制 5 5 2-5ms

从500次绘制调用降到5次,性能提升5-10倍!

3.2 进阶用法——脏区域重绘

脏区域重绘是Canvas性能优化的进阶技巧。下面的示例展示了如何实现一个高效的脏区域重绘系统。

/**
 * 脏区域重绘管理器
 * 核心思路:只重绘变化的区域,而不是整个Canvas
 */
class DirtyRegionManager {
  private dirtyRegions: Rect[] = [];
  private maxRegions: number = 10; // 最大脏区域数量
  
  /**
   * 标记一个区域为脏区域
   */
  markDirty(x: number, y: number, width: number, height: number): void {
    this.dirtyRegions.push({ x, y, width, height });
    
    // 如果脏区域过多,合并所有区域为一个大区域
    if (this.dirtyRegions.length > this.maxRegions) {
      this.mergeAllRegions();
    }
  }
  
  /**
   * 获取所有脏区域
   */
  getDirtyRegions(): Rect[] {
    return [...this.dirtyRegions];
  }
  
  /**
   * 判断是否有脏区域
   */
  hasDirtyRegions(): boolean {
    return this.dirtyRegions.length > 0;
  }
  
  /**
   * 清空所有脏区域(重绘完成后调用)
   */
  clearDirtyRegions(): void {
    this.dirtyRegions = [];
  }
  
  /**
   * 合并所有脏区域为一个大区域
   */
  private mergeAllRegions(): void {
    if (this.dirtyRegions.length === 0) return;
    
    let minX = Infinity, minY = Infinity;
    let maxX = -Infinity, maxY = -Infinity;
    
    for (const region of this.dirtyRegions) {
      minX = Math.min(minX, region.x);
      minY = Math.min(minY, region.y);
      maxX = Math.max(maxX, region.x + region.width);
      maxY = Math.max(maxY, region.y + region.height);
    }
    
    this.dirtyRegions = [{
      x: minX,
      y: minY,
      width: maxX - minX,
      height: maxY - minY,
    }];
  }
  
  /**
   * 获取脏区域的包围盒
   */
  getDirtyBounds(): Rect | null {
    if (this.dirtyRegions.length === 0) return null;
    
    let minX = Infinity, minY = Infinity;
    let maxX = -Infinity, maxY = -Infinity;
    
    for (const region of this.dirtyRegions) {
      minX = Math.min(minX, region.x);
      minY = Math.min(minY, region.y);
      maxX = Math.max(maxX, region.x + region.width);
      maxY = Math.max(maxY, region.y + region.height);
    }
    
    return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
  }
}

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

脏区域重绘在组件中的应用:

/**
 * 脏区域重绘示例:可拖拽的圆形
 * 只有被拖拽的圆形区域需要重绘
 */
@Entry
@Component
struct DirtyRegionDemo {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private dirtyManager: DirtyRegionManager = new DirtyRegionManager();
  
  @State circles: DraggableCircle[] = [];
  private draggingIndex: number = -1;
  private lastX: number = 0;
  private lastY: number = 0;
  private offscreenBg: OffscreenCanvas | null = null;
  
  build() {
    Column() {
      Canvas(this.context)
        .width(360)
        .height(400)
        .backgroundColor('#1a1a2e')
        .onReady(() => {
          this.initCircles();
          this.initOffscreenBackground();
          this.fullRedraw();
        })
        .onTouch((event: TouchEvent) => {
          this.handleTouch(event);
        })
      
      Text('拖拽圆形体验脏区域重绘')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
  
  /**
   * 初始化圆形数据
   */
  private initCircles(): void {
    const colors = ['#e94560', '#0f3460', '#533483', '#00b4d8', '#4CAF50', '#FF9800'];
    this.circles = [];
    for (let i = 0; i < 6; i++) {
      this.circles.push({
        x: 60 + (i % 3) * 120,
        y: 100 + Math.floor(i / 3) * 150,
        radius: 40,
        color: colors[i],
        prevX: 60 + (i % 3) * 120,
        prevY: 100 + Math.floor(i / 3) * 150,
      });
    }
  }
  
  /**
   * 初始化离屏背景(静态内容只画一次)
   */
  private initOffscreenBackground(): void {
    this.offscreenBg = this.context.createOffscreenCanvas(360, 400);
    const ctx = this.offscreenBg.getContext('2d');
    
    // 绘制网格背景
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
    ctx.lineWidth = 1;
    for (let x = 0; x < 360; x += 30) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, 400);
      ctx.stroke();
    }
    for (let y = 0; y < 400; y += 30) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(360, y);
      ctx.stroke();
    }
  }
  
  /**
   * 全量重绘(初始化时使用)
   */
  private fullRedraw(): void {
    const ctx = this.context;
    ctx.clearRect(0, 0, 360, 400);
    
    // 绘制缓存的背景
    if (this.offscreenBg) {
      ctx.drawImage(this.offscreenBg, 0, 0);
    }
    
    // 绘制所有圆形
    this.drawAllCircles(ctx);
  }
  
  /**
   * 脏区域重绘(拖拽时使用)
   */
  private dirtyRedraw(): void {
    if (!this.dirtyManager.hasDirtyRegions()) return;
    
    const ctx = this.context;
    
    // 遍历所有脏区域
    for (const region of this.dirtyManager.getDirtyRegions()) {
      // 裁剪到脏区域
      ctx.save();
      ctx.beginPath();
      ctx.rect(region.x, region.y, region.width, region.height);
      ctx.clip();
      
      // 只在脏区域内重绘
      ctx.clearRect(region.x, region.y, region.width, region.height);
      
      // 重绘背景(脏区域部分)
      if (this.offscreenBg) {
        ctx.drawImage(this.offscreenBg, 
          region.x, region.y, region.width, region.height,
          region.x, region.y, region.width, region.height);
      }
      
      // 重绘与脏区域相交的圆形
      for (const circle of this.circles) {
        if (this.circleIntersectsRect(circle, region)) {
          this.drawCircle(ctx, circle);
        }
      }
      
      ctx.restore();
    }
    
    // 清空脏区域
    this.dirtyManager.clearDirtyRegions();
  }
  
  /**
   * 处理触摸事件
   */
  private handleTouch(event: TouchEvent): void {
    const touch = event.touches[0];
    const x = touch.x;
    const y = touch.y;
    
    switch (event.type) {
      case TouchType.Down: {
        // 查找被点击的圆形
        for (let i = this.circles.length - 1; i >= 0; i--) {
          const c = this.circles[i];
          const dist = Math.sqrt((x - c.x) ** 2 + (y - c.y) ** 2);
          if (dist <= c.radius) {
            this.draggingIndex = i;
            this.lastX = x;
            this.lastY = y;
            break;
          }
        }
        break;
      }
      
      case TouchType.Move: {
        if (this.draggingIndex < 0) break;
        
        const circle = this.circles[this.draggingIndex];
        const dx = x - this.lastX;
        const dy = y - this.lastY;
        
        // 标记旧位置为脏区域
        this.dirtyManager.markDirty(
          circle.x - circle.radius - 5,
          circle.y - circle.radius - 5,
          circle.radius * 2 + 10,
          circle.radius * 2 + 10
        );
        
        // 更新位置
        circle.prevX = circle.x;
        circle.prevY = circle.y;
        circle.x += dx;
        circle.y += dy;
        
        // 标记新位置为脏区域
        this.dirtyManager.markDirty(
          circle.x - circle.radius - 5,
          circle.y - circle.radius - 5,
          circle.radius * 2 + 10,
          circle.radius * 2 + 10
        );
        
        this.lastX = x;
        this.lastY = y;
        
        // 执行脏区域重绘
        this.dirtyRedraw();
        break;
      }
      
      case TouchType.Up: {
        this.draggingIndex = -1;
        break;
      }
    }
  }
  
  /**
   * 绘制所有圆形
   */
  private drawAllCircles(ctx: CanvasRenderingContext2D): void {
    for (const circle of this.circles) {
      this.drawCircle(ctx, circle);
    }
  }
  
  /**
   * 绘制单个圆形
   */
  private drawCircle(ctx: CanvasRenderingContext2D, circle: DraggableCircle): void {
    // 阴影
    ctx.shadowColor = circle.color;
    ctx.shadowBlur = 20;
    
    ctx.beginPath();
    ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
    ctx.fillStyle = circle.color;
    ctx.fill();
    
    // 高光
    ctx.shadowColor = 'transparent';
    ctx.shadowBlur = 0;
    const highlight = ctx.createRadialGradient(
      circle.x - circle.radius * 0.3,
      circle.y - circle.radius * 0.3,
      0,
      circle.x,
      circle.y,
      circle.radius
    );
    highlight.addColorStop(0, 'rgba(255, 255, 255, 0.3)');
    highlight.addColorStop(1, 'rgba(255, 255, 255, 0)');
    ctx.fillStyle = highlight;
    ctx.fill();
  }
  
  /**
   * 判断圆形是否与矩形相交
   */
  private circleIntersectsRect(circle: DraggableCircle, rect: Rect): boolean {
    const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
    const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));
    const distX = circle.x - closestX;
    const distY = circle.y - closestY;
    return (distX * distX + distY * distY) <= (circle.radius * circle.radius);
  }
}

interface DraggableCircle {
  x: number;
  y: number;
  radius: number;
  color: string;
  prevX: number;
  prevY: number;
}

3.3 完整示例——Canvas性能优化实战

这个完整示例集成了绘制调用优化、路径合并、脏区域重绘和离屏缓存四大优化策略,并提供了性能对比面板。

/**
 * Canvas性能优化实战
 * 综合运用:批处理 + 脏区域 + 离屏缓存 + 状态排序
 */
@Entry
@Component
struct CanvasPerfOptimization {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private dirtyManager: DirtyRegionManager = new DirtyRegionManager();
  
  // 离屏缓存
  private gridOffscreen: OffscreenCanvas | null = null;
  private chartOffscreen: OffscreenCanvas | null = null;
  
  // 数据
  @State dataPoints: ChartPoint[] = [];
  @State highlightIndex: number = -1;
  @State frameTime: number = 0;
  @State drawCallCount: number = 0;
  @State optimizedMode: boolean = true;
  
  private canvasWidth: number = 360;
  private canvasHeight: number = 300;
  private animOffset: number = 0;
  private animRunning: boolean = false;
  
  build() {
    Column() {
      // 性能面板
      Row() {
        Column() {
          Text(`${this.frameTime.toFixed(1)}ms`)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.frameTime > 16 ? '#F44336' : '#4CAF50')
          Text('帧耗时')
            .fontSize(10)
            .fontColor('#999999')
        }
        .alignItems(HorizontalAlign.Center)
        
        Divider().vertical(true).height(30).margin({ left: 16, right: 16 })
        
        Column() {
          Text(`${this.drawCallCount}`)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.drawCallCount > 50 ? '#F44336' : '#4CAF50')
          Text('绘制调用')
            .fontSize(10)
            .fontColor('#999999')
        }
        .alignItems(HorizontalAlign.Center)
        
        Divider().vertical(true).height(30).margin({ left: 16, right: 16 })
        
        Column() {
          Text(this.optimizedMode ? '优化' : '原始')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.optimizedMode ? '#4CAF50' : '#FF9800')
          Text('渲染模式')
            .fontSize(10)
            .fontColor('#999999')
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .height(60)
      .padding({ left: 24, right: 24 })
      .backgroundColor(Color.White)
      .justifyContent(FlexAlign.Center)
      
      // Canvas
      Canvas(this.context)
        .width(this.canvasWidth)
        .height(this.canvasHeight)
        .backgroundColor('#0f0c29')
        .borderRadius(12)
        .margin({ top: 12 })
        .onReady(() => {
          this.initData();
          this.initOffscreenCaches();
          this.startAnimation();
        })
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Move) {
            const x = event.touches[0].x;
            // 计算最近的数据点索引
            const stepX = this.canvasWidth / (this.dataPoints.length - 1);
            this.highlightIndex = Math.round(x / stepX);
            this.highlightIndex = Math.max(0, Math.min(this.dataPoints.length - 1, this.highlightIndex));
          } else if (event.type === TouchType.Up) {
            this.highlightIndex = -1;
          }
        })
      
      // 切换按钮
      Row() {
        Button(this.optimizedMode ? '切换到原始模式' : '切换到优化模式')
          .fontSize(14)
          .height(40)
          .layoutWeight(1)
          .backgroundColor(this.optimizedMode ? '#4CAF50' : '#FF9800')
          .onClick(() => {
            this.optimizedMode = !this.optimizedMode;
          })
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  /**
   * 初始化数据
   */
  private initData(): void {
    this.dataPoints = [];
    for (let i = 0; i < 100; i++) {
      this.dataPoints.push({
        x: i,
        value: Math.sin(i * 0.1) * 40 + 50 + Math.random() * 10,
      });
    }
  }
  
  /**
   * 初始化离屏缓存
   */
  private initOffscreenCaches(): void {
    // 缓存网格背景
    this.gridOffscreen = this.context.createOffscreenCanvas(
      this.canvasWidth, this.canvasHeight
    );
    const gridCtx = this.gridOffscreen.getContext('2d');
    this.drawGrid(gridCtx);
  }
  
  /**
   * 绘制网格(只画一次,缓存到离屏Canvas)
   */
  private drawGrid(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D): void {
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
    ctx.lineWidth = 1;
    
    // 水平线
    for (let y = 0; y < this.canvasHeight; y += 30) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(this.canvasWidth, y);
      ctx.stroke();
    }
    
    // 垂直线
    for (let x = 0; x < this.canvasWidth; x += 30) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, this.canvasHeight);
      ctx.stroke();
    }
  }
  
  /**
   * 启动动画
   */
  private startAnimation(): void {
    this.animRunning = true;
    const animate = () => {
      if (!this.animRunning) return;
      
      const startTime = Date.now();
      this.animOffset += 0.02;
      
      // 更新数据(模拟实时数据)
      for (let i = 0; i < this.dataPoints.length; i++) {
        this.dataPoints[i].value = Math.sin(i * 0.1 + this.animOffset) * 40 + 50 + Math.random() * 5;
      }
      
      if (this.optimizedMode) {
        this.renderOptimized();
      } else {
        this.renderNaive();
      }
      
      this.frameTime = Date.now() - startTime;
      requestAnimationFrame(animate);
    };
    animate();
  }
  
  /**
   * 原始渲染(无优化)
   */
  private renderNaive(): void {
    const ctx = this.context;
    let callCount = 0;
    
    // 全量清屏
    ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
    callCount++;
    
    // 每帧重绘网格
    this.drawGrid(ctx);
    callCount += 20; // 约20条线
    
    // 逐点绘制(每个点单独调用)
    const stepX = this.canvasWidth / (this.dataPoints.length - 1);
    
    // 绘制填充区域
    ctx.beginPath();
    ctx.moveTo(0, this.canvasHeight);
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      ctx.lineTo(x, y);
    }
    ctx.lineTo(this.canvasWidth, this.canvasHeight);
    ctx.closePath();
    const gradient = ctx.createLinearGradient(0, 0, 0, this.canvasHeight);
    gradient.addColorStop(0, 'rgba(0, 180, 216, 0.3)');
    gradient.addColorStop(1, 'rgba(0, 180, 216, 0)');
    ctx.fillStyle = gradient;
    ctx.fill();
    callCount++;
    
    // 绘制曲线
    ctx.beginPath();
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.strokeStyle = '#00b4d8';
    ctx.lineWidth = 2;
    ctx.stroke();
    callCount++;
    
    // 逐点绘制数据点(❌ 低效:每个点单独调用)
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      ctx.beginPath();
      ctx.arc(x, y, 2, 0, Math.PI * 2);
      ctx.fillStyle = '#00b4d8';
      ctx.fill();
      callCount++; // 100次fill调用!
    }
    
    // 高亮点
    if (this.highlightIndex >= 0) {
      const x = this.highlightIndex * stepX;
      const y = this.canvasHeight - this.dataPoints[this.highlightIndex].value * 2;
      ctx.beginPath();
      ctx.arc(x, y, 6, 0, Math.PI * 2);
      ctx.fillStyle = '#e94560';
      ctx.fill();
      callCount++;
    }
    
    this.drawCallCount = callCount;
  }
  
  /**
   * 优化渲染
   */
  private renderOptimized(): void {
    const ctx = this.context;
    let callCount = 0;
    
    // 全量清屏
    ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
    callCount++;
    
    // ✅ 使用离屏缓存的网格(1次drawImage代替20次stroke)
    if (this.gridOffscreen) {
      ctx.drawImage(this.gridOffscreen, 0, 0);
      callCount++;
    }
    
    const stepX = this.canvasWidth / (this.dataPoints.length - 1);
    
    // 绘制填充区域和曲线(合并为2次绘制调用)
    ctx.beginPath();
    ctx.moveTo(0, this.canvasHeight);
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      ctx.lineTo(x, y);
    }
    ctx.lineTo(this.canvasWidth, this.canvasHeight);
    ctx.closePath();
    const gradient = ctx.createLinearGradient(0, 0, 0, this.canvasHeight);
    gradient.addColorStop(0, 'rgba(0, 180, 216, 0.3)');
    gradient.addColorStop(1, 'rgba(0, 180, 216, 0)');
    ctx.fillStyle = gradient;
    ctx.fill();
    callCount++;
    
    // 曲线
    ctx.beginPath();
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.strokeStyle = '#00b4d8';
    ctx.lineWidth = 2;
    ctx.stroke();
    callCount++;
    
    // ✅ 批量绘制数据点(1次fill代替100次)
    ctx.fillStyle = '#00b4d8';
    ctx.beginPath();
    for (let i = 0; i < this.dataPoints.length; i++) {
      const x = i * stepX;
      const y = this.canvasHeight - this.dataPoints[i].value * 2;
      ctx.moveTo(x + 2, y);
      ctx.arc(x, y, 2, 0, Math.PI * 2);
    }
    ctx.fill();
    callCount++; // 1次fill代替100次!
    
    // 高亮点
    if (this.highlightIndex >= 0) {
      const x = this.highlightIndex * stepX;
      const y = this.canvasHeight - this.dataPoints[this.highlightIndex].value * 2;
      ctx.beginPath();
      ctx.arc(x, y, 6, 0, Math.PI * 2);
      ctx.fillStyle = '#e94560';
      ctx.fill();
      callCount++;
    }
    
    this.drawCallCount = callCount;
  }
  
  aboutToDisappear(): void {
    this.animRunning = false;
    if (this.gridOffscreen) this.gridOffscreen.release();
    if (this.chartOffscreen) this.chartOffscreen.release();
  }
}

interface ChartPoint {
  x: number;
  value: number;
}

四、踩坑与注意事项

坑点1:beginPath()忘记调用导致路径累积

这是Canvas开发中最常见的bug之一。如果你在绘制多个独立图形时忘记调用beginPath(),新的路径会追加到旧路径后面,导致所有图形被一起填充或描边。

// ❌ 错误:缺少beginPath,所有圆会被连在一起
for (const circle of circles) {
  ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
  ctx.fill(); // 每次fill都会填充从第一个圆到当前圆的所有路径!
}

// ✅ 正确:每个独立图形前调用beginPath
for (const circle of circles) {
  ctx.beginPath();
  ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
  ctx.fill();
}

// ✅ 更好:批量绘制时用moveTo分隔
ctx.beginPath();
for (const circle of circles) {
  ctx.moveTo(circle.x + circle.r, circle.y); // moveTo避免路径自动连接
  ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2);
}
ctx.fill(); // 一次fill绘制所有圆

坑点2:频繁切换Canvas状态

每次修改fillStylestrokeStylelineWidthfont等状态,Canvas内部都需要更新渲染管线。频繁切换状态会导致GPU状态机频繁刷新,性能下降。

优化策略:按状态分组绘制。 先画所有红色的元素,再画所有蓝色的元素,而不是红蓝交替画。

坑点3:shadowBlur的性能陷阱

shadowBlur是Canvas中最耗性能的属性之一。阴影需要额外的模糊计算,而且模糊半径越大,计算量呈指数级增长。一个shadowBlur = 20的fill操作,可能比没有阴影的fill慢10倍。

建议: 能用离屏Canvas预渲染阴影就预渲染,不要在动画循环中使用shadowBlur。

坑点4:clip()后忘记restore()

clip()会裁剪后续所有绘制操作到指定区域,如果你忘记restore(),后续所有绘制都会被裁剪,导致画面"消失"。每次clip()前都要save(),clip()使用完后restore()。

坑点5:Canvas分辨率与设备像素比不匹配

在高DPI设备上,如果Canvas的分辨率和显示尺寸一致,画面会模糊。但如果Canvas分辨率设为2倍,绘制开销也会变成4倍(面积翻倍)。需要根据场景权衡清晰度和性能。

// 适配高DPI
const dpr = display.getDefaultDisplaySync().densityDPI / 160;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
ctx.scale(dpr, dpr); // 缩放绘制上下文

坑点6:clearRect的范围不完整

clearRect只清除指定矩形区域的内容。如果你绘制了超出Canvas边界的图形(如旋转后的矩形),clearRect(0, 0, width, height)可能无法完全清除。建议使用比Canvas稍大的清除范围,或者使用canvas.width = canvas.width强制清空。

坑点7:离屏缓存的尺寸与主Canvas不一致

如果离屏Canvas的尺寸和主Canvas不一致,drawImage时会发生缩放,这会引入额外的插值计算,降低性能。确保离屏Canvas的尺寸与主Canvas完全一致。


五、HarmonyOS 6适配说明

API差异

API HarmonyOS 5.0 HarmonyOS 6.0 迁移建议
CanvasRenderingContext2D 基础2D绘制 新增Path2D对象支持 使用Path2D预构建路径
drawImage() 不支持异步 新增drawImageAsync() 大图使用异步绘制
fill()/stroke() 同步执行 新增批量绘制API 使用批量API减少调用次数
Canvas硬件加速 默认开启 新增可配置的GPU渲染策略 根据场景选择渲染策略
getImageData() 全量获取 新增支持区域和格式参数 使用区域参数减少数据拷贝

行为变更

  • Canvas渲染管线优化:6.0中Canvas的渲染管线从"立即模式"改为"延迟模式",绘制命令不会立即提交到GPU,而是在帧结束时批量提交,自动实现批处理优化
  • Path2D对象:6.0新增Path2D对象,可以预构建路径并复用,避免每帧重新构建路径
  • 硬件加速策略:6.0新增RenderingStrategy枚举,可以选择PERFORMANCE(优先性能)或QUALITY(优先质量)模式
  • Canvas纹理缓存:6.0自动缓存Canvas的GPU纹理,连续帧内容相同时不会重新上传

适配代码

/**
 * HarmonyOS 6.0适配的Canvas绘制工具
 */
class CanvasDrawCompat {
  private static instance: CanvasDrawCompat;
  private isV6: boolean = false;
  
  private constructor() {
    this.isV6 = this.detectApiVersion();
  }
  
  static getInstance(): CanvasDrawCompat {
    if (!CanvasDrawCompat.instance) {
      CanvasDrawCompat.instance = new CanvasDrawCompat();
    }
    return CanvasDrawCompat.instance;
  }
  
  /**
   * 批量绘制圆形(6.0优化)
   */
  drawCirclesBatch(
    ctx: CanvasRenderingContext2D,
    circles: Array<{ x: number; y: number; r: number; color: string }>
  ): number {
    let drawCalls = 0;
    
    // 按颜色分组(减少状态切换)
    const colorGroups = new Map<string, Array<{ x: number; y: number; r: number }>>();
    for (const c of circles) {
      if (!colorGroups.has(c.color)) {
        colorGroups.set(c.color, []);
      }
      colorGroups.get(c.color)!.push({ x: c.x, y: c.y, r: c.r });
    }
    
    // 每种颜色一次绘制调用
    for (const [color, group] of colorGroups) {
      ctx.fillStyle = color;
      ctx.beginPath();
      
      for (const c of group) {
        ctx.moveTo(c.x + c.r, c.y);
        ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
      }
      
      ctx.fill();
      drawCalls++;
    }
    
    return drawCalls;
  }
  
  /**
   * 使用Path2D预构建路径(6.0新增)
   */
  buildPath2D(commands: (path: Path2D) => void): Path2D | null {
    // 6.0新增Path2D
    if (this.isV6 && typeof Path2D !== 'undefined') {
      try {
        const path = new Path2D();
        commands(path);
        return path;
      } catch {
        return null;
      }
    }
    return null;
  }
  
  /**
   * 使用预构建路径绘制(6.0优化)
   */
  drawWithCachedPath(
    ctx: CanvasRenderingContext2D,
    path: Path2D | null,
    fallbackDraw: () => void
  ): void {
    if (path) {
      // 6.0:直接使用缓存的Path2D
      ctx.fill(path);
    } else {
      // 5.0降级:每帧重新构建路径
      fallbackDraw();
    }
  }
  
  private detectApiVersion(): boolean {
    try {
      return typeof Path2D !== 'undefined';
    } catch {
      return false;
    }
  }
}

六、总结

维度 评价
学习难度 ⭐⭐⭐⭐
使用频率 ⭐⭐⭐⭐⭐
重要程度 ⭐⭐⭐⭐⭐

Canvas性能优化不是玄学,而是有章可循的工程实践。今天我们掌握了四大核心策略,它们就像工具箱里的四把利器,各有各的用武之地:

绘制调用优化是第一把利器——减少fill/stroke的调用次数,把1000次调用压缩到10次,性能立竿见影。核心口诀:同色合并、一次fill

路径合并批处理是第二把利器——用moveTo分隔独立图形,把多个小路径合并为一个大路径,让GPU一次处理完。核心口诀:先分组、再合并、一次提交

脏区域重绘是第三把利器——只重绘变化的区域,而不是每帧重绘整个Canvas。在拖拽、局部动画等场景中,性能提升可达5-20倍。核心口诀:标记脏区、裁剪绘制、用完清空

离屏缓存是第四把利器——静态内容只画一次,缓存到离屏Canvas,每帧直接drawImage。核心口诀:静态预渲染、动态实时画、缓存即性能

最后,记住一个原则:优化之前先测量,优化之后必验证。 不要凭感觉优化,要用数据说话。帧耗时从多少降到多少?绘制调用从多少减到多少?有了量化数据,优化才有方向,成果才有说服力。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。