利用鸿蒙NAPI实现高效绘制技术

325 阅读16分钟

一、什么是NAPI

NAPI(Node-API)是一个用于 Node.js 的官方 C-API,旨在简化 Node.js 插件开发的过程并提高其可移植性。NAPI 允许开发者编写独立于 Node.js 版本的插件,从而使插件更加稳定和可靠。

通俗一点理解:NAPI是js可以调用c/c++语言实现(或者c/c++语言调用js)的一套接口。类似java的jni。

二、为什么要使用NAPI

  1. 绘制效率

      a)效率不高的理论基础

    • 目前版本的鸿蒙上有两种不同的开发范式,类 Web 开发范式和ArkTs声明式开发范式,对应两种不同的渲染方式。
开发范式名称语言生态UI更新方式适用场景适用人群
声明式开发范式ArkTS语言数据驱动更新复杂度较大、团队合作度较高的程序移动系统应用开发人员、系统应用开发人员
类Web开发范式JS语言数据驱动更新界面较为简单的程序应用和卡片Web前端开发人员

ArkTs声明式开发范式架构图

  • 声明式UI前端
  • 提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。
  • 语言运行时
  • 选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。
  • 声明式UI后端引擎
  • 后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。
  • 渲染引擎
  • 提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。
  • 平台适配层
  • 提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。

类 Web 开发范式架构图

  • Application
  • 应用层表示开发者开发的FA应用,这里的FA应用特指JS FA应用。
  • Framework
  • 前端框架层主要完成前端页面解析,以及提供MVVM(Model-View-ViewModel)开发模式、页面路由机制和自定义组件等能力。
  • Engine
  • 引擎层主要提供动画解析、DOM(Document Object Model)树构建、布局计算、渲染命令构建与绘制、事件管理等能力。
  • Porting Layer
  • 适配层主要完成对平台层进行抽象,提供抽象接口,可以对接到系统平台。比如:事件对接、渲染管线对接和系统生命周期对接等。

从上面的架构图可以看出,无论是ArkTs声明式开发范式还是类 Web 开发范式的js形式,绘制操作都需要通过中间语言的解释层到渲染引擎。

在考虑纯粹的渲染效率而不涉及开发效率时,从理论上来说,NAPI方式相对于以上存在中间层的方式具有更高的渲染效率。以下是一些可能的原因:

原因分析:

  1. 直接底层访问

    1. NAPI 允许开发者直接调用底层系统的API和硬件加速功能,如GPU加速。这样可以更直接地利用系统和硬件资源,提升渲染效率。
  2. 优化和控制

    1. 使用 NAPI 可以更精确地控制和优化渲染流程。开发者可以直接管理内存和数据处理过程,避免一些中间语言绘制过程中的中间层和额外开销。
  3. 执行速度

    1. NAPI 调用本身在执行时通常更为高效,因为它避免了ArkTs的运行时和JS引擎的解释器和即时编译器的处理,直接执行原生的机器码或近似的机器码。
  4. 复杂图形处理

    1. 对于复杂的图形处理需求,如大规模数据可视化、3D渲染和游戏引擎,使用NAPI可以更好地处理复杂的计算和图形操作,提升渲染效率。

b)效率不高的实际情况

以下是一组绘制不同方法ArkTs绘制和Native绘制的对比数据:

绘制方式Path (微秒)1次Text (微秒)1次rect (微秒)1000次
JsDraw468.8641236.9929330.304
NativeDraw79.616731.520458.624

我们可以结合绘制代码来看鸿蒙绘制的原理

ArkTs绘制Path

drawPathJS(context: DrawContext) {
  let startTime = systemDateTime.getTime(true)

  const canvas = context.canvas
  let height_ = 1200
  let width_ = 600
  let len = height_ / 4
  let aX = width_ / 2
  let aY = height_ / 4
  let dX = aX - len * Math.sin(18.0)
  let dY = aY + len * Math.cos(18.0)
  let cX = aX + len * Math.sin(18.0)
  let cY = dY
  let bX = aX + (len / 2.0)
  let bY = aY + Math.sqrt((cX - dX) * (cX - dX) + (len / 2.0) * (len / 2.0))
  let eX = aX - (len / 2.0)
  let eY = bY;

  // 创建一个path对象,然后使用接口连接成一个五角星形状
  let path = new drawing.Path()

  // 指定path的起始位置
  path.moveTo(aX, aY)

  // 用直线连接到目标点
  path.lineTo(bX, bY)
  path.lineTo(cX, cY)
  path.lineTo(dX, dY)
  path.lineTo(eX, eY)

  // 闭合形状,path绘制完毕
  path.close()

  // 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制
  let pen = new drawing.Pen()
  pen.setAntiAlias(true)
  let pen_color: common2D.Color = {
    alpha: 0xFF,
    red: 0xFF,
    green: 0x00,
    blue: 0x00
  }
  pen.setColor(pen_color)
  pen.setStrokeWidth(10.0)

  // 将Pen画笔设置到canvas中
  canvas.attachPen(pen)

  // 创建一个画刷Brush对象,Brush对象用于形状的填充
  let brush = new drawing.Brush()
  let brush_color: common2D.Color = {
    alpha: 0xFF,
    red: 0x00,
    green: 0xFF,
    blue: 0x00
  }
  brush.setColor(brush_color)

  // 将Brush画刷设置到canvas中
  canvas.attachBrush(brush)

  canvas.drawPath(path)

  let endTime = systemDateTime.getTime(true)
  let useTime = endTime - startTime
  console.log("draw use time in JsDraw Path", useTime);
}

Native绘制Path

static napi_value OnDrawPathNative(napi_env env, napi_callback_info info) {
  size_t argc = 3;
  napi_value args[3] = {nullptr};
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

  // 获取 Canvas 指针
  void *temp = nullptr;
  napi_unwrap(env, args[0], &temp);
  OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas *>(temp);

  // 获取 Canvas 宽度
  int32_t width;
  napi_get_value_int32(env, args[1], &width);

  // 获取 Canvas 高度
  int32_t height;
  napi_get_value_int32(env, args[2], &height);

  double height_ = 1200;
  double width_ = 600;
  double len = height_ / 4;
  double aX = width_ / 2;
  double aY = height_ / 4;
  double dX = aX - len * sin(18.0);
  double dY = aY + len * cos(18.0);
  double cX = aX + len * sin(18.0);
  double cY = dY;
  double bX = aX + (len / 2.0);
  double bY = aY + sqrt((cX - dX) * (cX - dX) + (len / 2.0) * (len / 2.0));
  double eX = aX - (len / 2.0);
  double eY = bY;

  // 创建一个path对象,然后使用接口连接成一个五角星形状
  auto path = OH_Drawing_PathCreate();


  OH_Drawing_PathMoveTo(path, aX, aY);
  OH_Drawing_PathLineTo(path, bX, bY);
  OH_Drawing_PathLineTo(path, cX, cY);
  OH_Drawing_PathLineTo(path, dX, dY);
  OH_Drawing_PathLineTo(path, eX, eY);
  OH_Drawing_PathClose(path);

  auto pen = OH_Drawing_PenCreate();
  OH_Drawing_PenSetAntiAlias(pen, true);
  OH_Drawing_PenSetColor(pen, OH_Drawing_ColorSetArgb(0x00, 0x00, 0xFF, 0x00));
  OH_Drawing_PenSetWidth(pen, 10);
  // 将Pen画笔设置到canvas中
  OH_Drawing_CanvasAttachPen(canvas, pen);


  auto brush = OH_Drawing_BrushCreate();
  OH_Drawing_BrushSetColor(brush, OH_Drawing_ColorSetArgb(0xFF, 0x00, 0xFF, 0x00));
  OH_Drawing_CanvasAttachBrush(canvas, brush);
  OH_Drawing_CanvasDrawPath(canvas, path);

  OH_Drawing_CanvasDetachPen(canvas);
  OH_Drawing_CanvasDetachBrush(canvas);

  OH_Drawing_PathDestroy(path);
  OH_Drawing_PenDestroy(pen);
  OH_Drawing_BrushDestroy(brush);

  return nullptr;
}
  • 两组绘制的效果是一致的。我们可以发现两组调用的api方法基本是一致的。这是因为ArkTs的每一个方法都是对应Native的一个实现。比如path.lineTo(bX, bY)其实对应就是调用Native层的 OH_Drawing_PathLineTo(path, bX, bY);。所以上面数据为什么Path的性能ArkTs对比Native要差一些,是因为drawPath需要调用比较多的Api,ArkTs的方式造成了过多的ArkTs与Native交互,而纯Native的绘制直接调用的Native的Api,少了一层ArkTs的调用,所以整体性能会好很多。

结论: ArkTs的每次调用都是调用鸿蒙本身的Native层,所以比起直接调用Native层始终是多一层调用。多一层性能损耗。性能上还是比直接在native层绘制要差一些。

c)js的执行只能在UI线程,所以异步绘制的情况要考虑清楚

三方库能力复用

一些应用会依赖一些c/c++实现的第三方跨平台库,尤其是绘制相关的库,为了可以复用这部分跨平台的能力就可以使用NAPI的绘制能力来桥接这些跨平台库。

三、鸿蒙上的绘制接口

鸿蒙上具有绘制功能的组件主要有以下几种:

绘制接口CanvasRenderingContext2DOffscreenCanvasRenderingContext2DWebGLRenderNodeXcomponent
语言接口js/ArkTsjs/ArkTsjsArkTs/cc
开发范式类 Web 开发范式/ArkTs声明式开发范式类 Web 开发范式/ArkTs声明式开发范式类 Web 开发范式ArkTs声明式开发范式ArkTs声明式开发范式
绘制触发方式被动绘制主动绘制主动绘制被动绘制主动绘制
是否支持硬件加速
渲染方式gpucpugpugpucpu
  • 简单方法判断cpu绘制还是gpu绘制:关注绘制的结果是在内存中还是显存中,因为gpu是没办法直接访问内存的,所以绘制结果会在显存中。只要结果在内存中的基本可以确定是cpu绘制。比如常见的双缓冲绘制,bitmap的创建和保存都是在内存中操作,所以这种基本都是cpu绘制。

主动绘制:

工作方式:

  • 主动绘制是指开发者明确控制绘制的时机和方式。开发者通过调用绘制命令来告诉系统或设备如何渲染图形或场景。

特点:

  • 明确控制:开发者有完全的控制权,可以精确地决定每一帧的绘制内容。

  • 适用于实时交互:适用于需要快速响应用户输入或实时更新的应用场景,如游戏、动画等。

  • 性能可控:开发者可以通过优化绘制逻辑、减少不必要的绘制操作等方式,提高整体性能。

  • 举例:android上的Surfaceview就是主动绘制,可以一直主动调用绘制方法把绘制的内容送显,实时刷新ui界面。帧率不受系统刷新率的影响。不管是60帧率还是120帧率,每秒绘制多少帧都是由开发者控制。

被动绘制:

工作方式:

  • 被动绘制是指绘制操作由系统或设备自动触发或处理,开发者不直接控制每一帧的绘制时机。

特点:

  • 系统控制:绘制的触发和执行由系统或设备决定,可能会受到系统资源和调度的影响。

  • 适用于静态或间歇更新:适用于静态内容或需要较少更新频率的场景,如图像查看器、静态数据可视化等。

  • 资源效率:系统可以根据需要进行优化和调度,以最大化资源利用效率。

  • 举例:android上的普通view,普通view的刷新依赖于onDraw方法的调用频次。因为view内容的刷新是通过重写onDraw方法来实现的。而onDraw方法的调用频次是由系统的刷新率决定的。在帧率(60帧/s)的设置下。每帧的绘制时间大约是16.7ms,所以会有绘制时间要在16ms以内,否则会ui卡顿丢帧。这都是因为被动刷新机制的原因。而在系统设置是120帧率情况下,每帧的绘制时间约为8ms。被动刷新叫做被动的主要原因就是绘制送显的触发时机是由系统决定而不是开发者决定。

四、如何利用NAPI实现高效绘制

  哪些组件可以使用 NAPI 绘制

  • Xcomponent
  • RenderNode

  鸿蒙上可以使用 NAPI 的绘制组件主要是以上两个。

  Xcomponent主要用于c接口的绘制,类似android中的SurfaceView,拥有单独的NativeWindow,绘制方式是主动绘制方式

  RenderNode不仅支持c接口的绘制,还支持ArkTs接口的绘制,所以RenderNode可是实现混合绘制,也就是ArkTs绘制一部分,c绘制一部分。另外RenderNode使用的是gpu绘制,支持硬件加速,所以在刷新率不高的场景绘制效率比较高,类似于android中的普通view。因为Xcomponent是属于主动绘制,所以可以实现非常高的帧率以及可以实现自定义帧率。

  所以Xcomponent通常比较适用于视频和游戏等,需要频繁刷新画面的场景,而RenderNode适合于文本图片等不需要频繁刷新画面的场景。

  绘制渲染流程

  • RenderNode

whiteboard_exported_image.png

  • Xcomponent

whiteboard_exported_image (1).png

答疑:为什么Xcomponent是一次绘制结束?而不是一帧绘制结束?

  答:因为在一次绘制中可以多次送显,每次送显就是一帧,所以可能一次绘制会出现绘制多帧。

  提供了哪些绘制能力

  两种组件提供的绘制能力基本是一致的,基本的文本、路径、形状、图片等基础2D渲染需要的能力都支持。

  具体可以参考华为文档:

Xcomponent

    developer.huawei.com/consumer/cn…

  Canvas

    developer.huawei.com/consumer/cn…

  drawing相关方法

    developer.huawei.com/consumer/cn…

  如何使用napi进行绘制

  RenderNode 方式


ArkTs侧:
import { DrawContext, RenderNode } from '@ohos.arkui.node';
import drawNapi, { DrawConfig } from 'libtest.so';

export  class XNodeRender extends RenderNode {
private drawConfig: DrawConfig | undefined = undefined;

draw(context: DrawContext) {
  drawNapi.napiRenderNodeOnDraw(context, this.drawConfig);
}

constructor() {
  super()
}

public setDrawInfo(drawConfig: DrawConfig) {
  this.drawConfig = drawConfig
}
}

c++侧:
void RenderNodeRender::Register(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {{"napiRenderNodeOnDraw", nullptr, RenderNodeRender::napiOnDraw, nullptr, nullptr,
                                    nullptr, napi_default, nullptr}};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
}

napi_value RenderNodeRender::napiOnDraw(napi_env env, napi_callback_info info) {
napi_value thisArg;
size_t argc = 1;
napi_value args[1] = {nullptr};

if (napi_get_cb_info(env, info, &argc, args, &thisArg, nullptr) != napi_ok) {
  return nullptr;
}

// 获取 Canvas 指针
void *temp = nullptr;
napi_unwrap(env, args[0], &temp);
OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas *>(temp);

void *drawConfig = nullptr;
napi_unwrap(env, args[0], &drawConfig);
DrawConfig *drawConfigPtr = reinterpret_cast<DrawConfig *>(drawConfig);
if (drawConfigPtr == nullptr) {
  return nullptr;
}

OH_Drawing_CanvasSave(canvas);
OH_Drawing_CanvasTranslate(canvas, 0, 0);
OH_Drawing_CanvasScale(canvas, density, density);
GraphicContext graphicContext(canvas, drawConfigPtr, density);
//draw 的业务逻辑

OH_Drawing_CanvasRestore(canvas);
return nullptr;
}

RenderNodeRender::RenderNodeRender() {}

RenderNodeRender::~RenderNodeRender() {}

Xcomponent 方式


ArkTs侧
import XComponentRender from '../render/XComponentRender';
import { DrawConfig } from '../model/TestModel';


export interface XComponentBookViewArgs {
  drawConfig: DrawConfig
}

@Component
export  struct XcomponentBookView {
  @Watch("onArgsChanged") @Consume('drawConfig') drawConfig: DrawConfig
  private xComponentRenderView: XComponentRender | undefined = undefined;
  xcomponentController: XComponentController = new XComponentController()

  onArgsChanged() {
    if (this.xComponentRenderView) {
      this.xComponentRenderView.napiXcompenentOnDraw(
        this.drawConfig
      );
    }
  }

  build() {
    Stack() {
      XComponent({
        id: 'XComponent1',
        type: XComponentType.SURFACE,
        libraryname: 'test'
      })
        .onLoad((xComponentContext) => {
          this.xComponentRenderView = xComponentContext as XComponentRender;
          this.onArgsChanged()
        }).width('100%').height('100%')
    }.width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }
}

import { DrawConfig } from 'libtest.so';

export  default interface XComponentRender {

  napiXcompenentSetWidth(width: number): void;

  napiXcompenentSetHeight(height: number): void;

  napiXcompenentOnDraw(drawConfig: DrawConfig): void;
};

c++侧
//
//
// Node APIs are not fully supported. To solve the compilation error of the interface cannot be found,
// please include "napi/native_api.h".

#include "xcomponent_render.h"
#include "utils/napi_utils.h"
#include "common/log_common.h"
#include <bits/alltypes.h>
#include <native_drawing/drawing_text_typography.h>
#include <map>
#include <sys/mman.h>
#include <native_drawing/drawing_pen.h>
#include <native_drawing/drawing_path.h>
#include "graphics/graphics_context.h"

namespace test_napi {

  std::map<OH_NativeXComponent *, XcomponentRender *> XcomponentRender::S_NativeXComponentToRenderView;
  OH_NativeXComponent_Callback XcomponentRender::S_RenderCallback;

  void XcomponentRender::Register(napi_env env, napi_value exports) {
    if ((env == nullptr) || (exports == nullptr)) {
      DRAWING_LOGE("Export: env or exports is null");
      return;
    }

    OH_NativeXComponent *nativeXComponent =
      (OH_NativeXComponent *)NapiUtils::GetNativeXComponentFromExports(env, exports);
    if (nativeXComponent == nullptr) {
      return;
    }

    S_RenderCallback.OnSurfaceCreated = XcomponentRender::OnSurfaceCreatedCB;

    S_RenderCallback.OnSurfaceDestroyed = XcomponentRender::OnSurfaceDestroyedCB;
    // Callback must be initialized
    S_RenderCallback.DispatchTouchEvent = nullptr;
    S_RenderCallback.OnSurfaceChanged = nullptr;
    OH_NativeXComponent_RegisterCallback(nativeXComponent, &S_RenderCallback);

    napi_property_descriptor desc[] = {
      {"napiXcompenentSetWidth", nullptr, XcomponentRender::napiSetWidth, nullptr, nullptr, nullptr, napi_default,
       nullptr},
      {"napiXcompenentSetHeight", nullptr, XcomponentRender::napiSetHeight, nullptr, nullptr, nullptr, napi_default,
       nullptr},
      {"napiXcompenentOnDraw", nullptr, XcomponentRender::napiDraw, nullptr, nullptr, nullptr, napi_default, nullptr}};
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    if (napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc) != napi_ok) {
      DRAWING_LOGE("Export: napi_define_properties failed");
    }
  }

  napi_value XcomponentRender::napiSetWidth(napi_env env, napi_callback_info info) {
    napi_value thisArg;
    if (napi_get_cb_info(env, info, nullptr, nullptr, &thisArg, nullptr) != napi_ok) {
      return nullptr;
    }
    XcomponentRender *renderView = nullptr;
    napi_unwrap(env, thisArg, (void **)&renderView);
    if (renderView == nullptr) {
      return nullptr;
    }
    std::vector<napi_value> args = NapiUtils::GetArgs(env, info);
    if (args.empty()) {
      return nullptr;
    }
    int width = NapiUtils::GetInt32Param(env, args[0]);
    renderView->setWidth(width);
    return nullptr;
  }

  napi_value XcomponentRender::napiSetHeight(napi_env env, napi_callback_info info) {
    napi_value thisArg;
    if (napi_get_cb_info(env, info, nullptr, nullptr, &thisArg, nullptr) != napi_ok) {
      return nullptr;
    }
    XcomponentRender *renderView = nullptr;
    napi_unwrap(env, thisArg, (void **)&renderView);
    std::vector<napi_value> args = NapiUtils::GetArgs(env, info);
    if (args.empty()) {
      return nullptr;
    }
    int height = NapiUtils::GetInt32Param(env, args[0]);
    renderView->setHeight(height);
    return nullptr;
  }

  napi_value XcomponentRender::napiDraw(napi_env env, napi_callback_info info) {
    napi_value thisArg;
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    if (napi_get_cb_info(env, info, &argc, args, &thisArg, nullptr) != napi_ok) {
      return nullptr;
    }

    OH_NativeXComponent *nativeXComponent =
      (OH_NativeXComponent *)NapiUtils::GetNativeXComponentFromExports(env, thisArg);
    if (nativeXComponent == nullptr) {
      return nullptr;
    }

    std::map<OH_NativeXComponent *, XcomponentRender *>::iterator iter =
      S_NativeXComponentToRenderView.find(nativeXComponent);
    if (iter == S_NativeXComponentToRenderView.end()) {
      return nullptr;
    }
    XcomponentRender *renderView = iter->second;

    void *drawConfig = nullptr;
    napi_unwrap(env, args[0], &drawConfig);
    DrawConfig *drawConfigPtr = reinterpret_cast<DrawConfig *>(drawConfig);
    if (drawConfigPtr == nullptr) {
      return nullptr;
    }
    renderView->draw(drawConfigPtr);
    return nullptr;
  }

  XcomponentRender::XcomponentRender() {}

  XcomponentRender::~XcomponentRender() {
    if (m_Canvas_ != nullptr) {
      OH_Drawing_CanvasDestroy(m_Canvas_);
    }
    if (m_Bitmap_ != nullptr) {
      OH_Drawing_BitmapDestroy(m_Bitmap_);
    }
    m_Canvas_ = nullptr;
    m_Bitmap_ = nullptr;
    m_Buffer_ = nullptr;
    m_BufferHandle_ = nullptr;
    m_NativeWindow_ = nullptr;
    m_MappedAddr_ = nullptr;
  }

  void XcomponentRender::OnSurfaceCreatedCB(OH_NativeXComponent *component, void *window) {
    DRAWING_LOGI("OnSurfaceCreatedCB");
    XcomponentRender *renderView = nullptr;
    std::map<OH_NativeXComponent *, XcomponentRender *>::iterator iter = S_NativeXComponentToRenderView.find(component);
    if (iter == S_NativeXComponentToRenderView.end()) {
      renderView = new XcomponentRender();
      S_NativeXComponentToRenderView[component] = renderView;
    } else {
      renderView = iter->second;
    }

    OHNativeWindow *nativeWindow = static_cast<OHNativeWindow *>(window);
    renderView->setWindow(nativeWindow);

    uint64_t width;
    uint64_t height;
    int32_t xSize = OH_NativeXComponent_GetXComponentSize(component, window, &width, &height);
    if (xSize == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
      renderView->setHeight(height);
      renderView->setWidth(width);
      renderView->init();
      DRAWING_LOGI("xComponent width = %{public}llu, height = %{public}llu", width, height);
    }
  }

  void XcomponentRender::OnSurfaceDestroyedCB(OH_NativeXComponent *component, void *window) {
    DRAWING_LOGI("OnSurfaceDestroyedCB");
    std::map<OH_NativeXComponent *, XcomponentRender *>::iterator iter = S_NativeXComponentToRenderView.find(component);
    if (iter == S_NativeXComponentToRenderView.end()) {
      return;
    }
    XcomponentRender *renderView = iter->second;
    renderView->release();
    S_NativeXComponentToRenderView.erase(iter);
    delete renderView;
  }

  void XcomponentRender::setWindow(OHNativeWindow *nativeWindow) { m_NativeWindow_ = nativeWindow; }

  void XcomponentRender::setWidth(uint64_t width) { m_ViveWidth_ = width; }

  void XcomponentRender::setHeight(uint64_t height) { m_ViewHeight_ = height; }

  void XcomponentRender::init() {
    // The nativeWindow here is obtained from the callback function in the previous step
    // Apply for a buffer for nativeWindows and get the OHNativeWindowBuffer instance to the buffer
    if (m_Buffer_ != nullptr) {
      OH_NativeWindow_NativeWindowAbortBuffer(m_NativeWindow_, m_Buffer_);
      m_Buffer_ = nullptr;
    }
    int ret = OH_NativeWindow_NativeWindowRequestBuffer(m_NativeWindow_, &m_Buffer_, &m_FenceFd_);
    DRAWING_LOGI("request buffer ret = %{public}d", ret);
    // Get the handle of the buffer, which is used to operate the buffer
    m_BufferHandle_ = OH_NativeWindow_GetBufferHandleFromNative(m_Buffer_);
    uint32_t bufferWidth = static_cast<uint32_t>(m_BufferHandle_->stride / 4);
    // Create a bitmap object
    m_Bitmap_ = OH_Drawing_BitmapCreate();
    // // Define the bitmap pixel format object, including the color type and transparency type of the pixel
    OH_Drawing_BitmapFormat cFormat{COLOR_FORMAT_RGBA_8888, ALPHA_FORMAT_OPAQUE};
    // Initialize the width and height of the bitmap object and set the pixel format for the bitmap
    OH_Drawing_BitmapBuild(m_Bitmap_, bufferWidth, m_ViewHeight_, &cFormat);
    m_BitmapWidth_ = bufferWidth;
    m_BitmapHeight_ = m_ViewHeight_;
  }

  void XcomponentRender::release() {
    if (m_Bitmap_ != nullptr) {
      OH_Drawing_BitmapDestroy(m_Bitmap_);
      m_Bitmap_ = nullptr;
    }
  }

  void XcomponentRender::draw(DrawConfig *drawConfig) {
    if (m_NativeWindow_ == nullptr) {
      DRAWING_LOGE("nativeWindow_ is nullptr");
      return;
    }
    // Get the memory virtual address of bufferHandle through the system mmap interface
    m_MappedAddr_ = static_cast<uint32_t *>(mmap(m_BufferHandle_->virAddr, m_BufferHandle_->size,
                                                 PROT_READ | PROT_WRITE, MAP_SHARED, m_BufferHandle_->fd, 0));
    if (m_MappedAddr_ == MAP_FAILED) {
      DRAWING_LOGE("mmap failed");
    }

    uint32_t bufferWidth = static_cast<uint32_t>(m_BufferHandle_->stride / 4);
    if (bufferWidth != m_BitmapWidth_ && m_ViewHeight_ != m_BitmapHeight_ && bufferWidth != 0 && m_ViewHeight_ != 0) {
      OH_Drawing_BitmapDestroy(m_Bitmap_);
      // Create a bitmap object
      m_Bitmap_ = OH_Drawing_BitmapCreate();
      // // Define the bitmap pixel format object, including the color type and transparency type of the pixel
      OH_Drawing_BitmapFormat cFormat{COLOR_FORMAT_RGBA_8888, ALPHA_FORMAT_OPAQUE};
      // Initialize the width and height of the bitmap object and set the pixel format for the bitmap
      OH_Drawing_BitmapBuild(m_Bitmap_, bufferWidth, m_ViewHeight_, &cFormat);
    }

    // Create a canvas object and bind the bitmap object to the canvas
    m_Canvas_ = OH_Drawing_CanvasCreate();
    OH_Drawing_CanvasBind(m_Canvas_, m_Bitmap_);
    // Use the specified color to clear the canvas, OH_Drawing_ColorSetArgb(): Convert 4 variables (respectively
    // describing transparency, red, green and blue) into a 32-bit (ARGB) variable describing the color
    OH_Drawing_CanvasClear(m_Canvas_, OH_Drawing_ColorSetArgb(0xFF, 0xFF, 0xFF, 0xFF));

    OH_Drawing_CanvasTranslate(m_Canvas_, 0, 0);
    OH_Drawing_CanvasScale(m_Canvas_, density, density);
    GraphicContext graphicContext(m_Canvas_, drawConfig, density);
    //绘制的真正业务逻辑

    // Get the pixel address of the bitmap and copy it to the specified address
    void *bitmapAddr = OH_Drawing_BitmapGetPixels(m_Bitmap_);
    uint32_t *value = static_cast<uint32_t *>(bitmapAddr);

    uint32_t *pixel = static_cast<uint32_t *>(m_MappedAddr_);
    if (pixel == nullptr) {
      DRAWING_LOGE("pixel is null");
      return;
    }
    if (value == nullptr) {
      DRAWING_LOGE("value is null");
      return;
    }
    for (uint32_t x = 0; x < bufferWidth; x++) {
      for (uint32_t y = 0; y < m_ViewHeight_; y++) {
        *pixel++ = *value++;
      }
    }
    // If the Rect in the Region is nullptr or the rectNumber is 0, all the contents of the OHNativeWindowBuffer are
    // considered to have changed.
    Region region{nullptr, 0};
    // Put the OHNativeWindowBuffer with drawn content back into the Buffer queue
    OH_NativeWindow_NativeWindowFlushBuffer(m_NativeWindow_, m_Buffer_, m_FenceFd_, region);
    int result = munmap(m_MappedAddr_, m_BufferHandle_->size);
    if (result == -1) {
      DRAWING_LOGE("munmap failed!");
    }

    OH_Drawing_CanvasDestroy(m_Canvas_);
    m_Canvas_ = nullptr;
  }
} // namespace test_napi

  有关鸿蒙NAPI方面支持的绘制接口,可以通过查看鸿蒙sdk目录中HarmonyOS-NEXT-DB1/openharmony/native/sysroot/usr/include/native_drawing目录中的头文件。所有绘制相关的接口都在这个目录中。

  多层级绘制如何处理

  对于多层级的绘制场景如何使用NAPI来实现,有两种已知方式:

  1. 通过控制绘制顺序,实现不同层级的内容绘制。每个层级根据绘制优先级从低层级到高层级一层一层的进行绘制。
  2. 结合NodeController可以实现每一层使用一个RenderNode或者一个Xcomponent,多层就是多个具有NAPI绘制能力的组件,从而实现多层级绘制。

五、技术挑战与解决方案

  • 在RenderNode方式中NAPI使用的canvas因为是与ArkTs共用的,所以在canvas使用时,需要注意使用之前save和使用之后Restore,防止影响ArkTs的canvas状态。
  • 在使用NAPI进行文本绘制场景,对于字体的fallback逻辑需要开发者来处理,目前NAPI默认的Typeface是不带字体fallback能力的,不过我们可以通过鸿蒙的drawing_font_mgr.h头文件中
OH_Drawing_Typeface* OH_Drawing_FontMgrMatchFamilyStyleCharacter(OH_Drawing_FontMgr*, const char* familyName,
    OH_Drawing_FontStyleStruct fontStyle, const char* bcp47[], int bcp47Count, int32_t character);

  api接口来自己实现。不过鸿蒙侧给出的口径预计在10月左右也会支持默认Typeface支持字体fallback能力。

  • xcomponent绘制,鸿蒙官方给的例子中缓冲区贴图是存在问题的,像素数量的计算是不正确的。可以参考上面例子的贴图逻辑。

  • xcomponent绘制,xcomponent每次创建都需要获取NativeWindow并获取图形数据缓冲区,这部分会有一定的性能损耗,所以xcomponent尽量不要用在频繁销毁创建的场景。

六、未来展望

  • 相比于android的jni,鸿蒙的NAPI具有更大潜力。

  • android的jni很大的一个能力缺陷是没有办法获取到native的canvas,所以绘制部分基本都要抛回java层处理,java的绘制送显最终也是要通过native给到的系统显示模块,这就大大的限制了绘制效率。

  • 鸿蒙可以通过RenderNode可以获取到native的canvas,可以实现于ArkTs层混合绘制,有很大的想象空间。

  • 而且native层可以获取到canvas,可以把很多绘制相关的功能实现放到native。对于跨平台应用的移植会更容易。

七、写在最后

  • 鸿蒙NAPI相比于其他平台,给予了很大的接口支持,对于实现高性能绘制提供了便利,同时绘制性能也比较有保障。在鸿蒙上设计绘制相关方案时,NAPI可以是一个优先考虑的方向。
  • 鸿蒙的UI框架既引入了类前端的布局方式提高UI的开发效率,又增加NAPI绘制能力提升渲染效率。可以说是集众家之所长。预测未来鸿蒙上会出现越来越多体验优秀的app。