本文记录 LazySurface 接入 OpenHarmony 的完整过程,覆盖关系式布局引擎盘点、社区 Compose 制品线选型、上游源码零修改编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

LazySurface 是基于 Compose Multiplatform 的关系式二维懒布局库:条目不说自己在哪,只说自己挨着谁——item(key = "stats", neighbors = { below("hero", margin = 16.dp) }),布局引擎从枢纽数原点出发做广度优先联合布放,再跑一遍确定性约束求解器联合精修全部位置;滚动被钳制在已解析条目的包围形状上;删除条目时指向它的关系自动拼接到最近的存活锚点(墓碑愈合),重新声明同名 key 又会把原始关系赢回来。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上的 HarmonyOS Compose 制品线(1.12.0-0.1.0-07,runtime / foundation / ui / animation 全部带 ohosArm64 / ohosX64 klib 变体)已经覆盖布局引擎依赖的全部符号,因此不需要提取内核、也不需要把 Compose 文件隔离到 composeMain——上游 commonMain 的 17 个源文件被一行不改地编译成 OpenHarmony KLIB,注册、布放、求解、愈合、钳制、路由全部是上游原件;唯一新增的源码是一个无头引擎门面:把组合宿主才有的"组合测量"一步换成宿主供尺寸的 needMeasure 往返,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/LazySurface.git

上游仓库: 07jasjeet/LazySurface

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

LazySurface 为 Compose Multiplatform 应用提供"条目 ↔ 相对位置"这一件事的完整答案:LazySurface(state) { item(key, neighbors) { content } } 一个入口,关系双向可用(声明一边即可)、多关系联合定位(均值 + 软硬约束)、Alignment.Free 纯邻接边(路由 + 硬不重叠)、margin 只属于单条关系、atPivot() 把内容锚到曲面原点。引擎侧还有:惰性解析(视口外条目用零尺寸探针暂定位置,接近才测量)、确定性约束求解器(声明的邻居永不重叠是硬约束,间距精确与交叉轴对齐是软偏好)、方向性滚动钳制(包围形状按带状决定可达范围,未测邻居把边界保持开放)、墓碑愈合与复活、沿关系边的路由导航。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、wasm 共享。OpenHarmony 应用同样需要这套关系式布局能力,但上游构建(Kotlin 2.3.20 / CMP 1.11.1 / Gradle 9.4.1 / AGP 9.2.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示卡片,却无法证明公共布局引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证布局引擎、Compose 快照状态运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
无组合宿主社区 Compose 线只发布 klib,没有窗口、指针管道和 LazyLayout 子组合——LazySurface composable 本体挂不上设备,必须找到引擎的无头驱动方式。
依赖矩阵引擎的公共 API 面站在 runtime / foundation / ui / animation 四件套上,每一件都要有 ohos klib 变体才能解析;androidx.collection 的散列容器还要单独找 ohos 坐标。
测量语义跨不了桥条目尺寸来自"内容在无界约束下自测"(组合测量),C ABI 是同步一来一回,尺寸获取必须改造成宿主供数的往返。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:上游引擎的零修改 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。布局语义完全由上游原版实现负责,ArkTS 只负责手势采样、供尺寸和渲染。

1.2 库提供的能力

Demo 提供一个真实的关系布局会话,对应桥接的全部入口:

  • 注册(configure):把条目与关系图(atPivot / above / below / startOf / endOf 动词 + 对齐 + 边距)交给引擎,引擎跑一遍完整 pass 返回全部已解析矩形;
  • 供尺寸(measureItem):引擎按接近度逐个讨要未测条目的尺寸(needMeasure),宿主应答后条目立即落位——这是上游"按需组合"的鸿蒙同构物;
  • 手势(gesturePan / setZoom):平移与缩放走真实的方向性钳制,响应带回消费量,未消费量即边缘接触信号;
  • 愈合与复活:删条目后重新 configure,关系自动拼接到幸存锚点;原 key 回归赢回原关系;
  • 导航(route / aim):关系图最短路径 + 逐跳瞄准点,视口沿关系边飞行;
  • 版本握手:lazySurfaceVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

每个响应都是一次完整 pass 快照:

字段作用
offsetX/Y / zoom / viewport视口状态(offset = 视口中心所在的曲面坐标)
rects / bounds全部已解析矩形与包围盒(曲面坐标、未缩放)
visible / provisional视口内条目 / 未测前沿(零尺寸探针)
needMeasure / stranded引擎讨要尺寸的 key / 搁浅回落目标
perf.measureUs / resolveUs / solveUs引擎自己上报的三相耗时

引擎侧完整能力(求解器、墓碑愈合、暂定闭包、钳制数学、路由 BFS、RTL 镜像)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 142 项 desktopTest 交叉验证用例(134 项上游原件 + 8 项门面冒烟),证明 ohos .so 里执行的布局逻辑没有漂移。

1.3 实现适配

维度要求
代码复用LazySurface 模块全部 17 个 commonMain 文件原样编译,不提取、不重写、不隔离 composeMain;库模块仅新增 1 个 ohos 无头门面源文件(全仓唯一新增 Kotlin 源文件于库内)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 app:* 演示模块留盘不进构建;原始构建文件逐字保存为 *.upstream。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版),上游测试零改动。
UI 完整单页覆盖全部桥接入口:五个 Tab(关系 / 求解器 / 自愈 / 导航 / 懒测量)、手势钳制、性能 HUD 和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙尚无 Compose 组合宿主,LazySurface composable 已编译进 klib 但挂不上设备;ArkTS Demo 把桥暴露的引擎闭环画出来,手势采样进真实钳制,渲染只是曲面坐标到屏幕的一次线性映射。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认社区 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、依赖双线分列
第 3 阶段:桥协议建模     ── 关系 DSL 的 JSON 契约、pass 快照响应、needMeasure 往返、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、无头引擎门面、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 五 Tab、手势钳制、needMeasure 应答、性能 HUD 和版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
LazySurface/                           上游布局引擎 + composable(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
LazySurface/src/ohosMain/              新增:无头引擎门面 LazySurfaceHeadless(唯一新增库源码)
LazySurface/src/desktopTest/           上游 134 项测试 + 新增 8 项门面冒烟(HeadlessSmokeTest)
example/nativeApp/                     Kotlin/Native 动态库(Bridge + JSON 解析器)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面
app/                                   上游 Android/iOS/Desktop/Web 演示,留盘不进 settings
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存
*.upstream                             原始构建文件(settings / 根 build / properties / catalog / 库 build)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :LazySurface 与 :example:nativeApp,rootProject.name = "ohos_lazysurface"。上游的 AGP classpath、Android/iOS/wasm 目标、vanniktech 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle9.6.1(上游 wrapper 9.4.1 的本机缓存损坏且不可下载,切到同代已验证版本)
Compose(ohos 线)runtime / foundation / ui / animation 1.12.0-0.1.0-07;collection 经 org.jetbrains.compose.collection-internal:collection:1.5.0-0.1.0-07;coroutines 1.11.0-0.1.0-07
Compose(桌面线)Maven Central 1.11.1(与上游同体,交叉验证专用)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.lazysurface
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
文件群文件数依赖形态处置
纯 Kotlin 引擎(求解器 / 关系 DSL / 性能与调试钩子)4零 Compose import整体编译(jvm + 双 ohos)
引擎 + 快照状态(State / MeasureLogic / 暂定位置 / ItemInfo)4runtime 快照 + ui 几何值类 + foundation.lazy.layout 接口整体编译(无宿主 API)
Compose UI(composable 本体 / overscroll / snap / ItemScope)9pointerInput / LazyLayout / Modifier.Node整体编译(设备上无宿主调用方,klib 里待用)

对 ohos 编译可行性至关重要的一个盘点结论:全部 17 个文件的 import 只落在 runtime / animation(.core) / foundation(.lazy.layout, .gestures) / ui(.geometry, .unit, .layout, .node, .input.pointer, .util) 与 androidx.collection 上——没有任何文件 import ComposeScene / 窗口 / 视图系统。也就是说引擎只是"没有人调用 UI 层",不是"UI 层编不过"。

expect 声明全库为零,ohos 不需要任何 actual。唯一的新增源文件是无头门面(见第 4.2 节)。

1.4 页面边界
标题区          LazySurface · OpenHarmony / 版本握手行(关系式二维懒布局 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        关系 / 求解器 / 自愈 / 导航 / 懒测量
曲面区          手势 Stack(pan/pinch 并行手势),卡片按引擎返回的 rect 定位渲染
状态行          最近一次 pass(op @ (offset) ×zoom),搁浅时显示回落目标
性能行          measure / resolve / solve 三相微秒 + visible/rects 计数 + published 标记
控制区          按 Tab 变化:场景预设、矛盾开关、晶格密度、删除/复活、飞往 far、回原点
讨尺寸行        needMeasure 命中时显示引擎正在讨要的 key(自动应答后消失)

页面启动时先调 lazySurfaceVersion() 做握手,再按场景注册条目并注入视口尺寸。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有引擎依赖的 Compose ohos klib。用 .module 元数据逐个验证:

org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07        .module 200
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07  .module 200(依赖表含 collection-internal)
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07                  .module 200
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07    .module 200
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07 .module 200

三个关键结论:其一,上游写的 CMP 1.11.1 与社区线 1.12.0-0.1.0-07 同代且 API 兼容——实测上游 17 个文件(含 LazyLayoutIntervalContent、LazyLayoutMeasurePolicy 这些 foundation 内部接口的调用方)零修改编译通过;其二,ohos 线不发布 *-desktop 子制品,jvm("desktop") 交叉验证目标不能从这条线解析,必须双线分列(compose-shimmer 先例);其三,androidx.collection 的上游坐标没有 ohos 变体,但 foundation 的依赖表里藏着遮蔽坐标 org.jetbrains.compose.collection-internal:collection:1.5.0-0.1.0-07(包名仍是 androidx.collection,散列容器一个不少)。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 Compose 1.12.0-0.1.0-07 线的 klib 元数据要求 ≥2.3 的编译器,工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.3.20 编的,2.4.20 高一小代,源码级兼容成本为零;Gradle 9.x 线可用(KStore 先例 9.6.1)。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_lazysurface"

include(":LazySurface")
include(":example:nativeApp")
2.3 编译真实引擎

LazySurface/build.gradle.kts 收敛为纯 KMP 模块,依赖按目标分线:

kotlin {
    applyDefaultHierarchyTemplate()

    jvm("desktop") { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }  // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        ohosMain.dependencies {                    // ohos 线(klib)
            api(libs.compose.runtime)              // 1.12.0-0.1.0-07
            api(libs.compose.foundation)
            api(libs.compose.ui)
            api(libs.compose.animation)
            api(libs.collection.ohos)              // collection-internal 1.5.0-0.1.0-07
            implementation(libs.kotlinx.coroutines.core)
        }
        val desktopMain by getting {               // Maven Central 上游线(1.11.1)
            dependencies {
                api(libs.compose.runtime.jvm)
                api(libs.compose.foundation.jvm)
                api(libs.compose.ui.jvm)
                api(libs.compose.animation.jvm)
                api(libs.collection.jvm)
            }
        }
        val desktopTest by getting { /* junit + 上游 skiko 坐标 + 门面冒烟源集 */ }
    }
}

实测:

./gradlew :LazySurface:compileKotlinOhosArm64 :LazySurface:compileKotlinOhosX64   # BUILD SUCCESSFUL,17 文件零修改

引擎的 commonMain 一个字没动——这是本批 CMP 适配里最彻底的一次:连 Compose UI 文件都没有隔离,composeMain 这个目录在本仓不存在。

2.4 测试的处置:测试统一 JVM 跑

ohos 目标没有 KN 测试运行器,处置是社区纪律:测试统一在 JVM 跑(desktopTest),根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

上游 27 个测试文件零改动保留(目标名 jvm("desktop") 原样,测试目录名都不用动);另外把 src/ohosMain 挂进 desktopTest 源集,为无头门面加 8 项冒烟测试——桥层语义也钉在 JVM 线上。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.jasjeet.lazysurface/**(上游引擎,原样编译)
        ↓ LazySurfaceState / ResolveScratch / placeItemTopLeft / solveMemoized
LazySurfaceHeadless(库内 ohosMain 无头门面,唯一替换:组合测量 → 尺寸往返)
        ↓
Bridge.kt(@CName × 2,UTF-8 JSON 进出)
        ↓
N-API lazySurfaceVersion() / lazySurfaceDispatch()
        ↓
ArkUI Index.ets(五 Tab)

请求与响应:

请求 : {"op":"configure","items":[{"key":"hero","w":340,"h":150,
                                   "relations":[{"verb":"atPivot"}]},
                                 {"key":"stats","w":340,"h":110,
                                   "relations":[{"verb":"below","of":"hero","alignment":"center","margin":16}]}],
        "viewport":{"w":1080,"h":1800},"rtl":false,"zoom":1.0,"offsetX":0,"offsetY":0}
       {"op":"measureItem","key":"a3","w":300,"h":100}
       {"op":"gesturePan","dx":-18.4,"dy":6.2,"zoom":1.4}
       {"op":"setZoom","zoom":1.8} / {"op":"setViewport","w":1080,"h":1800}
       {"op":"clamp","x":900,"y":0} / {"op":"route","from":"","to":"far"} / {"op":"aim","key":"far"}
响应 : {"ok":true,"op":"...","offsetX":..,"offsetY":..,"zoom":..,
        "rects":[{"key":"hero","l":-170,"t":-75,"r":170,"b":75},...],
        "provisional":[...],"bounds":{...},"visible":[...],"needMeasure":[...],
        "stranded":null,"published":true,"perf":{"measureUs":..,"resolveUs":..,"solveUs":..}}
       {"ok":false,"error":"..."}

三个不对称设计:关系动词(below 等)由桥映射回 LazySurfaceNeighbor 的上游工厂——below(x) 在上游内部记为"邻居在我上方"的语义倒置由上游代码处理,桥不重复实现;margin 与全部坐标是 px(引擎 px 原生,Dp 只存在于上游 DSL 边界,ArkTS 侧换算);条目可以不声明尺寸,引擎接近时经 needMeasure 讨要——这是把上游"接近才组合测量"的惰性原样搬过桥。

3.2 无头引擎门面为什么放在库模块里

布放 pass 用到的 ResolveScratch、placeItemTopLeft、solveMemoized、ProvisionalPositions 都是库内 internal 声明,跨模块不可见。门面 LazySurfaceHeadless 因此放在 LazySurface/src/ohosMain(同模块可触达全部 internal 原语),对桥暴露 public API;example/nativeApp 只调 public。注册直接走真实的 LazySurfaceContent(Density(1f))——重复 key / 自关系 / 负 margin / Free-only 连通性这四条上游 fail-fast 校验一份实现、不分叉。

门面把 measureLazySurfaceImpl 逐句镜像:pivot 播种、own/fallback 边选择、jointPlace 均值、探针/停泊/推迟、approach 预算、搁浅判定、solveMemoized、发布边界、ProvisionalPositions.update、visibleItemsInfo 发布。唯一替换:compose(index) + 无界测量 → measuredSizes 查表;没有尺寸就记入 needMeasure 并停泊。条目动画与预取(帧时钟 / 子组合宿主特性)无对应物,不暴露——它们是纯视觉与延迟优化,不影响布局语义。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("LazySurfaceDispatch")
fun lazySurfaceDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${Json.string(t.message ?: t.toString())}}"""
}

非法 JSON、未知 op、注册校验失败(重复 key 等)、非法尺寸,全部走同一个 ok:false 通道,ArkTS 侧统一渲染,HAP 不崩。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(liblazysurface_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoslazysurface.so)
  │ LazySurfaceDispatch / LazySurfaceVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制布局语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数与门面
@CName("LazySurfaceDispatch")
fun lazySurfaceDispatch(request: String): String   // configure / measureItem / gesturePan / ... 一口进

@CName("LazySurfaceVersion")
fun lazySurfaceVersion(): String = "0.2.0-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
LazySurfaceDispatchlazySurfaceDispatch(req)注册 + 愈合 + 布放 pass + 求解 + 钳制 + 路由 + 懒测量
LazySurfaceVersionlazySurfaceVersion()fork 版本握手(真机版本行的数据来源)

桥内 JSON 是一个小型递归下降解析器(对象 / 数组 / 字符串 / 数字 / 布尔),因为关系协议带嵌套数组——第一代桥的扁平 MiniJson 不够用;零外部依赖的纪律不变。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"lazySurfaceDispatch", nullptr, NapiLazySurfaceDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"lazySurfaceVersion", nullptr, NapiLazySurfaceVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoslazysurface_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoslazysurface_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoslazysurface SHARED IMPORTED)
set_target_properties(ohoslazysurface PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(lazysurface_napi SHARED napi_init.cpp)
target_link_libraries(lazysurface_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoslazysurface)

OHOS_ARCH 为空或 libohoslazysurface.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const lazySurfaceDispatch: (request: string) => string;
export const lazySurfaceVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 注册、手势和讨尺寸应答
aboutToAppear(): void {
  this.pxPerVp = display.getDefaultDisplaySync().densityPixels;  // px2vp/vp2px 已废弃,自算换算
  this.version = lazySurfaceVersion();                           // 版本握手:来自真实 Native 调用
  this.setScenario('quickstart');
}

private sendConfigure(offsetX: number, offsetY: number, zoom: number): void {
  const viewport: JsonSize = { w: this.viewportW, h: this.viewportH };
  const req: ConfigureReq = { op: 'configure', items: this.items, viewport: viewport,
    rtl: this.rtl, padding: [8, 8, 8, 8], zoom: zoom, offsetX: offsetX, offsetY: offsetY };
  this.applyPass(this.dispatch(JSON.stringify(req)));
}

手势是 GestureGroup(Parallel, PanGesture, PinchGesture):pan 的位移增量经密度换算成 px 进 gesturePan(真实方向性钳制),pinch 的比例进 setZoom;onAreaChange 把曲面区实际尺寸喂给 setViewport。applyPass 里把每个返回 rect 做一次线性映射(vw/2 + (rect.l - offsetX) * zoom,与上游 placeWithLayer 共享同一滚动项取整思想)得到卡片屏幕位置;needMeasure 非空时按 key 从卡片注册表应答 measureItem——列表场景务必应答后再渲染该卡片。

5.2 页面交互

五个 Tab 覆盖桥的全部入口:关系(README 快速上手图 + placeAround 环形 + RTL 镜像开关)、求解器(3×3–9×9 全晶格 + 矛盾开关:中间格额外声明一条几何不相容关系,观察软约束融合与 solveUs 上升 + 实时性能 HUD)、自愈(a0–a6 链,删中间链路看尾部拼接、复活看赢回原关系)、导航(蛇形分区,点卡片沿 route 返回的关系路径逐跳飞行,每跳经真实钳制)、懒测量(14 节点链只声明前两个尺寸,向下滚动时 needMeasure 逐个讨要、应答后落位)。状态行与性能行每帧刷新,搁浅时显示回落目标 key。

5.3 对比度和错误边界

场景按钮选中态反色,破坏性操作(删除)与冲突态(矛盾开)用酒红;ok:false 时错误原文进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进 hilog。卡片在视口外降低不透明度,provisional(已解析但不在视口)与可见卡片有区分。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoslazysurface)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + 递归 JSON 解析器
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.lazysurface、versionName 0.2.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoslazysurface_api.h
        ├── src/main/cpp/types/liblazysurface_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 五 Tab 验收页面
6.2 构建、签名和安装
./gradlew :LazySurface:desktopTest                                     # 142 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

命令行打包注意:PackageHap 需要 java 在 PATH 上(spawn java ENOENT 即此坑),把 DevEco 自带 JBR 加进 PATH 即解。签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.lazysurface -m entry

四、完整代码对照

4.1 整体架构

com.jasjeet.lazysurface/**(上游引擎原样编译)
    │ LazySurfaceState / 关系 DSL / BFS 布放 / 约束求解器 / 愈合 / 钳制 / 路由
LazySurfaceHeadless(库内 ohosMain 无头门面)
    │ configure / measureItem / gesturePan / setZoom / setViewport / clamp / route / aim
Bridge.kt
    │ LazySurfaceDispatch / LazySurfaceVersion
liblazysurface_napi.so
    │ lazySurfaceDispatch / lazySurfaceVersion
Index.ets
    │ JSON parse + @State 更新 + 手势采样
    ▼
ArkUI 真机页面(五 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_lazysurface、上游 app 模块移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + ohos Compose 线 / Maven Central 桌面线双列
build.gradle.kts(根)版本 0.2.0-ohos.1、全局禁用 Ohos*Test* 任务
LazySurface/build.gradle.ktsjvm(“desktop”) + ohosArm64 + ohosX64,依赖按目标分线
LazySurface/src/ohosMain/.../LazySurfaceHeadless.ohos.kt唯一新增库源文件(无头引擎门面)
LazySurface/src/desktopTest/.../HeadlessSmokeTest.kt门面行为 JVM 钉子(8 例)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、op 分发、递归 JSON 解析器、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 五 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎item(key, neighbors) { content }关系式注册(校验四条 fail-fast)
引擎placeItemTopLeft / CompiledConstraints.solveInto布放数学 / 约束求解(internal,门面同模块复用)
引擎LazySurfaceState.gesturePan方向性钳制平移,返回精确消费量(internal)
引擎updateItems图重建 + 墓碑愈合 + 复活(internal)
引擎routeTowards关系图 BFS 路由(internal)
引擎LazySurfacePerformance.monitor三相耗时钩子(public,门面经它上报 perf)
门面configure / measureItem / gesturePan / setZoom / setViewport / clamp / route / aim桥的 public API
NativeLazySurfaceDispatch / LazySurfaceVersionC ABI JSON 导出
N-APIlazySurfaceDispatch(req) / lazySurfaceVersion()向 ArkTS 暴露能力
ArkTSdispatch(op) / applyPass(pass)触发操作并按快照刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const req: PanReq = { op: 'gesturePan', dx: dxPx, dy: dyPx, zoom: this.zoom };
const pass = JSON.parse(lazySurfaceDispatch(JSON.stringify(req))) as PassJson;
// pass.consumedX/Y 是引擎消费掉的位移;未消费部分 = 边缘接触信号
val pan = engine.gesturePan(Offset(dx, dy), gestureZoom)   // 上游 LazySurfaceState 真实钳制
return HeadlessPan(pan.consumedX, pan.consumedY, runPass())

两者之间只传输 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:零修改编译整库,而不是提取内核或隔离 Compose

早期仓库抽内核(qrose)或把 Compose 文件挪进不编译的 composeMain(wavy-slider / ComposeHints),是因为 ohos 上没有它们依赖的制品或工具链对不齐。本次先验证制品线(runtime / foundation / ui / animation klib 全部存在),再实测上游源码零修改编译通过——再抽内核或隔离文件,等于凭空制造与上游合并的冲突面。真编译让求解器常数、愈合算法、钳制数学全部是上游原件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 走 9.x

Compose 1.12.0-0.1.0-07 线的 klib 元数据需要 ≥2.3 编译器,旧钉法 2.2.21 读不动;Gradle 方面 2.4.20 线支持 9.x(KStore 先例 9.6.1),上游 wrapper 9.4.1 的本机缓存损坏后切到 9.6.1——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:依赖双线分列(换线不换版)

ohos 线不发布 *-desktop 子制品,所以 commonMain 不放 Compose 依赖:ohosMain 吃 ohos 线 klib(1.12.0-0.1.0-07),desktopMain 吃 Maven Central 上游线(1.11.1,与上游声明同体)。版本主体与上游一致,交叉验证跑的就是上游同代的 API 面。androidx.collection 换用社区遮蔽坐标 org.jetbrains.compose.collection-internal:collection:1.5.0-0.1.0-07(包名不变)。

决策 4:无头门面放库内 ohosMain,尺寸走 needMeasure 往返

布放 pass 焊在 LazyLayoutMeasureScope.compose 上,那是全引擎唯一必须组合宿主的步骤。门面在库模块内(internal 原语可达)逐句镜像上游 pass,把"组合测量"换成宿主供尺寸:引擎接近某条目时把它记入 needMeasure 停泊,宿主应答后下一 pass 落位——惰性语义原样过桥,且尺寸来源本来就是上游设计里"内容自测"的宿主职责。门面同时把 src/ohosMain 挂进 desktopTest 源集,8 项冒烟测试钉住桥层行为。

决策 5:desktopTest 交叉验证 + 上游测试零改动

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;142 项 desktopTest(134 项上游原件 + 8 项门面冒烟)是"引擎没漂"的闸门。ohos 目标没有测试运行器,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——上游测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Eclipse Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :LazySurface:desktopTest

142 项用例,141 项通过(上游测试文件零改动):

测试类用例数钉住什么
ConstraintSolverTest / SolverEdgeCaseTest / SolverPlateauTest / SolverRtlTest / SolveMemoTest 等数十项求解器:硬分离、软间距、平台早停、RTL、memo 回放
PlaceItemRectTest / ViewportRectTest / DslVerbsTest / ItemsDslTest—布放数学、视口映射、DSL 语义与校验
TombstoneHealingTest / BoundingShapeTest / GesturePanTest / RoutedFlingTest 等—愈合、钳制、fling、路由(手写 MonotonicFrameClock 无头驱动)
HeadlessSmokeTest(新增)8门面:快速上手图坐标精确断言、needMeasure 往返、删除愈合、复活、巨幅 pan 钳制与二次不动、路由路径、矛盾确定性收敛、重复 key 拒绝
MeasurePassBenchmark1macOS skiko 渲染基准,Windows 上必然失败(上游在本机同样失败),与适配无关

依赖换线没有 API 漂移:Compose 1.11.1 换成 1.12.0-0.1.0-07 线后同代 API 兼容,上游测试照跑——这正是"换线不换版"的直接证据。

6.3 HAP 验证

:LazySurface:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL(上游 17 文件零修改)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: LazySurfaceDispatch / LazySurfaceVersion / libohoslazysurface_symbols 均为全局 T 符号
BuildNativeWithNinja: finished(双 ABI 编出 liblazysurface_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 42.4 MB,内含双 ABI 各三件套(liblazysurface_napi.so / libohoslazysurface.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 19.8 MB、x86_64 约 19.7 MB(debugShared 未 strip 时分别约 29.2 MB / 27.9 MB,含 Kotlin/Native 运行时与静态链入的 Compose klib,同代方案 compose-shimmer / bubbles 同为 ~30 MB 量级)。

6.4 功能用例

  1. 启动后版本行出现 lazySurfaceVersion() → 0.2.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 关系 Tab:hero 居中原点,stats 在 hero 下方 16px,sleep 在 stats 右侧 16px、垂直居中——无一个坐标,全靠关系;切 Rings 看环形排布;开 RTL 整体水平镜像,startOf/endOf 与 Start/End 对齐随之翻转;
  3. 求解器 Tab:7×7 晶格 49 节点全部解析,性能行显示 measure / resolve / solve 三相微秒;开"矛盾"后软约束融合、晶格出现可见拉伸,solveUs 上升;
  4. 自愈 Tab:删除 a2 后 a3 及尾部拼接到幸存锚点(间距保持);复活链后 a2 赢回原关系;
  5. 导航 Tab:点"飞往 far",状态行先显示 路由: mid → far 关系路径,视口沿路径逐跳飞行(每跳经真实 gesturePan 钳制);
  6. 懒测量 Tab:向下滚动,讨尺寸行出现 needMeasure: a3...,ArkTS 应答后卡片落位——远处条目接近才测量;
  7. 双指捏合缩放(0.4–2.5),平移到内容边缘被钳制,再次同向巨幅平移 offset 不再移动(方向性钳制的可重复性);
  8. 篡改关系为重复 key(理论操作),注册被拒并返回 ok:false 原文,HAP 不崩。

6.5 验证结论

布局引擎、Compose 快照状态运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的关系布局会话:版本握手、关系布放、约束求解(矛盾融合)、删除愈合与复活、关系路由飞行和按需懒测量闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/LazySurface.git
cd LazySurface
export JAVA_HOME="/path/to/jdk-21"
./gradlew :LazySurface:compileKotlinOhosArm64 :LazySurface:compileKotlinOhosX64   # klib
./gradlew :LazySurface:desktopTest                                               # 142 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.lazysurface -m entry
hdc shell hilog | grep LazySurface

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :LazySurface 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 wrapper 9.4.1 的本机 dists 缓存是半截下载(只有 .part/.lck,无 .ok 标记),wrapper 反复尝试联网下载超时——换成本机缓存完整的 9.6.1 一次通过;缓存完整与否看 .ok 文件;
  4. ohos Compose 线不发布 *-desktop 子制品,commonMain 放 Compose 依赖会让 desktop 目标解析失败——依赖必须按目标分线(ohosMain / desktopMain);
  5. androidx.collection 的上游坐标没有 ohos 变体:从 foundation 1.12.0-0.1.0-07 的 .module 依赖表里找到遮蔽坐标 org.jetbrains.compose.collection-internal:collection:1.5.0-0.1.0-07(包名仍是 androidx.collection)——找不到坐标时,先读上游制品的依赖表而不是猜版本号;
  6. Gradle 9 对 include(":example:nativeApp") 要求目录真实存在,否则配置期报 Configuring project ':example' without an existing directory——先建模块目录再 sync;
  7. ArkTS 严格模式禁止无类型对象字面量(arkts-no-untyped-obj-literals):给每种请求声明 interface,const req: PanReq = {...} 再 JSON.stringify(req);
  8. @Component struct 的成员名不能撞 ArkUI 内置属性:私有字段 offset 与 CustomComponent 的 offset(...) 冲突直接编译失败,改名 centerOffset;
  9. 全局 px2vp / vp2px 已标记废弃:用 display.getDefaultDisplaySync().densityPixels 取密度自算换算,一次初始化双向使用;
  10. 命令行 hvigorw assembleHap 的 PackageHap 报 spawn java ENOENT:打包工具链要找 java,把 DevEco 自带 JBR 加进 PATH 即解(DevEco 内构建无此问题);
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();签名材料由 DevEco 自动写入 build-profile.json5,仓库内 signingConfigs 保持空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙没有 Compose 组合宿主:LazySurface composable、Modifier.animateItem、snap fling、overscroll 工厂已编译进 klib 但设备上无调用方,Demo 视觉由 ArkUI 复刻;
  • 无头门面是布放 pass 的镜像而非同一函数:JVM 冒烟测试兜底,上游若改 measureLazySurfaceImpl 门面需同步(测试会红);
  • 条目动画与预取未暴露:帧时钟 / 子组合宿主特性,纯视觉与延迟优化,无头路径无对应物;
  • KN so 约 20–29 MB(strip 前):Compose 四件套 klib 静态链入所致,同代方案同量级;多 CMP 库共存时共享同一份运行时副本,不叠加;
  • 桥当前单实例单曲面:多曲面需要 JSON 加 surface id 字段(C ABI 不用改);
  • MeasurePassBenchmark 只能在 macOS 跑(上游即如此,非适配引入);
  • HAP 安装必须使用与 bundle name org.terminator.ohos.lazysurface 匹配的签名。

8.3 未来优化方向

  • 等社区 Compose 组合宿主落地:接上宿主直接跑真 composable,门面降级为测试通道,ArkUI 复刻层退役;
  • 桥扩展多曲面 id、settleIntoBounds 挂起版(宿主逐帧驱动)与 animateZoomTo;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 LazySurface(全部 commonMain,17 文件)
    → 验证社区 Compose ohos klib 制品线(runtime / foundation / ui / animation)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,依赖双线分列)
    → ohosArm64 / ohosX64 KLIB,上游源码零修改(无 composeMain、无注解剥离)
    → 库内 ohosMain 无头门面(组合测量 → needMeasure 尺寸往返)
    → C ABI(@CName × 2)+ pass 快照 JSON
    → C++ N-API(DisposeString 纪律)
    → ArkTS 手势采样 + 钳制 + 渲染映射
    → ArkUI 真机五 Tab 验收(关系 / 矛盾求解 / 愈合复活 / 路由飞行 / 懒测量)

9.2 封装层次

  • 引擎层:上游原版关系 DSL、图构建与墓碑愈合、BFS 联合布放、约束求解器、暂定位置闭包;
  • 状态层:上游原版快照状态、方向性滚动钳制、搁浅回落、路由 BFS、性能三相钩子;
  • 门面层:无头 pass 镜像 + 尺寸往返(唯一新增库源码);
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、手势采样、讨尺寸应答、渲染映射与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:社区 Compose klib 线(1.12.0-0.1.0-07)覆盖布局引擎全部符号后,CMP 库的正确答案从"抽内核 / 隔离 Compose"进化为"零修改整编 + 无头门面"——LazySurface 连 composeMain 都不需要;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 布局引擎的宿主接缝只有一处,找准它比绕开它便宜:全引擎唯一必须组合宿主的步骤是"组合测量",把它换成宿主供尺寸的往返,惰性、求解、愈合、钳制全部原样过桥;接缝找错(比如把手势或渲染搬进 Kotlin)会复制一半引擎;
  3. 交叉验证是"引擎没漂"的唯一可信证明:142 项 desktopTest(上游 134 项原件 + 门面 8 项)跑的是同一份代码,上游测试零改动才让"零修改编译"有说服力;布局类库没有存储类库"重启还在"的单一闭环,验收重心应放在坐标断言与钳制可重复性上。

9.4 适配成果

当前适配已完成:

  • LazySurface 0.2.0 真实引擎的双 ABI Kotlin/Native 编译(上游 17 个 commonMain 文件零重写、零隔离,库内仅新增 1 个无头门面源文件);
  • 社区 Compose 制品线在布局库上的完整落地(runtime / foundation / ui / animation 1.12.0-0.1.0-07 + collection-internal + coroutines 1.11.0-0.1.0-07,桌面交叉验证走 Maven Central 1.11.1);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 五 Tab 验收页面(关系布放、RTL、矛盾求解与性能 HUD、删除愈合与复活、关系路由飞行、needMeasure 懒测量、版本握手);
  • 142 项 JVM 交叉验证(141 过,唯一失败为 macOS 专属渲染基准,预期)+ HAP 构建(双 ABI 六件套 so,约 42.4 MB)、签名、真机安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐