开源鸿蒙平台 CMP 三方库「LazySurface」适配全流程
本文记录
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/ohosX64klib 变体)已经覆盖布局引擎依赖的全部符号,因此不需要提取内核、也不需要把 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 组合宿主,
LazySurfacecomposable 已编译进 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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证) |
| Gradle | 9.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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.lazysurface |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 文件群 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
| 纯 Kotlin 引擎(求解器 / 关系 DSL / 性能与调试钩子) | 4 | 零 Compose import | 整体编译(jvm + 双 ohos) |
| 引擎 + 快照状态(State / MeasureLogic / 暂定位置 / ItemInfo) | 4 | runtime 快照 + ui 几何值类 + foundation.lazy.layout 接口 | 整体编译(无宿主 API) |
| Compose UI(composable 本体 / overscroll / snap / ItemScope) | 9 | pointerInput / 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 方法 | 覆盖内核 |
|---|---|---|
LazySurfaceDispatch | lazySurfaceDispatch(req) | 注册 + 愈合 + 布放 pass + 求解 + 钳制 + 路由 + 懒测量 |
LazySurfaceVersion | lazySurfaceVersion() | 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.toml | Kotlin 2.4.20-0.1.0-07 + ohos Compose 线 / Maven Central 桌面线双列 |
build.gradle.kts(根) | 版本 0.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
LazySurface/build.gradle.kts | jvm(“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.kt | 2 个 C ABI、op 分发、递归 JSON 解析器、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 五 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 |
| Native | LazySurfaceDispatch / LazySurfaceVersion | C ABI JSON 导出 |
| N-API | lazySurfaceDispatch(req) / lazySurfaceVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(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 拒绝 |
MeasurePassBenchmark | 1 | macOS 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 功能用例
- 启动后版本行出现
lazySurfaceVersion() → 0.2.0-ohos.1(来自真实 Native 调用,非静态文字); - 关系 Tab:hero 居中原点,stats 在 hero 下方 16px,sleep 在 stats 右侧 16px、垂直居中——无一个坐标,全靠关系;切 Rings 看环形排布;开 RTL 整体水平镜像,
startOf/endOf与 Start/End 对齐随之翻转; - 求解器 Tab:7×7 晶格 49 节点全部解析,性能行显示
measure / resolve / solve三相微秒;开"矛盾"后软约束融合、晶格出现可见拉伸,solveUs 上升; - 自愈 Tab:删除 a2 后 a3 及尾部拼接到幸存锚点(间距保持);复活链后 a2 赢回原关系;
- 导航 Tab:点"飞往 far",状态行先显示
路由: mid → far关系路径,视口沿路径逐跳飞行(每跳经真实 gesturePan 钳制); - 懒测量 Tab:向下滚动,讨尺寸行出现
needMeasure: a3...,ArkTS 应答后卡片落位——远处条目接近才测量; - 双指捏合缩放(0.4–2.5),平移到内容边缘被钳制,再次同向巨幅平移 offset 不再移动(方向性钳制的可重复性);
- 篡改关系为重复 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:LazySurface真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游 wrapper 9.4.1 的本机 dists 缓存是半截下载(只有
.part/.lck,无.ok标记),wrapper 反复尝试联网下载超时——换成本机缓存完整的 9.6.1 一次通过;缓存完整与否看.ok文件; - ohos Compose 线不发布
*-desktop子制品,commonMain放 Compose 依赖会让 desktop 目标解析失败——依赖必须按目标分线(ohosMain / desktopMain); androidx.collection的上游坐标没有 ohos 变体:从 foundation1.12.0-0.1.0-07的.module依赖表里找到遮蔽坐标org.jetbrains.compose.collection-internal:collection:1.5.0-0.1.0-07(包名仍是androidx.collection)——找不到坐标时,先读上游制品的依赖表而不是猜版本号;- Gradle 9 对
include(":example:nativeApp")要求目录真实存在,否则配置期报Configuring project ':example' without an existing directory——先建模块目录再 sync; - ArkTS 严格模式禁止无类型对象字面量(
arkts-no-untyped-obj-literals):给每种请求声明 interface,const req: PanReq = {...}再JSON.stringify(req); @Component struct的成员名不能撞 ArkUI 内置属性:私有字段offset与CustomComponent的offset(...)冲突直接编译失败,改名centerOffset;- 全局
px2vp/vp2px已标记废弃:用display.getDefaultDisplaySync().densityPixels取密度自算换算,一次初始化双向使用; - 命令行
hvigorw assembleHap的PackageHap报spawn java ENOENT:打包工具链要找java,把 DevEco 自带 JBR 加进 PATH 即解(DevEco 内构建无此问题); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();签名材料由 DevEco 自动写入build-profile.json5,仓库内signingConfigs保持空数组,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙没有 Compose 组合宿主:
LazySurfacecomposable、Modifier.animateItem、snap fling、overscroll 工厂已编译进 klib 但设备上无调用方,Demo 视觉由 ArkUI 复刻; - 无头门面是布放 pass 的镜像而非同一函数:JVM 冒烟测试兜底,上游若改
measureLazySurfaceImpl门面需同步(测试会红); - 条目动画与预取未暴露:帧时钟 / 子组合宿主特性,纯视觉与延迟优化,无头路径无对应物;
- KN so 约 20–29 MB(strip 前):Compose 四件套 klib 静态链入所致,同代方案同量级;多 CMP 库共存时共享同一份运行时副本,不叠加;
- 桥当前单实例单曲面:多曲面需要 JSON 加
surfaceid 字段(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 三条经验
- 先验证生态制品线再定适配路线:社区 Compose klib 线(1.12.0-0.1.0-07)覆盖布局引擎全部符号后,CMP 库的正确答案从"抽内核 / 隔离 Compose"进化为"零修改整编 + 无头门面"——LazySurface 连
composeMain都不需要;路线选择必须基于当天的制品可达性,而不是历史结论; - 布局引擎的宿主接缝只有一处,找准它比绕开它便宜:全引擎唯一必须组合宿主的步骤是"组合测量",把它换成宿主供尺寸的往返,惰性、求解、愈合、钳制全部原样过桥;接缝找错(比如把手势或渲染搬进 Kotlin)会复制一半引擎;
- 交叉验证是"引擎没漂"的唯一可信证明: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 + coroutines1.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。
参考文档
更多推荐



所有评论(0)