1# 复杂绘制场景下使用Native Drawing自绘制能力替代Canvas提升性能 2 3<!--Kit: Common--> 4<!--Subsystem: Demo&Sample--> 5<!--Owner: @mgy917--> 6<!--Designer: @jiangwensai--> 7<!--Tester: @Lyuxin--> 8<!--Adviser: @huipeizi--> 9 10## 简介 11 12[Canvas](../reference/apis-arkui/arkui-ts/ts-components-canvas-canvas.md) 画布组件是用来显示自绘内容的组件,它具有保留历史绘制内容、增量绘制的特点。Canvas 有 [CanvasRenderingContext2D](../reference/apis-arkui/arkui-ts/ts-canvasrenderingcontext2d.md)/[OffscreenCanvasRenderingContext2D](../reference/apis-arkui/arkui-ts/ts-offscreencanvasrenderingcontext2d.md) 和 [DrawingRenderingContext](../reference/apis-arkui/arkui-ts/ts-drawingrenderingcontext.md) 两套API,应用使用两套API绘制的内容都可以在绑定的 Canvas 组件上显示。其中 CanvasRenderingContext2D 按照W3C标准封装了 [Native Drawing](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md) 接口,可以方便快速复用web应用的绘制逻辑,因此非常适用于web应用和游戏、快速原型设计、数据可视化、在线绘图板、教学工具或创意应用等场景。然而,由于它的性能依赖于浏览器的实现,不如原生API那样接近硬件,因此对于性能要求比较高绘制比较复杂或者硬件依赖性比较强的场景如高性能游戏开发、专业图形处理软件、桌面或移动应用等,使用 Canvas CanvasRenderingContext2D 绘制会存在一定的卡顿、掉帧等性能问题,此时可以直接使用 Native Drawing 接口自绘制替代 Canvas 绘制来提升绘制性能。 13 14| 方案 | 适用场景 | 特点 | 15| ------------------------- | -------- | ------------------------------------------------------------ | 16| 适用于Canvas CanvasRenderingContext2D绘制 | web应用和游戏、快速原型设计、数据可视化、在线绘图板、教学工具或创意应用 | 场景简单、跨平台、快捷灵活、兼容性强、开发维护成本低、性能要求低 | 17| 适用于Native Drawing绘制 | 高性能游戏开发、专业图形处理软件、桌面或移动应用 | 场景复杂、资源管理精细、硬件依赖强、与平台深度集成、性能要求高 | 18 19## 原理机制 20由于 Canvas 的 CanvasRenderingContext2D 绘制本质上是对 Native Drawing 接口的封装,相对于直接使用 Native Drawing 接口,使用 Canvas 的 CanvasRenderingContext2D 绘制多了一层接口的调用,并且它依赖于浏览器的具体实现。如果图片绘制比较复杂,执行的绘制指令可能会成倍数的增长,进而绘制性能下降的更加严重,导致卡顿、掉帧等问题。下面我们以实现在背景图上绘制1000个透明空心圆的玻璃效果来对比两者的性能差异。 21 22## 场景示例 23 24 25 26上图是一个绘制1000个透明空心圆与背景图融合的绘制场景,下面我们分别使用 Canvas 的 CanvasRenderingContext2D(反例) 和 Native 侧的 Drawing(正例) 来实现该场景,并分析两者的性能差异。 27 28- **反例(使用Canvas 的 CanvasRenderingContext2D绘制)** 29 30Canvas 的 CanvasRenderingContext2D 绘制使用 [globalCompositeOperation](../reference/apis-arkui/arkui-js/js-components-canvas-canvasrenderingcontext2d.md#globalcompositeoperation) 属性来实现各种图层混合模式。此处将该属性值设置为 destination-out 来实现透明空心圆的玻璃融合效果。具体实现步骤如下: 31 321、使用自定义组件 GlassCoverView 来实现透明圆圈。 在首页点击"Begin Draw"按钮,随机生成1000个0-1的位置列表。 33 34```ts 35// \entry\src\main\ets\pages\Index.ets 36import GlassCoverView from '../view/GlassCoverView'; 37 38@Entry 39@Component 40struct Index { 41 @State pointsToDraw: number[][] = []; 42 43 build() { 44 Stack() { 45 Image($r('app.media.drawImage')) 46 .width('100%') 47 .height('100%') 48 // 透明圆圈自定义组件,在此组件中绘制1000个透明空心圆 49 GlassCoverView({ pointsToDraw: this.pointsToDraw }) 50 .width('100%') 51 .height('100%') 52 Button('Begin Draw') 53 .width(100) 54 .height(40) 55 .margin({ bottom: 40 }) 56 .onClick(() => { 57 this.startDraw(); 58 }) 59 }.alignContent(Alignment.Bottom) 60 .width('100%') 61 .height('100%') 62 } 63 // 随机生成1000个0-1的位置列表 64 private startDraw() { 65 this.pointsToDraw = []; 66 for (let index = 0; index < 1000; index++) { 67 this.pointsToDraw.push([Math.random(), Math.random()]); 68 } 69 } 70} 71``` 72 732、GlassCoverView子页面使用@Watch装饰器,监控到母页面位置列表数据pointsToDraw更新后,在屏幕上绘制1000个透明空心圆圈(具体参见 onDraw() 方法)。 74 75```ts 76// \entry\src\main\ets\view\GlassCoverView.ets 77 78/** 79 * 玻璃蒙层效果 80 */ 81 82@Preview 83@Component 84export default struct GlassCoverView { 85 /** 86 * 位置列表,x、y都在[0,1]之间 87 */ 88 @Prop 89 @Watch('onDraw') 90 pointsToDraw: number[][] = []; 91 private settings = new RenderingContextSettings(true); 92 private renderContext = new CanvasRenderingContext2D(this.settings); 93 private viewWidth: number = 0; 94 private viewHeight: number = 0; 95 96 build() { 97 Stack() { 98 Canvas(this.renderContext) 99 .width('100%') 100 .height('100%') 101 .onAreaChange((_: Area, newValue: Area) => { 102 this.handleAreaChange(newValue); 103 }) 104 } 105 .height('100%') 106 .width('100%') 107 } 108 109 private handleAreaChange(area: Area) { 110 this.viewWidth = parseInt(area.width.toString()); 111 this.viewHeight = parseInt(area.height.toString()); 112 this.onDraw(); 113 } 114 115 private onDraw() { 116 const canvas = this.renderContext; 117 if (canvas === undefined) { 118 return; 119 } 120 hiTraceMeter.startTrace('chisj slow', 1); 121 console.warn('chisj debug: slow start'); 122 // 保存绘图上下文 123 canvas.save(); 124 // 删除指定区域内的绘制内容 125 canvas.clearRect(0, 0, this.viewWidth, this.viewHeight); 126 // 指定绘制的填充色 127 canvas.fillStyle = '#77CCCCCC'; 128 // 填充一个矩形 129 canvas.fillRect(0, 0, this.viewWidth, this.viewHeight); 130 // 绘制空心圆圈 131 canvas.globalCompositeOperation = 'destination-out'; 132 canvas.fillStyle = '#CCCCCC'; 133 this.pointsToDraw.forEach((xy: number[]) => { 134 this.drawOneCell(canvas, xy[0] * this.viewWidth, xy[1] * this.viewHeight, 5); 135 }) 136 // 对保存的绘图上下文进行恢复 137 canvas.restore(); 138 console.warn('chisj debug: slow end'); 139 hiTraceMeter.finishTrace('chisj slow', 1); 140 } 141 142 // 根据指定的位置及宽度绘制圆 143 private drawOneCell(canvas: CanvasRenderer, x: number, y: number, width: number) { 144 canvas.beginPath(); 145 // 绘制弧线路径 146 canvas.arc(x, y, width, 0, Math.PI * 2); 147 canvas.closePath(); 148 canvas.fill(); 149 } 150} 151``` 152 153使用Canvas 的 CanvasRenderingContext2D 绘制trace图 154 155 156 157从图可以看到绘制1000个圆圈耗时34.1毫秒。 158 159- **正例(使用Native侧Drawing绘制)** 160 161[Native Drawing](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md) 主要使用分层接口 [OH_Drawing_CanvasSaveLayer](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md#oh_drawing_canvassavelayer) 和融合接口 [OH_Drawing_BrushSetBlendMode](../reference/apis-arkgraphics2d/capi-drawing-brush-h.md#oh_drawing_brushsetblendmode) 来实现多图融合效果。通过在前端创建一个自绘制节点 [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md), 并将图形绘制上下文及背景图参数通过 Native 侧暴露的接口传入,由 Native 侧使用相应的 Drawing 接口进行绘制,具体实现步骤如下: 162### 前端实现 1631、前端定义一个 [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) 自绘制渲染节点,将背景图 this.pMap 和图形绘制上下文 context 传入 Native,调用 Native 侧的 nativeOnDraw 接口进行绘制。 164 165```ts 166// entry\src\main\ets\pages\Index.ets 167enum DrawType { NONE, PATH, TEXT, IMAGE }; 168 169class MyRenderNode extends RenderNode { 170 private drawType: DrawType = DrawType.NONE; 171 private pMap: image.PixelMap | undefined = undefined; 172 private uiContext: UIContext | undefined = undefined; 173 174 draw(context: DrawContext) { 175 // 调用Native侧的nativeOnDraw接口进行绘制,将背景图 this.pMap 和图形绘制上下文 context 作为参数传入 176 testNapi.nativeOnDraw(666, context, this.uiContext?.vp2px(this.size.width), 177 this.uiContext?.vp2px(this.size.height), this.drawType, this.pMap); 178 } 179 180 setUIContext(context: UIContext) { 181 this.uiContext = context; 182 } 183 184 // 设置绘制类型 185 resetType(type: DrawType) { 186 this.drawType = type; 187 } 188 // 设置背景图 189 setPixelMap(p: PixelMap) { 190 this.pMap = p; 191 } 192} 193``` 194 1952、新建一个自绘制渲染节点,并定义一个 [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md),对该节点进行管理。 196```ts 197// entry\src\main\ets\pages\Index.ets 198// 创建一个 MyRenderNode 对象 199const newNode = new MyRenderNode(); 200// 定义 newNode 的大小和位置 201newNode.frame = { 202 x: 0, 203 y: 0, 204 width: 980, 205 height: 1280 206}; 207 208class MyNodeController extends NodeController { 209 private rootNode: FrameNode | null = null; 210 211 makeNode(uiContext: UIContext): FrameNode | null { 212 this.rootNode = new FrameNode(uiContext); 213 if (this.rootNode === null) { 214 return null; 215 } 216 const renderNode = this.rootNode.getRenderNode(); 217 if (renderNode !== null) { 218 renderNode.appendChild(newNode); 219 } 220 return this.rootNode; 221 } 222} 223``` 224 2253、在页面中将自绘制节点挂载到 [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md) 上。 226 227```ts 228@Entry 229@Component 230struct Index { 231 private myNodeController: MyNodeController = new MyNodeController(); 232 233 aboutToAppear(): void { 234 const context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext; 235 const resourceMgr: resourceManager.ResourceManager = context.resourceManager; 236 resourceMgr.getRawFileContent('drawImage.png').then((fileData: Uint8Array) => { 237 console.info('success in getRawFileContent'); 238 const buffer = fileData.buffer.slice(0); 239 const imageSource: image.ImageSource = image.createImageSource(buffer); 240 imageSource.createPixelMap().then((pMap: image.PixelMap) => { 241 newNode.setUIContext(this.getUIContext()); 242 // 自绘制渲染节点背景图 243 newNode.setPixelMap(pMap); 244 245 }).catch((err: BusinessError) => { 246 console.error('fail to create PixelMap'); 247 }).catch((err: BusinessError) => { 248 console.error('fail to getRawFileContent'); 249 }) 250 }) 251 } 252 253 build() { 254 Column() { 255 Row() { 256 // 将自绘制渲染节点挂载到 NodeContainer 257 NodeContainer(this.myNodeController) 258 .height('100%') 259 } 260 .width('100%') 261 .height('80%') 262 263 Row() { 264 Button('Draw Image') 265 .margin({ bottom: 50, right: 12 }) 266 .onClick(() => { 267 newNode.resetType(DrawType.IMAGE); 268 newNode.invalidate(); 269 }) 270 } 271 .width('100%') 272 .justifyContent(FlexAlign.Center) 273 .shadow(ShadowStyle.OUTER_DEFAULT_SM) 274 .alignItems(VerticalAlign.Bottom) 275 .layoutWeight(1) 276 } 277 } 278} 279 280``` 281 282### Native侧实现 2831、Native 侧暴露绘制接口 nativeOnDraw 供前端调用,该接口绑定 Native侧的 OnDraw 函数,ArkTs传入的参数在该函数中处理。 284```C++ 285EXTERN_C_START 286static napi_value Init(napi_env env, napi_value exports) { 287 napi_property_descriptor desc[] = { 288 {"nativeOnDraw", nullptr, OnDraw, nullptr, nullptr, nullptr, napi_default, nullptr}}; 289 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); 290 return exports; 291} 292EXTERN_C_END 293``` 294 2952、在 OnDraw 函数中接收前端传入的参数,此处主要是图形绘制上下文及背景图参数。 296```C++ 297static napi_value OnDraw(napi_env env, napi_callback_info info) { 298 size_t argc = 6; 299 napi_value args[6] = {nullptr}; 300 301 napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); 302 303 int32_t id; 304 napi_get_value_int32(env, args[0], &id); 305 306 // 图形绘制上下文参数 307 void *temp = nullptr; 308 napi_unwrap(env, args[1], &temp); 309 OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas *>(temp); 310 311 int32_t width; 312 napi_get_value_int32(env, args[2], &width); 313 314 int32_t height; 315 napi_get_value_int32(env, args[3], &height); 316 317 DRAWING_LOGI("OnDraw, width:%{public}d, helght:%{public}d", width, height); 318 int32_t drawOption; 319 napi_get_value_int32(env, args[4], &drawOption); 320 // 背景图参数 321 NativePixelMap *nativePixelMap = OH_PixelMap_InitNativePixelMap(env, args[5]); 322 if (drawOption == IMAGE) { 323 // 调用绘制图形融合接口进行绘制 324 NativeOnDrawPixelMap(canvas, nativePixelMap, width, height); 325 } 326 return nullptr; 327} 328``` 329 3303、在 NativeOnDrawPixelMap 函数中实现透明圆圈绘制(主要使用分层接口 [OH_Drawing_CanvasSaveLayer](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md#oh_drawing_canvassavelayer) 和融合接口 [OH_Drawing_BrushSetBlendMode](../reference/apis-arkgraphics2d/capi-drawing-brush-h.md#oh_drawing_brushsetblendmode) 来实现多图融合效果)。 331```C++ 332// entry\src\main\cpp\native_bridge.cpp 333 334enum DrawType { NONE, PATH, TEXT, IMAGE }; 335#define DRAW_MAX_NUM 1000 // 最大绘制圆圈数量 336 337// 随机生成坐标位置 338static int RangedRand(int range_min, int range_max) { 339 int r = ((double)rand() / RAND_MAX) * (range_max - range_min) + range_min; 340 return r; 341} 342 343static void NativeOnDrawPixelMap(OH_Drawing_Canvas *canvas, NativePixelMap *nativeMap) { 344 // 画背景图 345 OH_Drawing_CanvasSave(canvas); 346 OH_Drawing_PixelMap *pixelMap = OH_Drawing_PixelMapGetFromNativePixelMap(nativeMap); 347 // 创建采样选项对象 348 OH_Drawing_SamplingOptions *sampling = OH_Drawing_SamplingOptionsCreate(FILTER_MODE_NEAREST, MIPMAP_MODE_NONE); 349 // 获取背景图原图区域 350 OH_Drawing_Rect *src = OH_Drawing_RectCreate(0, 0, 360, 693); 351 // 创建渲染区域 352 OH_Drawing_Rect *dst = OH_Drawing_RectCreate(0, 0, 1300, 2500); 353 // 创建画刷 354 OH_Drawing_Brush *brush = OH_Drawing_BrushCreate(); 355 OH_Drawing_CanvasAttachBrush(canvas, brush); 356 // 将背景图渲染到画布指定区域 357 OH_Drawing_CanvasDrawPixelMapRect(canvas, pixelMap, src, dst, sampling); 358 OH_Drawing_CanvasDetachBrush(canvas); 359 360 // 调用分层接口 361 OH_Drawing_CanvasSaveLayer(canvas, dst, brush); 362 363 // 画蒙层 364 OH_Drawing_Rect *rect2 = OH_Drawing_RectCreate(0, 0, 1300, 2500); 365 OH_Drawing_Brush *brush2 = OH_Drawing_BrushCreate(); 366 // 设置画刷颜色 367 OH_Drawing_BrushSetColor(brush2, OH_Drawing_ColorSetArgb(0x77, 0xCC, 0xCC, 0xCC)); 368 OH_Drawing_CanvasAttachBrush(canvas, brush2); 369 OH_Drawing_CanvasDrawRect(canvas, rect2); 370 OH_Drawing_CanvasDetachBrush(canvas); 371 372 OH_Drawing_Point *pointArray[DRAW_MAX_NUM]; 373 int x = 0; 374 int y = 0; 375 for (int i = 0; i < DRAW_MAX_NUM; i++) { 376 // 生成随机坐标 377 x = RangedRand(0, 1300); 378 y = RangedRand(0, 2500); 379 pointArray[i] = OH_Drawing_PointCreate(x, y); 380 } 381 382 OH_Drawing_Point *point = OH_Drawing_PointCreate(800, 1750); 383 OH_Drawing_Brush *brush3 = OH_Drawing_BrushCreate(); 384 // 设置圆圈的画刷和混合模式 385 OH_Drawing_BrushSetBlendMode(brush3, BLEND_MODE_DST_OUT); 386 OH_Drawing_CanvasAttachBrush(canvas, brush3); 387 // 画圈 388 for (int i = 0; i < DRAW_MAX_NUM; i++) { 389 OH_Drawing_CanvasDrawCircle(canvas, pointArray[i], 15); 390 } 391 392 // 销毁对象 393 OH_Drawing_CanvasDetachBrush(canvas); 394 OH_Drawing_RectDestroy(rect2); 395 OH_Drawing_BrushDestroy(brush2); 396 OH_Drawing_BrushDestroy(brush3); 397 OH_Drawing_PointDestroy(point); 398 OH_Drawing_BrushDestroy(brush); 399 OH_Drawing_CanvasRestore(canvas); 400 OH_Drawing_SamplingOptionsDestroy(sampling); 401 OH_Drawing_RectDestroy(src); 402 OH_Drawing_RectDestroy(dst); 403} 404 405``` 406 407使用Native侧Drawing绘制trace图 408 409 410 411从图可以看到绘制1000个圆圈耗时1.2毫秒,相较于 Canvas 的 CanvasRenderingContext2D 绘制有较大的性能提升。 412 413## 效果对比 414 415| 方案 | 圆圈数量 | 耗时 | 416| ------------------------- | -------- | ------------------------------------------------------------ | 417| Canvas 的 CanvasRenderingContext2D画透明圈(反例) | 1000 | 34.1毫秒 | 418| Native Drawing画透明圈(正例) | 1000 | 1.2毫秒 | 419 420通过上述对比可以发现,在实现较大数量透明空心圆绘制时,相比于Canvas 的 [CanvasRenderingContext2D](../reference/apis-arkui/arkui-ts/ts-canvasrenderingcontext2d.md),使用 [Native Drawing](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md) 绘制可以得到明显的性能提升。以上只是实现透明空心圆,针对实心圆及其他场景(如 [globalCompositeOperation](../reference/apis-arkui/arkui-js/js-components-canvas-canvasrenderingcontext2d.md#globalcompositeoperation) 属性的其他值),由于实现机制的不同,绘制的指令数量也存在差异,从而性能数据会存在一些差异。实际应用中,我们可以根据自己的需要等实际情况,在对性能要求不高的情况下采用 Canvas 的 CanvasRenderingContext2D 绘制,如果对性能要求比较高或者比较依赖于硬件,建议使用 [Native Drawing](../reference/apis-arkgraphics2d/capi-drawing-canvas-h.md) 进行绘制。