HarmonyOS 鸿蒙Next Canvas 绘制大量图片纹理笔记时,设备性能严重劣化
HarmonyOS 鸿蒙Next Canvas 绘制大量图片纹理笔记时,设备性能严重劣化
问题: Canvas 做了 2.5 万次 stamp draw,Canvas 的绘制时间就上升到了 3 ~ 5s, 当前是采用了离屏渲染的设计防止页面卡死。
期望: 能否把这个绘制时间给降下来,当前试了很多种方法都降不下来,有没有良好的方案解决。
开发者您好,这边尝试复现您的问题最终未能复现,以下是本地尝试复现的代码,如果和您的代码有出入,麻烦您提供一下可复现的最小demo、sdk版本和设备版本,感谢您的支持和配合:
import { image } from '@kit.ImageKit'
@Entry
@Component
struct StampDrawingCanvas {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
// 核心离屏画布
private offscreenCanvas: OffscreenCanvas | null = null
private offCanvasContext: OffscreenCanvasRenderingContext2D | null = null
// 存储从 $rawfile 加载出来的笔尖纹理
private brushBitmap: ImageBitmap | null = null
// 记录上一个点的坐标
private lastX: number = 0
private lastY: number = 0
// 盖章步长:每隔 2 像素盖一次章(可根据图片大小和卡顿情况调整为 3 或 4)
private STAMP_STEP: number = 2
// 图片绘制的宽和高(可以根据实际需要调整,建议与原图大小一致或等比例缩放)
private BRUSH_SIZE: number = 16
async aboutToAppear() {
try {
// 获取当前上下文的资源管理器
const context = getContext(this)
const resourceMgr = context.resourceManager
// 读取 rawfile 中的图片二进制数据
const fileData = await resourceMgr.getRawFileContent('2.png')
// 将二进制数据解码为 ImageSource,再生成 PixelMap
const imageSource = image.createImageSource(fileData.buffer)
const pixelMap = await imageSource.createPixelMap()
// 将 PixelMap 包装为 Canvas 可用的 ImageBitmap
this.brushBitmap = new ImageBitmap(pixelMap)
// 释放中间产生的 imageSource
imageSource.release()
} catch (err) {
console.error('Failed to load rawfile image: ', JSON.stringify(err))
}
}
aboutToDisappear() {
if (this.brushBitmap) {
this.brushBitmap.close() // 释放笔刷内存
}
}
// 沿着两点之间的线段进行高密度“盖章”
private drawStampLine(x1: number, y1: number, x2: number, y2: number) {
if (!this.offCanvasContext || !this.brushBitmap) {
return
}
const dx = x2 - x1
const dy = y2 - y1
const distance = Math.sqrt(dx * dx + dy * dy)
const offset = this.BRUSH_SIZE / 2
// 如果移动距离极短,只盖一个章
if (distance === 0) {
this.offCanvasContext.drawImage(this.brushBitmap, x1 - offset, y1 - offset, this.BRUSH_SIZE, this.BRUSH_SIZE)
return
}
// 计算需要插值补上多少个章
const stampCount = Math.floor(distance / this.STAMP_STEP) * 5
// 沿着移动轨迹循环盖章(2.5万次调用引发卡顿的源头)
for (let i = 0; i <= stampCount; i++) {
const t = stampCount === 0 ? 0 : i / stampCount
const curX = x1 + dx * t
const curY = y1 + dy * t
// 在离屏画布上贴图,坐标减去 offset 是为了让图片中心对准手指落点
this.offCanvasContext.drawImage(this.brushBitmap, curX - offset, curY - offset, this.BRUSH_SIZE, this.BRUSH_SIZE)
}
}
build() {
Column() {
Text('ArkTS Rawfile Stamp 笔迹面板')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin(15)
Canvas(this.context)
.width('100%')
.height('80%')
.backgroundColor('#FFFFFF')
.onAreaChange((oldValue: Area, newValue: Area) => {
const width = newValue.width as number
const height = newValue.height as number
if (!this.offscreenCanvas && width > 0 && height > 0) {
this.offscreenCanvas = new OffscreenCanvas(width, height)
this.offCanvasContext = this.offscreenCanvas.getContext('2d', this.settings)
}
})
.onTouch((event: TouchEvent) => {
if (!this.offCanvasContext || !this.offscreenCanvas || !this.brushBitmap) {
return
}
const touchX = event.touches[0].x
const touchY = event.touches[0].y
switch (event.type) {
case TouchType.Down:
this.lastX = touchX
this.lastY = touchY
this.drawStampLine(touchX, touchY, touchX, touchY)
break
case TouchType.Move:
// 在后台离屏画布连续高频贴图
this.drawStampLine(this.lastX, this.lastY, touchX, touchY)
// 将后台渲染结果打包,一次性刷新到主屏幕画布
const screenBitmap = this.offscreenCanvas.transferToImageBitmap()
this.context.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height)
this.context.drawImage(screenBitmap, 0, 0)
screenBitmap.close() // 及时关闭释放内存
this.lastX = touchX
this.lastY = touchY
break
}
})
Button('重置画布').onClick(() => {
if (this.offscreenCanvas && this.offCanvasContext) {
this.context.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height)
this.offCanvasContext.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height)
}
}).margin(15)
}
.width('100%')
.height('100%')
}
}
更多关于HarmonyOS 鸿蒙Next Canvas 绘制大量图片纹理笔记时,设备性能严重劣化的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
开发者您好,在高性能游戏开发、专业图形处理软件、桌面或移动应用开发、场景复杂、资源管理精细、硬件依赖强、与平台深度集成、定制化、性能要求高等场景,建议使用Native Drawing,可参考:
Drawing自绘制性能提升
提升不了多少,还是没法控制到1s内
等我再去研究研究
用Drawing还是不行,还是在 1s 以上,有啥好思路不?
2.5 万次 stamp draw 还想压到很低耗时,方向上不能只优化 Canvas 调用本身,要减少“每帧重复绘制的次数”。离屏渲染能避免卡主页面,但如果每次都完整重画 2.5 万个图元,耗时还是会线性上去。
建议按渲染引擎思路处理:1. 把笔记内容分块/分层缓存,例如按页、tile、缩放级别缓存成位图;2. 只重绘脏区域,不要每次操作都全量重绘;3. 相同纹理先做图集或缓存,避免重复解码/创建 PixelMap;4. 缩放/拖动时先显示低清缓存,停止交互后再补高清;5. 如果需要无限画布和大量纹理,评估 XComponent + 原生 Drawing/OpenGL 类方案,把批量绘制和缓存交给更底层的渲染链路。
2.5 万次 stamp draw 已经不是普通 ArkUI Canvas 逐帧重绘适合承载的量级了。离屏渲染只能避免主页面直接卡死,但如果每次仍然把 2.5 万个图元重新 rasterize,3 到 5 秒是很容易出现的。
建议按这几个方向拆:
-
先做视口裁剪,只绘制当前可见区域和少量缓冲区,不要全量绘制整张笔记。
-
把静态笔迹分块缓存成 tile,例如 256/512/1024 像素一块,变更哪个块只重绘哪个块。
-
相同纹理先解码/缩放成可复用 PixelMap,避免 draw 阶段重复转换资源。
-
缩放/拖动画布时先用缓存位图快速预览,手势结束后再补高质量重绘。
-
如果仍然是高频大规模自绘,优先评估 Native Drawing/XComponent,把热点绘制放到更底层。
核心不是换一个 API 就一定快,而是把“每帧 2.5 万次 draw”改成“只画脏块、只画可见块”。
只能说你是真勇呀!虽没看到你的应用,不了解你的功能,但是你这思路都感觉你够呛🤣,
哈哈,就只是绘画写字而已
HarmonyOS 鸿蒙Next Canvas绘制大量图片纹理导致性能劣化,通常因纹理内存未及时释放、单帧绘制过多位图导致GPU过载、未复用ImageBitmap或离屏画布引起。建议在绘制循环中复用纹理对象,使用OffscreenCanvas分层渲染,并采用LOD策略降低单帧渲染量。
针对Canvas中2.5万次stamp draw导致3~5秒绘制耗时的问题,性能瓶颈主要在绘制指令的调用次数上,而非纹理本身。即使采用离屏渲染,若每次仍单独draw,开销依然线性累积。
优化思路应聚焦 减少draw调用:
- 纹理合并(图集):将大量小图预先拼到一张或几张大的OffscreenCanvas上,绘制时直接从图集取区域drawImage,调用次数可降低几个数量级。
- 空间降采样与可见区域裁剪:仅绘制视口内的元素,并适当降低每英寸的stamp密度(如远离视图的元素更稀疏),2.5万次完全可以直接削减到可承受范围。
- 离屏缓存与脏矩形:若部分stamp不常变,可将静态区域渲染到离屏画布缓存,每帧仅重绘变化部分,再整体贴回主画布,减少无效重绘。
- 分层渲染:将大量静态stamp合并为一层,动态stamp独立层,更新时只操作动态层。
在HarmonyOS Next中,可利用OffscreenCanvas完成上述图集预合并和缓存,主Canvas仅执行少数几次drawImage,从根本上压缩总绘制时间。

