一、第一站:build-profile 的 externalNativeOptions

Native 代码进入构建系统的入口在模块级 entry/build-profile.json5(不是工程级):

{
  "apiType": "stageMode",
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": "-std=c++17",
      "abiFilters": ["arm64-v8a", "x86_64"]
    }
  }
}

四个字段各有讲究:

  • path:指向 cpp 目录的 CMake 入口。从此 hvigor assembleHap 会自动驱动 CMake + Ninja,不需要你手动跑任何构建脚本——C++ 改完直接打包,和改 ArkTS 的心智模型一致。

  • cppFlags-std=c++17。注意它和 CMakeLists 里的 CMAKE_CXX_STANDARD 17 是双保险:一个管 hvigor 传给编译器的旗标,一个管 CMake 侧目标属性,两边必须一致,否则某些生成路径下会出现标准版本漂移。

  • abiFiltersarm64-v8a 覆盖真机,x86_64 覆盖模拟器。漏掉 x86_64 的项目在模拟器上第一次跑就 dlopen 失败,而报错信息只有一句库加载异常——这是 native 工程最常见的「第一天坑」。

  • arguments:透传给 CMake 的额外参数,留空即用默认配置。

二、第二站:CMakeLists——一个 SHARED 库和五个 NDK 稳定库

cpp/CMakeLists.txt 全文不到 30 行,但每一行都是必要信息:

cmake_minimum_required(VERSION 3.5.0)
project(metalfx)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(metalfx SHARED
    metalfx/metal_fx_manager.cpp
    metalfx/metal_fx_renderer.cpp
    napi_init.cpp
)

target_include_directories(metalfx PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

find_library(EGL_LIB EGL)
find_library(GLES_LIB GLESv3)
find_library(HILOG_LIB hilog_ndk.z)
find_library(ACE_NDK_LIB ace_ndk.z)
find_library(NAPI_LIB ace_napi.z)

target_link_libraries(metalfx PUBLIC
    ${EGL_LIB} ${GLES_LIB} ${HILOG_LIB} ${ACE_NDK_LIB} ${NAPI_LIB}
)

三个要点:

  1. 库名 metalfx 是全局身份证。CMake 的 add_library(metalfx ...) 产出 libmetalfx.so,后面 ArkTS 侧 libraryname: 'metalfx'、类型包 oh-package.json5"name": "libmetalfx.so"、NAPI 的 nm_modname = "metalfx"——四处命名必须咬合,任何一处拼错,运行期都是「库加载失败」而非编译错误。

  2. .z 后缀是 NDK 稳定库的标志hilog_ndk.zace_ndk.zace_napi.z。带 .z 的库承诺跨版本 ABI 稳定,是华为官方指定的 native 接入面;而不带后缀的 EGL / GLESv3 是系统图形标准库。链接错版本(比如链到了非稳定路径)在当前 SDK 能跑、升版后崩,属于定时炸弹。

  3. 源文件列表显式枚举:三个 cpp(NAPI 入口 / 管理器 / 渲染器)职责分明——注册、生命周期管理、绘制各一层,和 ArkTS 侧的分层一一呼应。

三、第三站:模块自注册——constructor 属性

NAPI 模块的注册不需要任何外部调用,靠的是 __attribute__((constructor)).so 被加载时自动执行:

static napi_module metalFxModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,     // JS 侧 import 时执行的初始化函数
    .nm_modname = "metalfx",      // ← 与 libraryname / .so 名咬合
    .nm_priv = nullptr,
    .reserved = {0}
};

extern "C" __attribute__((constructor)) void RegisterMetalFxModule()
{
    napi_module_register(&metalFxModule);
}

这里的 nm_modname 就是上一节说的身份证第二处。Init 里做 napi_create_function 导出(参数解析的防御式写法见上一篇 4.3 节),本文不重复。

四、第四站:types/libmetalfx——给 native 模块发「类型签证」

这是全链路最值得单独立节的一站。目录结构:

cpp/types/libmetalfx/
  ├── index.d.ts        # 类型声明
  └── oh-package.json5  # 把声明注册成一个「包」

oh-package.json5 只有四行有效内容:

{
  "name": "libmetalfx.so",
  "types": "./index.d.ts",
  "version": "1.0.0",
  "description": "ArkUILab E024 MetalFx native bridge"
}

name 必须是 .so 的文件名libmetalfx.so,带 lib 前缀带后缀)——构建系统按这个名字把类型声明和产物库配对。配对成功后,index.d.ts 里声明的接口就是 ArkTS 眼中这个 native 模块的全部类型真相

export interface MetalFxNativeConfig {
  colors: Array<number>;
  alphas: Array<number>;
  speed: number;
  intensity: number;
  scale: number;
  direction: number;
  distortion: number;
  complexity: number;
  blur: number;
  vignette: number;
  vigOpacity: number;
  shaderOpacity: number;
  strength: number;
  radiusPx: number;
  ringPx: number;
  samplingScale: number;
}

export interface MetalFxNativeContext {
  updateConfig(config: MetalFxNativeConfig): void;
  setActive(active: boolean): void;
  requestRender(): void;
}

这两个接口的设计值得抄走:

MetalFxNativeConfig 是纯扁平数据——17 个字段全是 number / Array<number>,没有一个嵌套对象、没有一个枚举、没有一个字符串联合类型。这不是偷懒,是跨界成本控制:NAPI 解析一个扁平对象是最便宜的路径,每个嵌套层级都要额外的 napi_get_named_property 防御链(上一篇 4.3 节的繁琐正源于此)。ArkTS 侧友好的枚举、预设、语义参数,全部在跨界之前由纯函数压平成这 17 个数字。

MetalFxNativeContext 只有三个方法——updateConfig(全量配置)、setActive(启停)、requestRender(请求一帧)。没有增量 patch、没有 getter、没有事件回调。native 边界的 API 面越小,两侧可以各自演进的自由度越大。

五、第五站:XComponent 按名加载,onLoad 领取句柄

ArkTS 侧与 .so 的握手机制:

XComponent({
  id: this.xComponentId,
  type: XComponentType.TEXTURE,
  libraryname: 'metalfx'          // ← 身份证第三处
})
  .backgroundColor('#00000000')
  .hitTestBehavior(HitTestMode.None)
  .onLoad((context?: object): void => {
    if (context !== undefined) {
      this.nativeContext = context as MetalFxNativeContext;   // ← .d.ts 在这里生效
      this.syncNative();
      this.reconcileActivity();
    }
  })
  .onDestroy((): void => {
    this.nativeContext = undefined;    // 句柄生命周期与组件严格同步
  })

关键点:onLoad 回调的 context 静态类型只是 object是第四站的 MetalFxNativeContext 声明让它可以被安全地 as 过去——之后每一次 updateConfig 调用都有编译期检查。没有这个声明,native 接口改名时 ArkTS 侧零报错,直接运行期崩。

onDestroy 里把句柄置空同样重要:XComponent 销毁后 native 侧对象已释放,残留引用就是悬空指针。(libraryname 与 native 侧生命周期回调的挂接细节见上一篇 4.2/5.3 节。)

六、第六站:包装成普通组件——分层范式的 native 版

最后一步,把「带 native 句柄的 XComponent」包装成宿主无感知的普通组件。MetalFx 的 ArkTS 侧完全复用了本系列第一篇的四层范式:

MetalFxModels.ets    枚举与值对象:Variant / Preset / Theme(Auto|Light|Dark)
MetalFxCore.ets      纯函数:buildMetalFxNativeConfig(...) 把语义参数压平成 17 个数字
MetalFxPresets.ets   预设表:metalFxPresetMode(preset, dark)
MetalFx.ets          组件:生命周期、句柄、@Prop @Watch 同步

组件形态是标准的内容包装器(与 BorderBeam 同构):Stack { content; 透明 XComponent }onAreaChange 测量内容尺寸驱动叠层,vp2px 换算跨界(pxScale,见上一篇第三节)。

三个 native 特有的工程细节:

多实例 id 生成器。XComponent 的 id 要求全局唯一,组件可能被同时挂载多份:

let nextMetalFxId: number = 0;

function createMetalFxId(): string {
  nextMetalFxId++;
  return `arkuilab_metalfx_${nextMetalFxId}`;
}

模块级计数器比随机数可靠——连续、可读、无碰撞,真机日志里还能当序号用。

@Watch 统一同步。八个配置型 @Prop(variant/preset/theme/darkSurface/strength/metalRadius/ringWidth/shaderScale)全部挂同一个 @Watch('onConfigChanged'),收敛到一条路径:

private syncNative(): void {
  if (this.nativeContext === undefined) {
    return;                                    // 句柄未就绪时静默容忍
  }
  this.nativeContext.updateConfig(this.nativeConfig());   // 全量推送
  this.nativeContext.requestRender();
}

配合「Config 扁平化 + updateConfig 全量语义」,任何参数变化的处理都是同一个动作:重建 config、全量推、请求一帧。没有增量 diff、没有部分更新的一致性坑。

生命周期收敛成一个布尔。三个来源(挂载、可见、暂停)汇成一个判定:

private reconcileActivity(): void {
  if (this.nativeContext !== undefined) {
    this.nativeContext.setActive(this.appeared && this.visible && !this.paused);
  }
}

appeared(aboutToAppear/Disappear)、visibleonVisibleAreaChange([0.0, 0.01, 1.0]),含完全离屏与刚露头两个阈值)、paused(宿主注入)三线合一,native 侧只见到一个 setActive(bool)——启停纪律(静止就停表)在组件边界就完成裁决,不把三个状态漏进 native。

七、工程链 Checklist

从零接入一个 native 图形库,按单打勾:

  • entry/build-profile.json5externalNativeOptions(path / cppFlags / abiFilters 含 x86_64)

  • CMakeLists:SHARED 库名 = 目标组件名;只链 .z 稳定库 + 标准图形库

  • napi_init.cpp:constructor 自注册,nm_modname 与库名一致

  • cpp/types/<libname>/oh-package.json5name 为完整 .so 文件名

  • index.d.ts:Config 扁平纯数据 + Context 最小方法面

  • ArkTS 侧:libraryname 与库名一致;onLoad 领句柄、onDestroy 清句柄

  • XComponent id 用模块级计数器保证多实例唯一

  • 配置变化收敛到全量 updateConfig + requestRender 一条路径

  • 生命周期三线合一成 setActive,边界处完成启停裁决

  • 验证线不变:hvigor assembleHap 三项全过(CompileArkTS / PackageHap / SignHap)

结语

渲染链决定 native 组件「能不能跑」,工程链决定它「是不是一个合格的 ArkUI 公民」。回头看这七站,其实只做了两件事:把命名咬合成一条链(metalfx → libmetalfx.so → libraryname → nm_modname → oh-package name,五处一个都不能错),把复杂度拦在边界两侧(语义参数压平成数字再跨界、三个生命周期收敛成一个布尔再跨界)。native 不是特区,它只是另一个需要接口纪律的模块——而 .d.ts 就是它的接口合同。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐