WhatsCanvas教程-第十一章:性能优化

WhatsCanvas教程-第十一章:性能优化

仓库地址:https://github.com/ClarkWain/WhatsCanvas

.

本章目标:掌握 WhatsCanvas 的性能优化策略,包括 Picture 录制与光栅化缓存、quickReject、渲染统计、资源管理和性能测试方法。


11.1 性能优化的层次

复制代码
应用层   → 减少不必要的绘制(quickReject、脏区域)
缓存层   → Picture 录制 + 光栅化缓存(避免重复计算)
资源层   → 纹理管理、Mipmap、Atlas
后端层   → 选择合适后端、减少状态切换

11.2 Picture 录制

Picture 将一系列绘制命令录制成一个不可变的"录像",可以多次重放:

cpp 复制代码
// 录制绘制命令(不立即执行)
auto picture = canvas->recordPicture([](wsc::Canvas& c) {
    wsc::Paint bg;
    bg.setLinearGradient(0, 0, 400, 300,
        wsc::Color(30, 60, 120, 255), wsc::Color(20, 20, 40, 255));
    c.drawRect(wsc::RectF(0, 0, 400, 300), bg);

    wsc::Paint text;
    text.setColor(wsc::Color(255, 255, 255, 255));
    text.setTextSize(24.0f);
    c.drawText("Static Content", 50, 150, text);

    // ... 大量静态绘制操作 ...
});

// 多次重放------比重新执行所有绘制命令更快
canvas->drawPicture(*picture);

适用场景

  • UI 中不随帧变化的静态部分(背景、工具栏、侧边栏)
  • 复杂的图标或装饰元素
  • 缓存复杂路径的绘制结果

Picture 的属性

cpp 复制代码
picture->operationCount();  // 录制了多少个绘制命令
picture->empty();           // 是否为空

11.3 光栅化缓存 (Rasterized Picture)

drawPictureRasterized 更进一步------将 Picture 的绘制结果缓存为 GPU 纹理,后续帧直接贴图:

cpp 复制代码
// 第一次调用:执行 Picture 并将结果缓存到 GPU 纹理
// 后续调用:直接使用缓存的纹理,跳过所有绘制命令
canvas->drawPictureRasterized(*picture);

缓存预算

cpp 复制代码
// 设置缓存总预算(字节)
canvas->setRetainedPictureRasterCacheBudgetBytes(64 * 1024 * 1024);  // 64MB

// 查询当前预算
size_t budget = canvas->retainedPictureRasterCacheBudgetBytes();

drawPicture vs drawPictureRasterized

方面 drawPicture drawPictureRasterized
首次开销 低(仅重放命令) 高(渲染 + 上传纹理)
后续开销 中(每次重放) 极低(纹理贴图)
内存占用 极低 高(缓存纹理)
变换支持 完美(矢量重放) 缩放会模糊
适用场景 命令不多或需要缩放 命令量大且尺寸固定

11.4 Quick Reject

在大量绘制操作中,quickReject 可以在不做实际渲染的情况下快速判断一个区域是否完全不可见:

cpp 复制代码
// 适用于滚动列表、大型画布等场景
void drawItems(wsc::Canvas& canvas, const std::vector<Item>& items) {
    for (const auto& item : items) {
        wsc::RectF bounds = item.getBounds();

        // 如果完全在裁剪区域之外,跳过
        if (canvas.quickReject(bounds)) {
            continue;
        }

        item.draw(canvas);
    }
}

配合裁剪使用

cpp 复制代码
// 设置可见区域裁剪
canvas->clipRect(viewportRect);

// 只绘制与视口有交集的内容
for (const auto& widget : widgets) {
    if (!canvas->quickReject(widget.bounds())) {
        widget.render(*canvas);
    }
}

11.5 减少状态切换

Paint 复用

cpp 复制代码
// 不好:每个元素创建新 Paint
for (auto& item : items) {
    wsc::Paint p;              // 每次构造 + 析构
    p.setColor(item.color);
    p.setTextSize(14.0f);
    p.setAntiAlias(true);
    canvas->drawText(item.text, item.x, item.y, p);
}

// 好:复用 Paint 对象,只修改变化的属性
wsc::Paint p;
p.setTextSize(14.0f);
p.setAntiAlias(true);
for (auto& item : items) {
    p.setColor(item.color);    // 只改变颜色
    canvas->drawText(item.text, item.x, item.y, p);
}

批量绘制同类型图形

cpp 复制代码
// 尽量将相同 Paint 属性的绘制操作放在一起
// GPU 后端会减少着色器/状态切换

// 先画所有背景
wsc::Paint bgPaint;
bgPaint.setColor(wsc::Color(240, 240, 240, 255));
for (auto& card : cards) {
    canvas->drawRoundRect(card.bgRect, 12, bgPaint);
}

// 再画所有文字
wsc::Paint textPaint;
textPaint.setTextSize(14.0f);
textPaint.setColor(wsc::Color(33, 33, 33, 255));
for (auto& card : cards) {
    canvas->drawText(card.title, card.titleX, card.titleY, textPaint);
}

11.6 图片资源优化

Mipmap

缩小绘制图片时,Mipmap 能显著减少锯齿并提高性能:

cpp 复制代码
// 加载时生成 Mipmap(默认开启)
canvas->loadImageFromEncodedMemory(image, data, size, true /*generateMipmaps*/);

// 绘制时使用 Mipmap 采样
wsc::Paint imgPaint;
imgPaint.setImageSampling(wsc::Paint::ImageSampling::MIPMAP_LINEAR);
canvas->drawImageFit(image, dst, wsc::Canvas::ImageFit::COVER, imgPaint);

避免每帧加载

cpp 复制代码
// 不好:每帧创建纹理
void onFrame(Canvas& canvas) {
    Image img;
    canvas.loadImage(img, "icon.png");  // 每帧上传!
    canvas.drawImage(img, 10, 10, paint);
}

// 好:初始化时加载,持有引用
class MyScene {
    Image icon_;

    void onInit(Canvas& canvas) {
        canvas.loadImage(icon_, "icon.png");
    }

    void onFrame(Canvas& canvas) {
        canvas.drawImage(icon_, 10, 10, paint);
    }
};

局部更新

如果只有图片的一小部分变化,使用局部更新而非整体替换:

cpp 复制代码
// 只更新变化的区域
canvas->updateImageRGBA(image, dirtyPixels, dirtyX, dirtyY, dirtyW, dirtyH);

11.7 渲染统计

WhatsCanvas 提供了内置的性能统计:

cpp 复制代码
// 开启 GPU 计时
canvas->setGpuTimingEnabled(true);

// 帧结束后获取统计
canvas->endFrame();
auto stats = canvas->getRenderStats();

RenderStats 包含的信息(参考 CanvasStats.h)可以帮助定位性能瓶颈。


11.8 异步像素回读

同步回读 (readPixelsRGBA) 会导致 GPU 管线停顿。对于非即时需要的回读,使用异步版本:

cpp 复制代码
// 发起异步读取请求
canvas->readPixelsRGBAAsync([](std::vector<unsigned char> pixels, int w, int h) {
    // 回调在数据就绪时触发
    // 可以在后台线程处理像素(保存文件、上传等)
    savePNG(pixels, w, h, "screenshot.png");
});

// 在后续帧中轮询完成状态
if (canvas->hasPendingReadPixelsRGBAAsync()) {
    canvas->pollReadPixelsRGBAAsync();
}

11.9 帧循环优化模式

脏区域追踪

如果只有部分 UI 变化,可以只重绘变化部分:

cpp 复制代码
void renderFrame(Canvas& canvas, const DirtyRegion& dirty) {
    if (dirty.isEmpty()) {
        // 无需重绘
        return;
    }

    canvas.beginFrame();

    // 裁剪到脏区域
    canvas.save();
    canvas.clipRect(dirty.bounds());

    // 重绘所有与脏区域相交的内容
    for (auto& widget : widgets_) {
        if (!canvas.quickReject(widget.bounds())) {
            widget.render(canvas);
        }
    }

    canvas.restore();
    canvas.endFrame();
}

条件帧更新

对于非动画场景,只在有变化时才绘制新帧:

cpp 复制代码
bool needsRedraw = false;

void onUserInput() {
    needsRedraw = true;
}

void mainLoop() {
    while (!shouldQuit) {
        pollEvents();

        if (needsRedraw || hasAnimation) {
            canvas->beginFrame();
            draw(*canvas);
            canvas->endFrame();
            canvas->present();
            needsRedraw = false;
        } else {
            // 无变化,等待事件(省电)
            waitEvents();
        }
    }
}

11.10 性能测试方法

使用桌面平台的 Benchmark 模式

WhatsCanvas 仓库内置了 benchmark 工具:

bash 复制代码
# 运行 benchmark(warmup 30帧 + 测量 300帧)
./build/whatscanvas_desktop --scene=feature_showcase --benchmark

# 指定帧数
./build/whatscanvas_desktop --scene=geometry_stress \
    --benchmark --warmup=60 --measured=600

自定义 Benchmark

cpp 复制代码
#include <chrono>

void benchmark(Canvas& canvas, int warmupFrames, int measuredFrames) {
    // 预热
    for (int i = 0; i < warmupFrames; ++i) {
        canvas.beginFrame();
        drawScene(canvas);
        canvas.endFrame();
    }

    // 测量
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < measuredFrames; ++i) {
        canvas.beginFrame();
        drawScene(canvas);
        canvas.endFrame();
    }
    auto end = std::chrono::high_resolution_clock::now();

    float totalMs = std::chrono::duration<float, std::milli>(end - start).count();
    float avgMs = totalMs / measuredFrames;
    float fps = 1000.0f / avgMs;

    printf("Average frame time: %.2f ms (%.0f FPS)\n", avgMs, fps);
}

像素 Hash 回归

确保优化不改变输出结果:

cpp 复制代码
canvas->endFrame();
uint64_t hash = canvas->computePixelsHashRGBA();
assert(hash == expectedHash && "Pixel regression detected!");

11.11 交互式场景的缓存策略

拖拽、缩放和动画会连续触发重绘。对于这类场景,除了减少绘制命令,还需要根据内容的变化频率选择合适的缓存方式。

稳定层与动态层

可以先将界面分为两层:

text 复制代码
稳定层:背景、固定工具栏、未变化的列表或画布内容
动态层:拖拽对象、选中效果、按压反馈和动画

稳定层适合使用 Picture 或 Image 缓存;动态层通常直接绘制,并放在稳定层之后。这样,动态对象的位置发生变化时,不需要重新生成整个背景缓存。

cpp 复制代码
drawStableContent(canvas);   // 重放稳定内容
drawMovingItem(canvas);      // 绘制当前帧的动态对象
drawInteractionEffect(canvas);

选择缓存类型

Picture、Image 和 Atlas 解决的问题不同:

场景 推荐方式
复杂矢量内容需要重复绘制 Picture
内容复杂、尺寸固定且长期不变 drawPictureRasterized
背景或装饰已经可以表示为稳定像素 Image
大量重复的图标、棋子或粒子 Image Atlas
内容每帧都变化或持续缩放 直接绘制或普通 drawPicture

Picture 保留矢量重放能力,适合仍需缩放或变换的内容。drawPictureRasterized 会额外占用纹理内存,但可以避免后续帧重复执行复杂命令。对于大面积静态渐变或装饰,也可以先使用 Software Canvas 绘制为位图,再加载为 Image

下面的示例使用离屏 Canvas 绘制背景,再将像素上传到目标 Canvas,创建可重复绘制的 Image:

cpp 复制代码
std::unique_ptr<wsc::Image> createBackgroundImage(wsc::Canvas& targetCanvas,
                                                   int width, int height) {
    // Software Canvas 用于一次性生成像素,不占用目标帧的绘制时间。
    auto scratch = wsc::Canvas::create(
        wsc::Canvas::Backend::Software, width, height);
    if (!scratch || !scratch->initializeContext()) {
        return nullptr;
    }

    scratch->beginFrame();

    wsc::Paint background;
    background.setRadialGradient(
        width * 0.5f, height * 0.5f, width * 0.7f,
        wsc::Color(42, 132, 88), wsc::Color(8, 48, 34));
    scratch->drawRect(
        wsc::RectF(0, 0, static_cast<float>(width),
                   static_cast<float>(height)),
        background);

    // 还可以在这里绘制阴影、装饰线和其他静态内容。
    scratch->endFrame();

    std::vector<unsigned char> pixels;
    const bool readOk = scratch->readPixelsRGBA(pixels);
    scratch->finalizeContext();
    if (!readOk) {
        return nullptr;
    }

    auto image = std::make_unique<wsc::Image>();
    if (!targetCanvas.loadImageFromRGBA(
            *image, pixels, width, height,
            false /* generateMipmaps */)) {
        return nullptr;
    }
    return image;
}

创建完成后,在帧循环中直接绘制该 Image:

cpp 复制代码
if (backgroundImage) {
    wsc::Paint imagePaint;
    imagePaint.setColor(wsc::Color::WHITE);
    canvas.drawImage(
        *backgroundImage,
        wsc::RectF(0, 0,
                   static_cast<float>(backgroundImage->getWidth()),
                   static_cast<float>(backgroundImage->getHeight())),
        viewport,
        imagePaint);
}

这段创建流程应放在初始化、资源恢复或尺寸变化时执行,不要放在每帧渲染函数中。对于 OpenGL/OpenGL ES 后端,调用 loadImageFromRGBA 时还应确保目标 Canvas 的图形上下文已经初始化并处于可用状态。

当大量对象只包含有限几种外观时,可以将这些外观合并到一张 Atlas 中:

cpp 复制代码
wsc::Paint imagePaint;
imagePaint.setColor(wsc::Color::WHITE);

for (const Item& item : visibleItems) {
    const wsc::RectF src = atlasCell(item.type, item.state);
    canvas.drawImage(atlas, src, item.bounds, imagePaint);
}

Atlas 中的对象共享同一张纹理,可以减少纹理切换和重复资源创建。图集尺寸应根据实际显示大小确定;RGBA 纹理的基本占用为 宽 × 高 × 4 字节,过大的图集会增加上传时间和内存占用。

控制缓存失效范围

缓存应按界面中能够独立变化的最小单元拆分。例如,列表中只有两个分组发生变化时,只需重建这两个分组的 Picture,不必让整个列表失效。

cpp 复制代码
void moveItem(int sourceGroup, int destinationGroup) {
    updateModel(sourceGroup, destinationGroup);
    invalidateGroup(sourceGroup);
    invalidateGroup(destinationGroup);
}

失效条件应与缓存内容实际依赖的状态保持一致。指针坐标如果只影响动态覆盖层,就不应使稳定 Picture 失效。动画对象也可以暂时从稳定缓存中排除,在动画结束后再更新对应组件:

cpp 复制代码
drawStableComponents(canvas);
for (const Motion& motion : motions) {
    drawMotionSprite(canvas, motion);
}

批量动画可以错开对象的开始或结束时间,将缓存更新分散到不同帧,避免同一帧重建多个大缓存。

内存与性能平衡

光栅化 Picture、Image 和 Atlas 都会占用纹理内存。设置缓存预算时,还需要考虑 CPU 像素副本、Mipmaps、多重采样缓冲和图形驱动开销。

如果缓存只命中少量帧,或者频繁被淘汰,栅格化成本可能高于收益。此时应缩小缓存范围,或者改用普通 drawPicture。Surface 或图形上下文销毁时,也应释放相关 Picture、Image 和 Atlas,并在上下文恢复后按需重建。

验证交互性能

交互性能测试应覆盖完整操作过程,例如按下、连续移动、释放和动画结束。除平均 FPS 外,还应记录最大帧时间、超过目标帧预算的次数,以及首次生成缓存时的耗时和内存峰值。

动画持续时间也应结合刷新率设计。60 FPS 下,50 ms 只有约 3 帧;需要看清运动轨迹的动画通常需要更长时间,并可通过错峰减少单帧负载。

完整的测试示例见Android 交互式 Canvas 性能实战


11.12 优化清单

优化 适用场景 效果
drawPictureRasterized 复杂静态 UI 部件 避免每帧重算,纹理直贴
quickReject 长列表、大画布 跳过不可见元素
Paint 复用 大量同类元素 减少对象构造和状态切换
Mipmap + LINEAR 采样 图片缩小显示 减少锯齿,GPU 友好
局部 updateImageRGBA 动态纹理 避免整体重传
异步像素回读 截图、录屏 不阻塞渲染管线
脏区域 + clipRect 部分更新 UI 减少绘制面积
条件帧更新 静态场景 省电、减少 GPU 负载
批量同属性绘制 列表渲染 减少着色器切换
组件级缓存失效 拖拽、棋盘、编辑器 避免整屏缓存重建
动态覆盖层 拖拽和批量动画 稳定缓存与逐帧对象解耦
Image Atlas 重复复杂精灵 减少纹理数量和绘制状态变化
错峰落地刷新 批量动画 分散缓存重建峰值
缓存预算与淘汰统计 内存受限设备 平衡命中率与常驻内存

11.13 小结

本章学习了:

  • Picture 录制与重放
  • 光栅化缓存(drawPictureRasterized)
  • quickReject 跳过不可见内容
  • 减少状态切换的技巧
  • 图片资源优化(Mipmap、复用、局部更新)
  • 渲染统计与 GPU 计时
  • 异步像素回读
  • 脏区域追踪和条件帧更新
  • 性能测试方法
  • 静态层与动态层拆分
  • Image Atlas 与缓存失效边界
  • 组件级 dirty 标记与动画覆盖层
  • 缓存预算、生命周期和平台差异
  • 交互动画的帧预算与观感验证

下一章跨平台实战 ------ 学习 Android、iOS 和 Web 平台的集成实践。

.

.

.

.

.

.

.

.

相关推荐
又吹风_Bassy1 小时前
WhatsCanvas教程-第九章:窗口呈现与交互
canvas·opengl·skia·2d渲染库·nanovg·whatscanvas·2d图形库
甜辣uu19 小时前
智能体Agent性能优化从原理到实战
人工智能·性能优化·大模型·llm·agent·rag·智能体
天天喝旺仔20 小时前
Python asyncio 异步编程实战:用协程与事件循环构建高性能网络服务
服务器·网络·python·性能优化·fastapi
落樱弥城1 天前
RHI 设计考量:不同 API 差异分析
服务器·前端·性能优化
hui-梦苑1 天前
[Gromacs]核心性能梯度测试:GROMACS 多线程性能调优实验
缓存·性能优化·多线程·gromacs·动力学模拟
用户307140958481 天前
零依赖实现「形切形」:我用 6200 行原生 Canvas 写了个网页设计工具
前端·ai编程·canvas
万年咸鱼1 天前
Java BufferedOutputStream 详解:原理、用法与性能优化
java·开发语言·性能优化
云雨巫山1 天前
Unity 游戏开发性能优化全景手册
unity·智能手机·性能优化·游戏引擎
云雨巫山1 天前
Unity 性能优化 · 图解全景手册
unity·性能优化·游戏引擎