本文记录 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.kmpJsBridge JS 双向通信、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,视图层由 ArkTS Web 组件承载,页面侧 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)。
scriptdispatch 成功时引擎产出的 onCallback 回注脚本(这是回包的主体)。
version / upstream版本握手返回 fork 版本与上游版本。
date / cookieCookie 日期格式化 / 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 wrapper 8.13 升一格;2.2.21 线的社区经验值)
引擎依赖零第三方依赖(commonMain 不声明任何 implementation)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 / WebViewJsBridgeserialization 注解 + 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 / WebViewBundleCompose runtime / 协程 / Compose 资源git mv 留盘
composeMain(12)WebSettings / PlatformWebSettings / KLogger / Platform / RequestInterceptor / ConsoleBridgekermit / Compose Color / serialization-jsongit 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() 注入初始化 JSwebviewJsBridgeScript() 返回同一份字符串,onPageEnd 注入
window.kmpJsBridge.postMessage = function (m) { window.androidJsBridge.call(m); }window.kmpJsBridge.postMessage=function(m){window.kmpNativeBridge.call(m);}
Json.decodeFromString<JsMessage> + dispatchWebviewDispatchMessage(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 方法覆盖内核
WebviewVersionwebviewVersion()版本握手(真机版本行数据来源)
WebviewJsBridgeScriptwebviewJsBridgeScript()Webview.jsBridgeInitScript() 协议注入原文
WebviewDispatchMessagewebviewDispatchMessage(req)JsMessage 构造 + WebViewJsBridge.dispatch 全链路
WebviewFormatCookieDatewebviewFormatCookieDate(req)ohos actual 纯历法日期格式化
WebviewCookieToStringwebviewCookieToString(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.kt5 个 C ABI、scriptRunner 捕获、MiniJson、Greet 处理器
harmonyApp/.../cpp/napi_init.cppN-API 双签名导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkTS 三 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() / getCookieExpirationDateSet-Cookie 拼接 / GMT 日期格式化
入口Webview.jsBridgeInitScript()与 IWebView.injectJsBridge 逐字相同的注入脚本
NativeWebviewDispatchMessage 等 × 5C ABI JSON 导出
N-APIwebviewDispatchMessage(req) 等向 ArkTS 暴露能力
ArkTSonNativeMessage(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.so2,379,632 B(约 2.3 MB)
arm64-v8a/libc++_shared.so1,850,184 B
x86_64/libohoswebview.so2,014,016 B(约 2.0 MB)
x86_64/libc++_shared.so1,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 功能用例

  1. 启动 Engine Tab 即出现 engine 2.0.4-ohos.1 (upstream 2.0.4)(真实 Native 握手,非静态文字);
  2. WebView Tab 默认加载 example.com,进度条随 onProgressChange 走到 100,Back/Forward/Reload 可用;
  3. JsBridge Tab 加载本地 greet.html,onPageEnd 后状态条显示 bridge injected (kmpJsBridge ready);
  4. 输入名字点 Call Native Greet,页面日志出现 JS <- onCallback: {"message":"KMP Received Hello"},Native 日志出现 KN< {"ok":true,"callbackId":N,"script":"window.kmpJsBridge.onCallback(N, '…')"},状态条变 handled, callbackId=N;
  5. 点 Call Unknown Method,页面无回调(预期),Native 侧 ok:false, no handler for 'Nope',HAP 不崩;
  6. 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :webview 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Windows 下 gradlew 启动器读不到 gradle.properties 里的 org.gradle.java.home,必须另设 JAVA_HOME 环境变量,否则报 JAVA_HOME is not set;
  4. @CName 无参与带参入口的 C 函数指针签名不同(const char*(void) vs const char*(*)(const char*)),C++ 包装必须两个重载,硬塞一个签名直接 no matching function for call 编译失败;
  5. ArkTS 严格模式三连:Tabs 底部栏是 BarPosition.End(没有 Bottom);Web 事件名是 onPageBegin(不是 onPageStart);事件回调参数必须显式类型((event: OnProgressChangeEvent)),否则 arkts-no-any-unknown 拒绝编译;
  6. javaScriptProxy 的回调不在 UI 线程,runJavaScript 与 @State 更新都要先 setTimeout(…, 0) 转回主线程;
  7. 页面就绪日志早于 onPageEnd 注入属预期时序,别把 kmpJsBridge = undefined 误判为注入失败——以状态条与按钮回路为准;
  8. entry/build-profile.json5 的 abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可,缺 so 安装直接报 9568347;
  9. KN 返回字符串必须经 libohoswebview_symbols()->DisposeString 归还 Kotlin 运行时,N-API 层不能 free();
  10. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组,勾选 DevEco 自动签名后由 IDE 回填,提交前 git update-index --skip-worktree 保护,证书绝不入库;
  11. 根锚定 gitignore 盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、entry/build/、entry/libs/ 要单独追加 gitignore;
  12. 改了 Kotlin 源码要重跑 linkDebugShared* 再打 HAP,CMake 只认 entry/libs 里的文件时间戳。

8.2 已知问题

  • WebView() composable、WebViewState / WebViewNavigator(Compose 状态与协程)、WebSettings / PlatformWebSettings 未进引擎,设置项在 ArkTS Web 组件属性上直接配置;
  • RequestInterceptor 接口(带 navigator 参数)随 UI 留盘,WebRequest / WebRequestInterceptResult 模型在引擎里但拦截执行依赖 ArkTS onInterceptorRequest,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.ui ohos 制品与上游 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 三条经验

  1. 适配路线按"库的耦合形态"选:KStore 的 commonMain 零 UI 耦合,正确答案是真编译整库;本库 UI 与协议混在 commonMain,正确答案是拆引擎——跨端库的复用主体是契约(这里是 window.kmpJsBridge 协议与消息模型),路线选择围绕"契约能不能原样过桥"展开;
  2. 零依赖引擎让工具链选择自由:引擎剥干净后不需要 kotlinx ohos 制品线,就能留在缓存最全、社区验证最多的 2.2.21 线上——依赖越少,适配的自由度越大,这条优先级应该排在"保留注解便利性"之前;
  3. “页面能感知的行为逐字一致、宿主感知的行为原生承载"是可执行的验收口径:注入脚本逐字提取、回包是引擎产出的 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。

参考文档

Logo

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

更多推荐