开源鸿蒙平台 CMP 三方库「compose-webview-multiplatform」适配全流程
本文记录
compose-webview-multiplatform接入 OpenHarmony 的完整过程,覆盖跨平台 WebView 库盘点、引擎/视图源码集拆分、真实协议引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkTS Web 组件真机页面、HAP 构建和真机验收。
compose-webview-multiplatform是 JetBrains Compose Multiplatform 生态的 WebView 库:一个WebView()composable 统一 Android(android.webkit.WebView)、iOS(WKWebView)、Desktop(KCEF/Chromium)与 WasmJS(iframe)四个平台,配套WebViewState状态记忆、WebViewNavigator导航、window.kmpJsBridgeJS 双向通信、CookieManager、请求拦截与 HTML 数据加载。本次适配与 KStore 的"真编译整库"路线有本质区别:上游把 API、状态与桥协议全部放在commonMain,但其中 Compose 部分(WebView()composable、mutableStateOf状态、协程导航)与社区鸿蒙线 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 对不齐——因此采用社区已过审的引擎复用、视图隔离路线(cmp_qrose 模式):JS 桥协议引擎(JsMessage/JsMessageDispatcher/WebViewJsBridge)、内容/状态/请求/Cookie 数据模型等纯 Kotlin 引擎原样编译成 OpenHarmony KLIB,视图层由 ArkTSWeb组件承载,页面侧window.kmpJsBridge协议与 Android 注入逐字节一致,真机上跑的是上游的协议,不是重写版。
项目地址: AtomGit/oh-tpc/compose-webview-multiplatform.git
上游仓库: KevinnZou/compose-webview-multiplatform
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-webview-multiplatform 为 CMP 应用提供"网页视图"这一件事的完整答案:rememberWebViewState(url) 一行建状态,WebView(state, navigator, webViewJsBridge) 一步挂视图,页面里 window.kmpJsBridge.callNative("Greet", params, callback) 直接调原生、onCallback 收应答;native 侧 WebViewJsBridge.register(handler) 注册处理器,WebViewNavigator 提供 loadUrl / goBack / reload / evaluateJavaScript 全套导航,CookieManager 管 Cookie,RequestInterceptor 拦请求。CMP 项目把这些能力放在 commonMain,四个平台各出一个薄 actual 层。OpenHarmony 应用同样需要这套跨端 WebView 契约——尤其是已经写好 kmpJsBridge 页面与处理器的工程,但上游构建(Kotlin 2.1.20 / CMP 1.8.0 / AGP 8.9.3 / Gradle 8.13)不会生成 OpenHarmony 变体。
如果只在 ArkTS 里手写一个 WebView 页面,页面可以显示,JS 通信也能做,但协议就和 Android / iOS / Desktop 分叉了——同一套 H5 代码在不同端行为不一致。适配需要同时验证桥协议引擎、数据模型、Native 动态库、C ABI、N-API、ArkTS Web 组件页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.1.20 / CMP 1.8.0 / AGP 8.9.3 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)无法共存于一个 Gradle 构建。 |
| UI 与引擎耦合 | commonMain 里 WebView() composable、WebViewState(mutableStateOf)、WebViewNavigator(协程 SharedFlow)、WebSettings(kermit/Compose Color)与 Compose 运行时深度耦合,ohos 制品线上没有对齐的 CMP 版本。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 协议语义跨不了桥 | WebViewJsBridge.dispatch → 处理器 → onCallback 回注脚本 的回调链是 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:桥协议引擎与数据模型的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。视图层由 ArkTS Web 组件原生承载——这本来就是上游架构的延续(四个平台的 ActualWebView 本就是平台视图),鸿蒙只是加了第五条路线。
1.2 库提供的能力
Demo 提供一次真实的 JS 桥通信会话,对应桥接的全部入口:
- 协议注入(script):
webviewJsBridgeScript()返回上游IWebView.injectJsBridge的初始化 JS 原文(Webview.jsBridgeInitScript()逐字提取),ArkTS 在onPageEnd注入——与 Android/iOS 注入同一个字符串; - 消息分发(dispatch):页面
callNative("Greet", JSON.stringify({message}), cb)→postMessage→javaScriptProxy→WebviewDispatchMessage→JsMessage→JsMessageDispatcher→ 处理器,返回引擎产出的回注脚本原文window.kmpJsBridge.onCallback(3, '{"message":"KMP Received Hello"}'),ArkTS 原样runJavaScript; - 错误通道:调用未注册的方法名返回
ok:false, no handler for 'Nope',HAP 不崩; - Cookie 模型:
webviewCookieToString()用上游Cookie.toString()拼 Set-Cookie 串;webviewFormatCookieDate()用 ohos 纯 Kotlin 历法输出EEE, dd MMM yyyy hh:mm:ss z(GMT),与 JVM 的SimpleDateFormat输出逐字节一致; - 版本握手:
webviewVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应字段:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
callbackId | 透传页面回调 id(无回调时 -1)。 |
script | dispatch 成功时引擎产出的 onCallback 回注脚本(这是回包的主体)。 |
version / upstream | 版本握手返回 fork 版本与上游版本。 |
date / cookie | Cookie 日期格式化 / Cookie 模型拼接结果。 |
引擎侧完整能力(消息分发、处理器注册表、协议脚本、内容/状态/请求/Cookie 全部数据模型)都在上游 commonMain 里原样随编译进入 .so,JVM 侧保留同一份引擎编译做交叉对照,真机页面作为验收宿主。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 引擎 13 个 commonMain 文件原样编译,不提取、不重写;手术只有两类(剥 @Serializable / @Immutable 注解、解耦 navigator 参数);新增 1 个公开入口 Webview.kt 与 2 个 cookie 日期 actual。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose UI 12 文件移入未注册的 src/composeMain,sample/ 与四个平台源码集留盘不进构建。 |
| 桥接稳定 | 使用 5 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 引擎零第三方依赖(不引入 coroutines / serialization / kermit ohos 制品线),工具链因此可以选社区缓存最全的 2.2.21 线。 |
| UI 完整 | 三 Tab 覆盖全部桥接入口:WebView 浏览、JsBridge 全链路、Engine 版本握手与 Cookie 模型。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkTS
Web组件页面不是 Compose Multiplatform UI 实现。上游的视图层本来就是各平台原生视图(AndroidView/UIKitView/SwingPanel/ DOM),鸿蒙侧由 ArkWeb 承担同一角色;适配面集中在 composable 之下的协议引擎与数据模型。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与源码依赖度、划定引擎/composeMain 边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版(零依赖线)
第 3 阶段:引擎手术 ── 剥注解、解耦 navigator、注入脚本提取为公开入口、cookie 日期双 actual
第 4 阶段:桥协议建模 ── JsMessage JSON 契约、script 回包语义、错误通道
第 5 阶段:原生桥接 ── @CName C ABI ×5、C++ N-API 双签名包装、CMake 三件套
第 6 阶段:ArkWeb 宿主 ── Web 组件 + javaScriptProxy、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
webview/ 库本体(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
webview/src/commonMain/ 引擎 13 文件:jsbridge 协议 + Web/请求/Cookie 模型
webview/src/ohosMain/ 新增:getCookieExpirationDate actual(纯 Kotlin 历法)
webview/src/jvmMain/ 新增:同一 expect 的 JVM actual(SimpleDateFormat,对照用)
webview/src/composeMain/ 上游 Compose UI 12 文件,留盘不参与任何目标编译
webview/src/androidMain/ 等 上游四平台视图嵌入层,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + Greet 处理器)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample/ docs/ .github/ 上游示例、文档站与 CI,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :webview 与 :example:nativeApp,rootProject.name = "ohos_webview"。上游的 dokka / ktlint / vanniktech 发布插件、AGP classpath、git hooks 任务全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 8.13 升一格;2.2.21 线的社区经验值) |
| 引擎依赖 | 零第三方依赖(commonMain 不声明任何 implementation) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.webview |
export JAVA_HOME="/path/to/jdk-21" # Windows 下 gradlew 启动器也需要 JAVA_HOME
java -version
1.3 源码依赖度盘点
上游 commonMain 共 25 个 Kotlin 文件,逐一核对 import 后一分为二:
| 去向 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| 引擎(13) | JsMessage / IJsMessageHandler / JsMessageDispatcher / WebViewJsBridge | serialization 注解 + Compose 注解 + IWebView 引用 | 剥注解、解耦 webView 引用为 scriptRunner |
| 引擎(13) | WebContent / LoadingState / WebViewError / WebViewFileReadType | 纯 Kotlin | 原样(WebViewError 剥 @Immutable) |
| 引擎(13) | WebRequest / WebRequestInterceptResult | 纯 Kotlin | 原样 |
| 引擎(13) | Cookie / CookieManager / Extension | 纯 Kotlin + 1 处 expect | 接口保留,expect 工厂移走、日期 expect 补 actual |
| composeMain(12) | WebView() composable / WebViewState / WebViewNavigator / IWebView / WebViewBundle | Compose runtime / 协程 / Compose 资源 | git mv 留盘 |
| composeMain(12) | WebSettings / PlatformWebSettings / KLogger / Platform / RequestInterceptor / ConsoleBridge | kermit / Compose Color / serialization-json | git mv 留盘 |
判断标准只有一条:不 import Compose / coroutines / serialization / kermit 的文件才留在引擎。全库 expect 声明原有 11 处(NativeWebView、WebViewBundle、ActualWebView、WebViewFactoryParam、PlatformWebViewParams、defaultWebViewFactory、WebViewCookieManager、getPlatform ×3、getCookieExpirationDate),其中 10 处随 UI 移入 composeMain,引擎只保留 getCookieExpirationDate 一处——补 ohos / jvm 两个 actual,这与 KStore 全库唯一新增源文件是 8 行 Dispatchers actual 是同款"expect 最小化"纪律。
反向手术同样有先例可循:qrose 把写在 Painter 文件里的 Code128Type 抽回 commonMain(引擎引用了 UI 文件里的类型);本仓是镜像情形——引擎 WebViewJsBridge 引用了 UI 的 IWebView,解法是把 webView?.evaluateJavaScript(...) 解耦为 scriptRunner: ((String) -> Unit)?,由宿主注入执行器。注入脚本本体从 IWebView.injectJsBridge 的字符串模板逐字提取为公开入口:
// webview/src/commonMain/kotlin/com/multiplatform/webview/Webview.kt
object Webview {
const val VERSION: String = "2.0.4-ohos.1"
fun version(): String = VERSION
/** 与 IWebView.injectJsBridge 注入的初始化 JS 逐字相同 */
fun jsBridgeInitScript(jsBridgeName: String = "kmpJsBridge"): String = """
if (typeof window.$jsBridgeName === 'undefined') {
window.$jsBridgeName = { callbacks: {}, callbackId: 0,
callNative: function (methodName, params, callback) { ... },
onCallback: function (callbackId, data) { ... } };
}
""".trimIndent()
}
1.4 页面边界
标题区 三 Tab 布局(WebView / JsBridge / Engine)
WebView Tab 地址栏 + Go、进度条、Web 组件、Back/Forward/Reload、状态行
JsBridge Tab 状态条(bridge injected / handled / native error)
Web 组件加载 $rawfile greet.html + javaScriptProxy(kmpNativeBridge)
Native 日志区(JS> 请求 / KN< 响应,最新在前)
Engine Tab 版本握手行、注入脚本长度、Cookie 日期格式化、Cookie.toString()
页面启动时先调 webviewVersion() 做握手;JsBridge Tab 在 onPageEnd 注入协议脚本并接线 postMessage。
第 2 阶段:目标与依赖打通
2.1 制品线决策(零依赖线,本适配的路线手)
先查社区 Maven(https://maven.eazytec-cloud.com)上的鸿蒙制品分布:kotlin-stdlib 有 2.2.21-1.0.0 与 2.4.20-0.1.0-01…08 两条线;kotlinx-serialization-json 的 ohos 变体在 1.11.0-0.1.0-02/05/07(2.4.20 线);kotlinx-coroutines-core 在 1.10.2-0.5.0-07…15。
两条可选路线的取舍:走 2.4.20 线可以保留上游的 @Serializable JsMessage 与 serialization 助手,但要整体升 Kotlin 与 Gradle、与 qrose 已验证的缓存线分叉;走 2.2.21 线则引擎必须零依赖。盘点结果显示引擎唯一的 serialization 触点是 JsMessage 一个注解与两个内联助手——桥上解析的 JSON 只有一个扁平对象,MiniJson(qrose 同款)百行内解决。选 2.2.21-1.0.0 + 零依赖引擎:本机 Konan 工具链缓存(sysroot-ohos-* / cpf-llvm-*)直接复用,构建可复现;上游的 coroutines 1.10.2 / serialization 1.8.1 / kermit 2.0.3 依赖随 composeMain 留盘,接回时恢复。
2.2 根构建替换
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()
}
}
// dependencyResolutionManagement 同款双仓置前
rootProject.name = "ohos_webview"
include(":webview")
include(":example:nativeApp")
根 build.gradle.kts 收敛为版本声明 + 测试纪律(group 沿用上游 io.github.kevinnzou,版本走 gradle.properties 的 version=2.0.4-ohos.1):
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}
allprojects {
group = "io.github.kevinnzou"
version = providers.gradleProperty("version").orElse("2.0.4-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
gradle/libs.versions.toml 砍到只剩 HarmonyOS Kotlin 的 multiplatform 插件;wrapper 从 8.13 升到 8.14.1;gradle.properties 写死 JDK 21 路径并带上三条 kotlin.native 开关。
2.3 编译真实引擎
webview/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 引擎零依赖
}
}
实测:
./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
# BUILD SUCCESSFUL
引擎文件的协议逻辑一行没动,改动只有"剥注解 / 去 navigator / 提取字符串常量"三类。
2.4 expect/actual 处置:cookie 日期的 ohos 实现
引擎保留的唯一 expect 是 getCookieExpirationDate(Long): String。上游 Android/Desktop actual 用 SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US) + GMT;JVM actual 照抄这份实现作对照,ohos actual 用纯 Kotlin 历法(KN 没有 SimpleDateFormat):
// webview/src/ohosMain/.../GetCookieExpirationDate.ohos.kt
actual fun getCookieExpirationDate(expiresDate: Long): String {
val days = floorDiv(floorDiv(expiresDate, 1000L), 86400L) // 自 1970-01-01 的天数
val (year, month, day) = civilFromDays(days) // Howard Hinnant 算法
val weekday = WEEKDAYS[floorMod(days + 4L, 7L).toInt()] // 1970-01-01 是周四
val h12 = h24 % 12; val hour12 = if (h12 == 0L) 12L else h12 // hh 12 小时制语义
return "$weekday, ${pad2(day)} ${MONTHS[month - 1]} $year ${pad2(hour12)}:${pad2(minute)}:${pad2(second)} GMT"
}
epoch 0 两边都输出 Thu, 01 Jan 1970 12:00:00 GMT——hh 是 12 小时制,00:00 输出 12,这个细节对不齐就会与 Android 逐字节比对失败。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.multiplatform.webview.jsbridge/**(上游引擎,原样编译)
↓ JsMessage / JsMessageDispatcher / WebViewJsBridge
Bridge.kt(@CName × 5,扁平 JSON 进出)
↓ UTF-8 JSON
N-API webviewDispatchMessage() 等
↓
ArkTS Web 组件(javaScriptProxy → runJavaScript)
请求与响应:
请求 : {"methodName":"Greet","params":"{\"message\":\"Hello\"}","callbackId":3}
响应 : {"ok":true,"callbackId":3,"script":"window.kmpJsBridge.onCallback(3, '{\"message\":\"KMP Received Hello\"}')"}
{"ok":false,"error":"no handler for 'Nope'","callbackId":4}
params 是 JSON 文本的字符串转义形式(页面 JSON.stringify 一行可得,与上游 JsMessage.params 语义一致),内层结构对桥不透明、原样透传给处理器。
3.2 script 回包语义:回包不是数据,是引擎产出的脚本
这是本次桥协议的核心设计。WebViewJsBridge 的上游回调链是 处理器 callback(data) → onCallback(data, id) → webView.evaluateJavaScript("window.kmpJsBridge.onCallback(id, 'data')")。桥把最后一步的脚本原文作为回包返回,宿主在 scriptRunner 里捕获:
// example/nativeApp/src/ohosMain/kotlin/Bridge.kt
private val bridge = WebViewJsBridge().apply {
register(GreetJsMessageHandler()) // 与 sample/shared 同名同应答
scriptRunner = { script -> lastScript = script }
}
@CName("WebviewDispatchMessage")
fun webviewDispatchMessage(request: String): String = try {
val json = MiniJson.parse(request)
val message = JsMessage(json.int("callbackId", -1), json.str("methodName"), json.str("params"))
bridge.dispatch(message)
val script = lastScript
if (script == null) """{"ok":false,"error":"no handler ...","callbackId":${message.callbackId}}"""
else """{"ok":true,"callbackId":${message.callbackId},"script":${jsonString(script)}}"""
} catch (t: Throwable) { """{"ok":false,"error":...}""" }
ArkTS 拿到 script 直接 controller.runJavaScript(script)——与 Android/iOS 的 evaluateJavaScript 完全同源。这样整条 dispatch → 处理器 → onCallback 链路全部在上游引擎内部走完,桥只搬运结果。
页面接线与 Android 逐字对照:
| Android(AndroidWebView.kt) | HarmonyOS Demo |
|---|---|
addJavascriptInterface(this, "androidJsBridge") | .javaScriptProxy({ object, name: 'kmpNativeBridge', methodList: ['call'] }) |
injectJsBridge() 注入初始化 JS | webviewJsBridgeScript() 返回同一份字符串,onPageEnd 注入 |
window.kmpJsBridge.postMessage = function (m) { window.androidJsBridge.call(m); } | window.kmpJsBridge.postMessage=function(m){window.kmpNativeBridge.call(m);} |
Json.decodeFromString<JsMessage> + dispatch | WebviewDispatchMessage(MiniJson → JsMessage → dispatch) |
evaluateJavaScript(onCallback 脚本) | scriptRunner 捕获同一句脚本,ArkTS runJavaScript |
Demo 的 GreetJsMessageHandler 与上游 sample/shared 的同名处理器协议一致:方法名 Greet、应答 {"message":"KMP Received <name>"}——greet.html 不做任何改动就能在两端跑同一协议。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;未注册方法名走 no handler 显式通道;非法 JSON、类型错误同样收敛到 ok:false,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libwebview_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoswebview.so)
│ WebviewVersion / WebviewJsBridgeScript / WebviewDispatchMessage
│ WebviewFormatCookieDate / WebviewCookieToString
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
WebviewVersion | webviewVersion() | 版本握手(真机版本行数据来源) |
WebviewJsBridgeScript | webviewJsBridgeScript() | Webview.jsBridgeInitScript() 协议注入原文 |
WebviewDispatchMessage | webviewDispatchMessage(req) | JsMessage 构造 + WebViewJsBridge.dispatch 全链路 |
WebviewFormatCookieDate | webviewFormatCookieDate(req) | ohos actual 纯历法日期格式化 |
WebviewCookieToString | webviewCookieToString(req) | 上游 Cookie.toString() 模型拼接 |
4.3 C++ N-API 方法分发:两个签名的教训
五个 KN 入口分两类:三个带参(const char* f(const char*))、两个无参(const char* f(void))。C++ 函数指针不做隐式转换,最初把无参入口塞进带参包装直接编译失败(no known conversion from 'const char *()' to 'const char *(*)(const char *)')——这正是本适配真机联调时踩过并修掉的坑。最终形态是双入口包装 + 公共尾部:
// 公共尾部:KN 字符串 → JS 字符串,随后必须 DisposeString 归还 Kotlin 运行时
static napi_value KnStringToNapi(napi_env env, const char *raw, const char *name);
// 带参入口:napi_typeof 校验 string → OH_LOG_INFO 打字节数 → entry(request.c_str())
static napi_value CallStringEntry(napi_env, napi_callback_info,
const char *(*entry)(const char *), const char *name, bool requireArg);
// 无参入口:version / jsBridgeScript
static napi_value CallVoidStringEntry(napi_env, napi_callback_info,
const char *(*entry)(void), const char *name);
libohoswebview_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与五个业务符号),不拖整份生成头;nm_modname 为 webview_napi,与 ArkTS import 的 libwebview_napi.so、index.d.ts 导出名、napi_define_properties 四处严格一致。
4.4 CMake 和类型声明
add_library(ohoswebview SHARED IMPORTED)
set_target_properties(ohoswebview PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(webview_napi SHARED napi_init.cpp)
target_link_options(webview_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(webview_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoswebview)
OHOS_ARCH 为空或 libohoswebview.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const webviewVersion: () => string;
export const webviewJsBridgeScript: () => string;
export const webviewDispatchMessage: (request: string) => string;
export const webviewFormatCookieDate: (request: string) => string;
export const webviewCookieToString: (request: string) => string;
第 5 阶段:ArkWeb 宿主
5.1 javaScriptProxy 注入与消息触发
aboutToAppear(): void {
this.proxy.setHandler((msg: string) => this.onNativeMessage(msg));
this.refreshVersion(); // 版本握手:真实 Native 调用
}
private onNativeMessage(msg: string): void {
setTimeout(() => { // proxy 回调不在 UI 线程,先转回主线程
const res = JSON.parse(webviewDispatchMessage(msg)) as NativeResult;
this.bridgeLog = 'JS> ' + msg + '\nKN< ' + res + '\n\n' + this.bridgeLog;
if (res.ok && res.script !== undefined) {
this.bridgeStatus = 'handled, callbackId=' + res.callbackId;
this.bridgeController.runJavaScript(res.script); // 引擎产物的 onCallback 脚本
} else {
this.bridgeStatus = 'native error: ' + res.error;
}
}, 0);
}
// onPageEnd 时注入协议 + postMessage 接线(与 Android 注入顺序一致)
private injectBridge(): void {
const parsed = JSON.parse(webviewJsBridgeScript()) as NativeResult;
const wiring = 'window.kmpJsBridge.postMessage=function(m){window.kmpNativeBridge.call(m);};';
this.bridgeController.runJavaScript(parsed.script + '\n' + wiring);
this.bridgeStatus = 'bridge injected (kmpJsBridge ready)';
}
Web 组件挂载 javaScriptProxy({ object: this.proxy, name: 'kmpNativeBridge', methodList: ['call'] }),加载本地 $rawfile('greet.html')——页面提供名字输入框、Greet 按钮、未知方法按钮和滚动日志。
5.2 页面交互
WebView Tab:地址栏 + Go、onProgressChange 驱动进度条、Back/Forward/Reload(accessBackward 先检查)、主帧错误进状态行。JsBridge Tab:状态条实时显示 bridge injected → handled, callbackId=N,Native 日志区按 JS> 请求 / KN< 响应 成对留痕。Engine Tab:版本握手行、注入脚本长度、Cookie 日期格式化与 Cookie.toString() 两个演示按钮。
5.3 错误边界与 ArkTS 严格模式
ok:false 时错误原文进状态条与日志;Native 调用抛错(理论不该发生)同样被 try/catch 收进日志区。ArkTS 严格模式在联调时贡献了三个真实编译错:Tabs 底部栏是 BarPosition.End(没有 Bottom);Web 事件是 onPageBegin(不是 onPageStart);事件回调参数必须显式类型(OnProgressChangeEvent / OnErrorReceiveEvent),否则 arkts-no-any-unknown 拒绝编译。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoswebview)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 5 + MiniJson + Greet 处理器
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.webview、versionName 2.0.4-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/libohoswebview_api.h
├── src/main/cpp/types/libwebview_napi/{index.d.ts, oh-package.json5}
├── src/main/resources/rawfile/greet.html 本地协议验收页
└── src/main/ets/pages/Index.ets ArkTS 三 Tab 验收页面
6.2 构建、签名和安装
./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 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.webview -m entry
四、完整代码对照
4.1 整体架构
com.multiplatform.webview.jsbridge/**(上游协议引擎原样编译)
│ JsMessage / JsMessageDispatcher / WebViewJsBridge / IJsMessageHandler
com.multiplatform.webview.{web,request,cookie}/**(上游数据模型原样编译)
│ WebContent / LoadingState / WebViewError / WebRequest / Cookie
Webview.kt(新增公开入口:版本 + 协议脚本逐字提取)
Bridge.kt + MiniJson
│ @CName × 5
libwebview_napi.so
│ webviewVersion / webviewJsBridgeScript / webviewDispatchMessage / …
Index.ets + Web 组件
│ javaScriptProxy + runJavaScript
▼
ArkTS 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_webview、sample 移出 |
gradle/libs.versions.toml | 只留 HarmonyOS Kotlin 2.2.21-1.0.0 multiplatform 插件 |
build.gradle.kts(根) | 版本 2.0.4-ohos.1、全局禁用 Ohos*Test* 任务 |
webview/build.gradle.kts | 纯 KMP:jvm() + ohosArm64 + ohosX64,零依赖 |
webview/src/commonMain/(13 文件) | 引擎:剥注解 / 解耦 navigator 后原样编译 |
webview/src/composeMain/(12 文件) | 上游 Compose UI 留盘,接回上游工具链时恢复 |
webview/src/{ohosMain,jvmMain}/…/GetCookieExpirationDate.*.kt | 唯一 expect 的双 actual(纯历法 / SimpleDateFormat) |
webview/src/commonMain/…/Webview.kt | 公开入口(版本 + 注入脚本逐字提取) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 5 个 C ABI、scriptRunner 捕获、MiniJson、Greet 处理器 |
harmonyApp/.../cpp/napi_init.cpp | N-API 双签名导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkTS 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(27 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | JsMessage(callbackId, methodName, params) | 页面 → native 的消息模型(桥请求体同名同义) |
| 引擎 | JsMessageDispatcher.dispatch | 方法名 → 处理器路由 |
| 引擎 | WebViewJsBridge.dispatch(message) | 全链路入口,内部走 onCallback |
| 引擎 | IJsMessageHandler.handle(message, callback) | 处理器接口(Demo 的 Greet 实现) |
| 引擎 | Cookie.toString() / getCookieExpirationDate | Set-Cookie 拼接 / GMT 日期格式化 |
| 入口 | Webview.jsBridgeInitScript() | 与 IWebView.injectJsBridge 逐字相同的注入脚本 |
| Native | WebviewDispatchMessage 等 × 5 | C ABI JSON 导出 |
| N-API | webviewDispatchMessage(req) 等 | 向 ArkTS 暴露能力 |
| ArkTS | onNativeMessage(msg) | proxy 回调 → 转主线程 → dispatch → runJavaScript(script) |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(webviewDispatchMessage(JSON.stringify({
methodName: 'Greet',
params: JSON.stringify({ message: 'Hello' }),
callbackId: 1
})));
// res.ok === true
// res.script === "window.kmpJsBridge.onCallback(1, '{"message":"KMP Received Hello"}')"
this.controller.runJavaScript(res.script);
val bridge = WebViewJsBridge().apply {
register(GreetJsMessageHandler())
scriptRunner = { script -> lastScript = script } // 宿主注入的执行器
}
bridge.dispatch(JsMessage(1, "Greet", """{"message":"Hello"}"""))
// lastScript == "window.kmpJsBridge.onCallback(1, '{"message":"KMP Received Hello"}')"
两者之间只传输扁平 JSON 字符串与脚本原文,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:拆引擎而不是真编译整库
KStore 能整库编译,是因为它的 commonMain 零 UI 耦合;本库的公共层一半是 Compose(composable、mutableStateOf 状态、协程导航、Compose 资源),ohos 制品线上没有与之对齐的 CMP。把引擎从 UI 里剥出来——协议、模型是纯 Kotlin,视图本来就是平台原生——既守住了"页面协议逐字一致"的跨端价值,又不必等 CMP 工具链对齐。
决策 2:工具链选 2.2.21-1.0.0 零依赖线,Gradle 8.14.1
引擎剥到零依赖后,kotlinx ohos 制品线(serialization 1.11.0-0.1.0-xx、coroutines 1.10.2-0.5.0-xx,都在 2.4.20 线)就没有存在必要——为 JsMessage 一个注解升整条工具链不划算。2.2.21 线与 qrose 完全同款,本机 Konan 缓存(sysroot / LLVM)直接复用;Gradle 按 2.2.21 线经验值从 8.13 升到 8.14.1。桥上 JSON 用 MiniJson 解决,与 qrose 的取舍一致。
决策 3:根构建替换(cmp_qrose 模式),Compose UI 与 sample 留盘
上游根构建(dokka / ktlint / vanniktech / AGP / git hooks)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。12 个 Compose 文件 git mv 到未注册的 src/composeMain(保留 git 重命名历史),sample/ 四端示例与平台源码集留盘不进 settings.gradle.kts——它们的存在方式只有这一种,接回上游工具链时原样恢复。
决策 4:script 回包语义 + navigator 解耦
桥的回包不是处理器的数据,而是引擎 onCallback 产出的回注脚本原文——宿主 runJavaScript 执行的字符串与 Android evaluateJavaScript 的完全相同,"引擎复用"从签名级提升到字节级。配套解耦是 navigator:导航属于宿主页面(ArkTS 路由 / Compose Navigator),引擎只管消息路由,WebViewJsBridge 的 webView: IWebView 引用换成 scriptRunner: ((String) -> Unit)?。
决策 5:cookie 日期 ohos 纯历法 + JVM 对照
引擎唯一保留的 expect 是 getCookieExpirationDate。ohos actual 不引任何日期库,纯 Kotlin 历法(civil-from-days + 周四锚点 + 12 小时制)拼出与 SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z") + GMT 逐字节一致的输出;JVM actual 照抄上游实现作对照。这是"expect 最小化 + 可对账"纪律在本库的落点,对应 KStore 的 8 行 Dispatchers actual。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium 21.0.12.101)、DevEco Studio 5.1.0(18)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 编译与符号验证
:webview:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: WebviewVersion / WebviewJsBridgeScript / WebviewDispatchMessage /
WebviewFormatCookieDate / WebviewCookieToString 均为全局 T 符号(双 ABI)
实测产物:
| 产物 | 大小 |
|---|---|
arm64-v8a/libohoswebview.so | 2,379,632 B(约 2.3 MB) |
arm64-v8a/libc++_shared.so | 1,850,184 B |
x86_64/libohoswebview.so | 2,014,016 B(约 2.0 MB) |
x86_64/libc++_shared.so | 1,731,432 B |
链接期 CAdapter 对 @CName 字符串入口有 5 条 non-void function does not return a value in all control paths 告警(每个入口一条),与 qrose 相同,属无害告警。JVM 侧同一份引擎编译通过,构成"无 Compose、无三方库前提下自洽"的交叉对照;Webview.jsBridgeInitScript() 与 composeMain 里 IWebView.injectJsBridge 的字符串常量逐字比对一致。
6.3 HAP 验证
BuildNativeWithNinja: finished(双 ABI 编出 libwebview_napi.so,三件套同目录)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
联调过程中真实踩掉两轮编译错(N-API 函数指针签名、ArkTS 严格模式三处),均已修复入库——细节见 8.1 踩坑复盘。
6.4 功能用例
- 启动 Engine Tab 即出现
engine 2.0.4-ohos.1 (upstream 2.0.4)(真实 Native 握手,非静态文字); - WebView Tab 默认加载 example.com,进度条随
onProgressChange走到 100,Back/Forward/Reload 可用; - JsBridge Tab 加载本地
greet.html,onPageEnd后状态条显示bridge injected (kmpJsBridge ready); - 输入名字点 Call Native Greet,页面日志出现
JS <- onCallback: {"message":"KMP Received Hello"},Native 日志出现KN< {"ok":true,"callbackId":N,"script":"window.kmpJsBridge.onCallback(N, '…')"},状态条变handled, callbackId=N; - 点 Call Unknown Method,页面无回调(预期),Native 侧
ok:false, no handler for 'Nope',HAP 不崩; - Engine Tab 点 Format cookie expiration,输出形如
Wed, 21 Oct 2026 xx:xx:xx GMT(12 小时制,与 JVM 对照);点 Build Cookie.toString(),输出session=kmp; Path=/; Domain=example.com; Expires=…; Max-Age=3600; SameSite=LAX;。
6.5 验证结论
协议引擎、数据模型、Native 动态库、C ABI、N-API、ArkTS Web 组件页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的桥通信会话:协议注入、Greet 全链路应答、错误通道与 Cookie 模型闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-webview-multiplatform.git
cd compose-webview-multiplatform
export JAVA_HOME="/path/to/jdk-21"
./gradlew :webview:compileKotlinJvm :webview:compileKotlinOhosArm64 :webview:compileKotlinOhosX64
./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.webview -m entry
hdc shell hilog | grep Webview
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:webview真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 下
gradlew启动器读不到gradle.properties里的org.gradle.java.home,必须另设JAVA_HOME环境变量,否则报JAVA_HOME is not set; @CName无参与带参入口的 C 函数指针签名不同(const char*(void)vsconst char*(*)(const char*)),C++ 包装必须两个重载,硬塞一个签名直接no matching function for call编译失败;- ArkTS 严格模式三连:Tabs 底部栏是
BarPosition.End(没有Bottom);Web 事件名是onPageBegin(不是onPageStart);事件回调参数必须显式类型((event: OnProgressChangeEvent)),否则arkts-no-any-unknown拒绝编译; javaScriptProxy的回调不在 UI 线程,runJavaScript与@State更新都要先setTimeout(…, 0)转回主线程;- 页面就绪日志早于
onPageEnd注入属预期时序,别把kmpJsBridge = undefined误判为注入失败——以状态条与按钮回路为准; entry/build-profile.json5的abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可,缺 so 安装直接报9568347;- KN 返回字符串必须经
libohoswebview_symbols()->DisposeString归还 Kotlin 运行时,N-API 层不能free(); build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组,勾选 DevEco 自动签名后由 IDE 回填,提交前git update-index --skip-worktree保护,证书绝不入库;- 根锚定 gitignore 盖不住嵌套目录——DevEco 打开工程后生成的
example/harmonyApp/.idea/、entry/build/、entry/libs/要单独追加 gitignore; - 改了 Kotlin 源码要重跑
linkDebugShared*再打 HAP,CMake 只认entry/libs里的文件时间戳。
8.2 已知问题
WebView()composable、WebViewState/WebViewNavigator(Compose 状态与协程)、WebSettings/PlatformWebSettings未进引擎,设置项在 ArkTSWeb组件属性上直接配置;RequestInterceptor接口(带 navigator 参数)随 UI 留盘,WebRequest/WebRequestInterceptResult模型在引擎里但拦截执行依赖 ArkTSonInterceptorRequest,Demo 未覆盖;ConsoleBridge(依赖 serialization-json)未进引擎,控制台转发未覆盖;- 引擎内
JsMessage剥除了@Serializable,序列化助手留在 composeMain——接回上游工具链时随 UI 一起恢复; - 桥回调数据沿用上游「单引号包裹」约定(
onCallback(id, '…')),数据含单引号时需先转义——与 Android 行为一致,不是鸿蒙退化; - Demo 的 bridge 是进程级单例(
scriptRunner捕获最后一次脚本),多 WebView 实例场景需为每个控制器建独立 bridge; - Android / iOS / Desktop / WasmJS 目标与
sample/剥离(源文件留在树里),发布、dokka、ktlint 一并剥离,本 fork 不发 Maven Central; - HAP 安装必须使用与 bundle name
org.terminator.ohos.webview匹配的签名。
8.3 未来优化方向
- 把
WebSettings的纯数据字段下沉引擎,供 ArkTS 侧生成配置(当前依赖 KLogger / Color 的部分留 UI); - 桥支持多 bridge 实例与脚本队列语义,对齐上游"每 WebView 一个 bridge"的持有方式;
- 补充请求拦截入口(ArkTS
onInterceptorRequest↔ 引擎WebRequestInterceptResult模型); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 跟踪社区 CMP ohos 线:
compose.uiohos 制品与上游 CMP 对齐后接回composeMain,引擎与 ArkTS 承载降级为过渡方案。
九、总结
9.1 核心难点回顾
上游 webview commonMain(25 文件)
→ 源码依赖度盘点:13 引擎 + 12 Compose(标准:不 import Compose/协程/serialization/kermit)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(零依赖线,Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式)
→ 引擎手术:剥注解 / 解耦 navigator / 注入脚本逐字提取为公开入口
→ ohosArm64 / ohosX64 KLIB + cookie 日期双 actual
→ C ABI(@CName × 5)+ script 回包语义
→ C++ N-API(双签名包装 + DisposeString 纪律)
→ ArkTS Web 组件 + javaScriptProxy(kmpNativeBridge ↔ androidJsBridge)
→ 真机三 Tab 验收(协议注入 / Greet 全链路 / Cookie 模型)
9.2 封装层次
- 引擎层:上游原版 jsbridge 协议(
JsMessage/ 分发器 /WebViewJsBridge)与 Web/请求/Cookie 数据模型; - 入口层:新增
Webview公开入口(版本 + 与injectJsBridge逐字相同的协议脚本); - Native 层:生成双 ABI 动态库并通过 5 个 C ABI 输出 JSON 与脚本原文;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放(双签名包装);
- ArkTS 层:管理页面状态、协议注入、proxy 回调转主线程、
runJavaScript回注与日志; - DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线按"库的耦合形态"选:KStore 的
commonMain零 UI 耦合,正确答案是真编译整库;本库 UI 与协议混在commonMain,正确答案是拆引擎——跨端库的复用主体是契约(这里是window.kmpJsBridge协议与消息模型),路线选择围绕"契约能不能原样过桥"展开; - 零依赖引擎让工具链选择自由:引擎剥干净后不需要 kotlinx ohos 制品线,就能留在缓存最全、社区验证最多的 2.2.21 线上——依赖越少,适配的自由度越大,这条优先级应该排在"保留注解便利性"之前;
- “页面能感知的行为逐字一致、宿主感知的行为原生承载"是可执行的验收口径:注入脚本逐字提取、回包是引擎产出的
onCallback脚本原文、Greet 应答与 sample 同名同格式——三者叠加,才能宣称"真机跑的是上游协议,不是重写版”。
9.4 适配成果
当前适配已完成:
- compose-webview-multiplatform
2.0.4协议引擎的双 ABI Kotlin/Native 编译(引擎 13 文件零重写,手术仅剥注解与解耦两类,新增 1 个公开入口 + 2 个 cookie 日期 actual); - 5 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 全局 T 符号,arm64 约 2.3 MB / x86_64 约 2.0 MB); - DevEco Stage ArkTS 三 Tab 验收页面(WebView 浏览、JS 桥全链路、版本握手与 Cookie 模型);
greet.html与 Android 示例页协议级兼容(方法名、应答格式、注入脚本逐字一致);- HAP 构建(双 ABI 三件套)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 27 节)和本地效果图。
适配过程 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)