• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![](./figures/drawing-canvas.gif)
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![](./figures/drawing_with_canvas.png)
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![](./figures/drawing_with_ndk.png)
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)  进行绘制。