用 Kodvent 把并查集放进鸿蒙界面
1. 连线、分组和数组
假设有六个节点,编号从零到五。零号与一号连起来,一号再连二号,三号与四号也建立联系。界面不只需要画出三条线,还需要回答:现在有几个组,零号与二号是否属于同一组,五号是不是仍然独立。如果每次都从头遍历全部关系,小例子还好,逻辑很快就会变得不好维护。
并查集适合回答这类连通关系问题。Kodvent 提供了 DisjointSetUnion,同时还有线段树、二分边界和一些数学工具。这次接入鸿蒙,先把六个节点排成一圈,用不同颜色显示当前分组;切到另一个模式,则改成数组条带与区间查询面板。两个模式都调用原库,不在页面里重新写数据结构。
文章按操作过程展开。先连线,再重复连线,随后看隔离操作为什么需要保留其余成员的关系。把返回值和当前状态一起看,会比背下一句复杂度更容易理解它在应用里怎样使用。
2. 从六个单独节点开始连线
创建 DisjointSetUnion(6) 后,每个节点都是自己的代表,连通组数为六。界面的第一条结果就是 0 | 1 | 2 | 3 | 4 | 5。这里的竖线分隔不同组,逗号表示同一组内的成员;代表节点只用来分组,没有把它当成业务上的组名称。
输入 0-1,1-2,3-4 后,代码逐条执行 union。第一条将组数降为五,第二条降为四,第三条降为三。最终能看到零、一、二在一起,三、四在一起,五单独一组。页面还单列“零与五是否连通”,把容易只看总数而忽略具体成员的问题补上。
组数能够直接从 count 读取,成员列表则通过 find 的结果归组。这个区别挺关键:库维护的是等价关系,不是一个持续保存的“组列表”。Kotlin 桥接为了显示六个节点,临时遍历编号并按代表归类;这是展示数据的整理,并没有取代并查集内部的路径压缩或按秩合并。
界面从这条“当前分组”结果读取成员列表,同一组使用相同颜色,图下方还保留带编号的分组标签。图上的线表示组内关系,不能当成原始连线顺序的回放;即使成员只是通过中间节点间接连通,也会显示为同色。旁边的逐条合并记录用于查看输入顺序,这样图形和操作记录各自说明一件事。
点“连成一组”会换成一条贯穿六个节点的链。五条有效连线把初始六组变成一组,最后的连通查询也随之变化。示例每次重新创建并查集,所以切换样例不会把前一个输入的连线偷偷累积进来。需要长期维护关系的业务,可以将实例放到生命周期合适的位置。
3. 重复连线为什么没有多减一组
再输入 0-1,1-0,0-1。三行操作都会显示,但只有第一条真正合并。后面两条的节点已经连通,union 返回 false,总组数仍是五。这个返回值表示“是否新合并了两个组”,不能理解成“这次调用有没有报错”。重复关系是合法操作,结果只是没有改变分组。
这样设计很适合处理重复出现的关联信息。比如不同数据来源都报告了同一对成员,应用可以继续调用合并,不必先自行维护一个去重集合。要统计实际新增了多少次连接,则需要累计 true 的次数,而不是累计输入里有多少条连线。
输入边界也保持在库的规则内。编号只允许零到五,查找负数节点会抛出越界异常。桥接将异常转换为页面提示,不会把非法编号钳制成零号。静默修正输入看似让界面总有结果,却可能让关系图把一个不存在的成员合并到真实成员中,排查时反而更难看出来。
4. isolate 的意义是拿走一个成员
合并之外,这个版本还提供 isolate(x)。它将某个成员从原组拿出来,放回独立集合,但原组中的其他成员仍然保持连通。对已经连成零、一、二的集合执行 isolate(1),结果应是零与二仍属于同一组,一独立出来,组数增加一。
这与删除一条关系边不是同一件事。并查集没有记录所有原始连线,也不能据此还原一号曾经作为中间连接点的图。例子把隔离放在设备检查里,分别验证组数增加和其余成员仍连通;两个条件一起成立,才对应这个 API 承诺的行为。界面演示的连线按钮本身没有提供一般图的删边功能。
源码中的隔离操作需要整理组内成员,因此复杂度也与普通合并不同。这里不把上游的复杂度描述写成设备计时结论。六个节点的交互主要用于看清行为,如果实际应用要频繁隔离大量成员,就应该针对数据规模和调用次数另外测量。
5. 真机和模拟器上的操作效果
运行演示先切换两组连线、全部连通和重复连线,圆周上的节点随原库返回的分组改变颜色,侧边操作记录同时更新。再切到区间求和,节点图换成数组条带,填入一到四后读取三块数值面板。这样能连续看到三种分组状态,以及单点更新前后的数值变化。截图上的框和箭头标出右上方的模式选择与设备检查入口,代码截图对应实际调用位置。



运行环境为 API 24 的华为电脑真机(电脑模式、ARM64)与 x86_64 模拟器。两端安装本次编译的签名应用,完成页面操作和 14 项原库检查,均返回 14/14。下面保留连续操作过程。
十四项检查覆盖初始化、合并返回值、链式连通、跨组查询、非法节点、隔离、单元素与全区间查询、单点更新、逆序区间、越界和空树。这里只验证本例使用的数据结构行为,不把少量样例的响应速度当成吞吐测试,也没有把整库所有工具列为已经实测。
6. 换一组数组,检查闭区间与单点更新
第二个模式使用 SegmentTree,数字固定为二、四、六、八、十、十二。输入 1,4 查询索引一到四的和,范围包含两端,所以原结果是二十八。桥接随后把索引二的值由六改成二十,同一区间再次查询得到四十二,全数组和则由四十二变为五十六。
数组视图将六个位置画成条带,索引二单独用橙色标出“六到二十”的更新。下方将原区间和、更新后区间和与全数组和并排显示。若误把终点当成不包含的右边界,第一次求和就会少算十;若更新错了位置,后面的全数组和和目标区间和又可能出现不同变化。示例没有让页面自己累加数字,条带只是展示固定数组,两个查询都来自原线段树。
创建线段树时,还要指定合并操作。本例使用整数加法,它满足结合律,适合将区间拆成子区间再合并。换成最小值、最大值也有对应场景;随便放一个不满足结合律的操作,结果可能跟拆分方式有关。数据结构负责组织查询,不会替调用者保证任意运算都符合要求。
越界时有两类接口。普通查询会抛异常,getOrNull 可以返回空值。页面特意显示一次安全越界查询,让两种处理路径有可见区别。起点大于终点的输入同样不会被交换修正。业务若允许用户反向拖选区间,可以在输入层明确规范化,而不应误认为库会替自己完成。
7. 接入代码与返回值检查选项
下面这段出自桥接的并查集检查,验证的正是重复合并和隔离后剩余成员的状态。检查返回值是布尔表达式;页面的“设备检查”按钮会把每一项名称与结果列出。
下面是 harmony/src/Bridge.kt 的完整内容。分组文本从 DisjointSetUnion 的 find 结果生成,数组模式则调用 SegmentTree。下面连同两种模式、错误处理和十四项检查一起给出。这样重复合并、隔离成员与闭区间求和都有对应的实际调用,而不是只留两条孤立断言。
package harmony
import kodvent.datastructures.DisjointSetUnion
import kodvent.datastructures.SegmentTree
private fun groups(q:DisjointSetUnion)= (0..5).groupBy{q.find(it)}.values.joinToString(" | "){it.joinToString(",")}
private fun linked()=DisjointSetUnion(6).apply{union(0,1);union(1,2);union(3,4)}
fun evaluate(input:String,mode:Int):String=try {
if(mode==0){
val q=DisjointSetUnion(6);val rows=mutableListOf(row("初始分组",groups(q)))
input.split(',',';','\n').map{it.trim()}.filter{it.isNotEmpty()}.forEachIndexed{i,s->
val v=s.split('-');require(v.size==2){"连线格式为 0-1,1-2"};val a=v[0].trim().toInt();val b=v[1].trim().toInt()
val merged=q.union(a,b);rows+=row("连线 ${i+1} · $a-$b",if(merged) "合并成功 · ${q.count} 组" else "已连接 · 不重复合并")
}
rows+=row("当前分组",groups(q));rows+=row("0 与 5 是否连通",q.connected(0,5).toString());rows+=row("连通组数",q.count.toString(),q.count/6.0)
result("六个节点 · ${q.count} 个连通组",rows)
}else{
val values=listOf(2,4,6,8,10,12);val tree=SegmentTree(values,Int::plus)
val v=input.split(',').map{it.trim().toInt()};require(v.size==2){"区间格式为起点,终点"}
val before=tree[v[0],v[1]];tree[2]=20
result("闭区间 [${v[0]},${v[1]}] · 单点更新",listOf(row("原始数组",values.joinToString(", ")),row("原区间和",before.toString()),row("更新动作","索引 2:6 → 20"),row("更新后区间和",tree[v[0],v[1]].toString()),row("全数组和",tree[0,5].toString()),row("安全查询越界",tree.getOrNull(-1,6).toString())))
}
}catch(e:Exception){result("输入不满足条件",listOf(row("原因",e.message?:"请检查节点或区间")),false)}
fun runChecks():String=checks(listOf(
"初始六个连通组" to {DisjointSetUnion(6).count==6},
"第一次合并返回真" to {DisjointSetUnion(2).union(0,1)},
"重复合并返回假" to {val q=DisjointSetUnion(2);q.union(0,1);!q.union(1,0)&&q.count==1},
"链式连通" to {linked().connected(0,2)},
"不同分组不连通" to {!linked().connected(0,3)},
"负节点拒绝" to {try{linked().find(-1);false}catch(_:IndexOutOfBoundsException){true}},
"隔离后组数增加" to {val q=linked();val old=q.count;q.isolate(1);q.count==old+1&&!q.connected(0,1)},
"隔离保留剩余连通" to {val q=linked();q.isolate(1);q.connected(0,2)},
"全区间求和" to {SegmentTree(listOf(2,4,6,8),Int::plus)[0,3]==20},
"单元素查询" to {SegmentTree(listOf(2,4,6,8),Int::plus)[2]==6},
"单点更新改变区间" to {val q=SegmentTree(listOf(2,4,6,8),Int::plus);q[2]=20;q[1,3]==32},
"逆序区间拒绝" to {try{SegmentTree(listOf(1,2),Int::plus)[1,0];false}catch(_:IllegalArgumentException){true}},
"安全越界返回空" to {SegmentTree(listOf(1,2),Int::plus).getOrNull(-1,2)==null},
"空树查询拒绝" to {try{SegmentTree(emptyList<Int>(),Int::plus)[0,0];false}catch(_:IllegalArgumentException){true}}
))
private fun quote(s: String): String = "\"" + buildString {
s.forEach { c -> when(c) { '"' -> append("\\\""); '\\' -> append("\\\\"); '\n' -> append("\\n"); '\r' -> append("\\r"); '\t' -> append("\\t"); else -> if(c.code < 32) append("\\u"+c.code.toString(16).padStart(4,'0')) else append(c) } }
} + "\""
private fun row(label: String, value: String, metric: Double = 0.0): String = "{\"label\":"+quote(label)+",\"value\":"+quote(value)+",\"metric\":"+(if(metric.isFinite()) metric else 0.0)+"}"
private fun result(summary: String, rows: List<String>, ok: Boolean = true): String = "{\"ok\":$ok,\"summary\":"+quote(summary)+",\"rows\":"+rows.joinToString(",","[","]")+"}"
private fun checks(cases: List<Pair<String, () -> Boolean>>): String {
var passed = 0
val details = cases.joinToString("\n") { (name,test) ->
try { if(test()) { passed++; "PASS $name" } else "FAIL $name" }
catch(e: Throwable) { "FAIL $name: ${e.message}" }
}
return "{\"passed\":$passed,\"total\":${cases.size},\"detail\":"+quote(details)+"}"
}

构建环境按 CPF-KMP-CMP 快速开始 配置即可。适配新增 harmony/native-config.json、harmony/build-native.py 和 harmony/src/Bridge.kt,直接编译原仓库的共享源码,没有外部运行依赖。鸿蒙示例则放到原仓库的 example/harmony 下。
首次编译遇到的具体问题来自 IgnorableReturnValue。上游使用较新的 Kotlin 版本,并在构建配置中开启返回值检查;官方鸿蒙编译器中,若保持默认关闭状态,使用这个注解会报错。适配沿用上游的 -Xreturn-value-checker=full 选项后完成编译,没有删除注解,也没有改动公共源码。
需要区分的是,本次使用官方 Kotlin/Native 命令行编译生成两个架构产物,不等于已经跑完上游的 Gradle 发布任务。原工程还包含文档、签名和发布插件,以及更高版本的工具环境要求,它们没有被混进这个算法接入例子的验证范围。
把接口定义和应用配置放到一起看,库到页面的调用次序才完整。下表中 entry 路径均位于 example/harmony 中;生成的 C 头文件由原生构建脚本输出,不需要手工猜测导出结构。
| 文件 | 在调用链中的职责 |
|
| 原库调用、返回编码与设备检查 |
|
| 共享源集、双 ABI 依赖和编译选项 |
|
| 编译 KLIB、链接动态库并生成匹配头文件 |
|
| 参数检查、原生调用与返回值生命周期 |
|
| 按 ABI 选择动态库和头文件 |
|
| ArkTS 能看到的接口类型 |
|
| 完整输入、运行反馈与独立界面 |
7.1 分组和区间结果共用的调用桥
N-API 先核对两个参数,再按 UTF-8 字节数分配缓冲区。Kotlin 返回的字符串先复制到 ArkTS 值中,随后调用 DisposeString 释放。下面给出整个文件,包含模块注册;只有 Evaluate 函数而没有 Init 和 Register,页面就无法从 libentry.so 取得接口。
完整文件:example/harmony/entry/src/main/cpp/napi_init.cpp。
#include "napi/native_api.h"
#include "kodvent_api.h"
#include <string>
static napi_value Copy(napi_env env,const char* text) {
napi_value value;
napi_create_string_utf8(env,text?text:"{}",NAPI_AUTO_LENGTH,&value);
if(text) kodvent_symbols()->DisposeString(text);
return value;
}
static napi_value Evaluate(napi_env env,napi_callback_info info) {
size_t count=2;napi_value args[2];napi_get_cb_info(env,info,&count,args,nullptr,nullptr);
size_t length=0;int32_t mode=0;
if(count!=2 || napi_get_value_string_utf8(env,args[0],nullptr,0,&length)!=napi_ok || napi_get_value_int32(env,args[1],&mode)!=napi_ok) {
napi_throw_type_error(env,nullptr,"evaluate requires text and a mode");return nullptr;
}
if(length>16384){napi_throw_range_error(env,nullptr,"Input exceeds demo limit");return nullptr;}
std::string text(length+1,'\0');napi_get_value_string_utf8(env,args[0],text.data(),text.size(),&length);text.resize(length);
return Copy(env,kodvent_symbols()->kotlin.root.harmony.evaluate(text.c_str(),mode));
}
static napi_value Check(napi_env env,napi_callback_info info){return Copy(env,kodvent_symbols()->kotlin.root.harmony.runChecks());}
static napi_value Init(napi_env env,napi_value exports){
napi_property_descriptor properties[]={
{"evaluate",nullptr,Evaluate,nullptr,nullptr,nullptr,napi_default,nullptr},
{"runChecks",nullptr,Check,nullptr,nullptr,nullptr,napi_default,nullptr}};
napi_define_properties(env,exports,2,properties);return exports;
}
static napi_module module={1,0,nullptr,Init,"entry",nullptr,{0}};
extern "C" __attribute__((constructor)) void Register(){napi_module_register(&module);}
7.2 源集、依赖和双架构链接配置
共享源码位置是 src/commonMain/kotlin。构建配置把 ARM64 与 X64 分开;每个目标当前需要 0 份显式 KLIB 依赖。没有外部依赖不代表可以使用桌面编译器,目标仍须是 OHOS。
harmony/native-config.json 完整内容:
{
"common": "src/commonMain/kotlin",
"code": "kodvent",
"dependencies": {
"ohos_arm64": [],
"ohos_x64": []
},
"compilerOptions": [
"-Xreturn-value-checker=full"
]
}
entry/src/main/cpp/CMakeLists.txt 把生成头文件与同 ABI 动态库配对,链接名称保持为 entry,对应页面的 libentry.so 导入:
cmake_minimum_required(VERSION 3.5.0)
project(harmony_library_example)
set(CMAKE_CXX_STANDARD 17)
if(CMAKE_OHOS_ARCH_ABI STREQUAL "x86_64" OR OHOS_ARCH STREQUAL "x86_64")
set(KMP_ABI x86_64)
else()
set(KMP_ABI arm64-v8a)
endif()
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/${KMP_ABI})
add_library(kmp_library SHARED IMPORTED)
set_target_properties(kmp_library PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${KMP_ABI}/libkodvent.so)
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC kmp_library libace_napi.z.so)
应用还需要启用外部 Native 构建,并声明本地接口类型依赖。下面依次是完整的 entry/build-profile.json5 与 entry/oh-package.json5,不包含个人签名:
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
},
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"abiFilters": [
"arm64-v8a",
"x86_64"
]
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
}
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest"
}
]
}
{
"name": "entry",
"version": "1.0.0",
"description": "一次连线改变哪些分组?一次更新改变哪个区间?",
"main": "",
"author": "",
"license": "",
"dependencies": {
"libentry.so": "file:./src/main/cpp/types/libentry"
}
}
7.3 分组图和区间条带的完整页面
分组图解析的是原库返回的当前组,而非重新计算连通性;数组条带只帮助观察区间,求和仍由原生线段树完成。完整文件保留两种视图、预设操作、图形绘制和检查反馈,避免只复制 Canvas 后缺少驱动它的数据。
先给出 entry/src/main/cpp/types/libentry/index.d.ts,紧接着是完整的 entry/src/main/ets/pages/Index.ets。类型声明与下面的实际调用保持一致。
export const evaluate: (input: string, mode: number) => string;
export const runChecks: () => string;
import native from 'libentry.so';
interface ResultRow { label: string; value: string; metric: number; }
interface Result { ok: boolean; summary: string; rows: ResultRow[]; }
interface Checks { passed: number; total: number; detail: string; }
@Entry
@Component
struct Index {
@State input: string = '0-1,1-2,3-4';
@State mode: number = 0;
@State view: Result = { ok: true, summary: '', rows: [] };
@State checkStatus: string = '等待数据结构检查';
@State details: string = '';
private samples: string[] = ['0-1,1-2,3-4', '0-1,1-2,2-3,3-4,4-5', '1,4', '0-1,1-0,0-1'];
private sampleLabels: string[] = ['两组连线', '连成一组', '区间 1 到 4', '重复连线'];
private nodeColors: string[] = ['#6553BC', '#D9903C', '#3E9A94', '#D55D85', '#597FAB', '#7F8D48'];
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private canvasWidth: number = 620;
private canvasHeight: number = 300;
private values: number[] = [2, 4, 6, 8, 10, 12];
aboutToAppear(): void { this.refresh(); }
private refresh(): void {
this.view = JSON.parse(native.evaluate(this.input, this.mode)) as Result;
this.paintGroups();
}
private rowValue(label: string): string {
for (const item of this.view.rows) {
if (item.label === label) return item.value;
}
return '—';
}
private chooseSample(index: number): void {
this.input = this.samples[index];
this.refresh();
}
private groupTexts(): string[] {
const value = this.rowValue('当前分组');
return value === '—' ? [] : value.split(' | ');
}
private pointX(index: number): number {
return this.canvasWidth / 2 + Math.cos((index - 1) * Math.PI / 3) * Math.min(this.canvasWidth / 3, 190);
}
private pointY(index: number): number {
return this.canvasHeight / 2 + Math.sin((index - 1) * Math.PI / 3) * 100;
}
private paintGroups(): void {
const c = this.ctx;
c.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
c.fillStyle = '#FFFFFF'; c.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
const groups = this.groupTexts();
groups.forEach((group: string, groupIndex: number) => {
const members = group.split(',').map((s: string) => Number(s));
c.strokeStyle = this.nodeColors[groupIndex % this.nodeColors.length];
c.lineWidth = 3;
for (let i = 1; i < members.length; i++) {
c.beginPath(); c.moveTo(this.pointX(members[0]), this.pointY(members[0]));
c.lineTo(this.pointX(members[i]), this.pointY(members[i])); c.stroke();
}
});
for (let n = 0; n < 6; n++) {
let color = '#D7D2E2';
groups.forEach((group: string, i: number) => {
if (group.split(',').includes(n.toString())) color = this.nodeColors[i % this.nodeColors.length];
});
c.beginPath(); c.arc(this.pointX(n), this.pointY(n), 25, 0, Math.PI * 2);
c.fillStyle = color; c.fill();
c.lineWidth = 4; c.strokeStyle = '#FFFFFF'; c.stroke();
c.font = 'bold 19px sans-serif'; c.fillStyle = '#FFFFFF';
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(n.toString(), this.pointX(n), this.pointY(n));
}
}
@Builder
sampleButton(index: number) {
Button(this.sampleLabels[index]).id(`sample-${index}`).type(ButtonType.Normal)
.fontSize(13).backgroundColor('#F0ECFA').fontColor('#6553A2').borderRadius(5)
.layoutWeight(1).height(37).onClick(() => { this.chooseSample(index); })
}
@Builder
valuePanel(label: string) {
Column({ space: 8 }) {
Text(label).fontSize(12).fontColor('#8A839B')
Text(this.rowValue(label)).fontSize(30).fontFamily('monospace').fontColor('#4C397E').fontWeight(FontWeight.Medium)
}.padding(18).layoutWeight(1).alignItems(HorizontalAlign.Start).backgroundColor('#F5F1FB').borderRadius(8)
}
build() {
Scroll() {
Column({ space: 20 }) {
Row({ space: 14 }) {
Text('06').fontSize(42).fontFamily('monospace').fontWeight(FontWeight.Bold).fontColor('#7561B7')
Column({ space: 5 }) {
Text('节点与区间').fontSize(29).fontWeight(FontWeight.Bold).fontColor('#332548')
Text('Kodvent · 看见数据关系的变化').fontSize(14).fontColor('#8A7C9C')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Row({ space: 4 }) {
Button('连通分组').id('mode-0').fontSize(14)
.backgroundColor(this.mode === 0 ? '#6B51A2' : '#EBE5F4').fontColor(this.mode === 0 ? '#FFFFFF' : '#705A8C')
.onClick(() => { this.mode = 0; this.refresh(); })
Button('区间求和').id('mode-1').fontSize(14)
.backgroundColor(this.mode === 1 ? '#6B51A2' : '#EBE5F4').fontColor(this.mode === 1 ? '#FFFFFF' : '#705A8C')
.onClick(() => { this.mode = 1; this.refresh(); })
}.padding(5).backgroundColor('#F2EDF8').borderRadius(22)
}.width('100%')
Row({ space: 24 }) {
Column({ space: 12 }) {
if (this.mode === 0) {
Text('同色节点属于同一组').fontSize(16).fontWeight(FontWeight.Medium).fontColor('#4B3B67').width('100%')
Canvas(this.ctx).id('network-canvas').width('100%').height(300).onReady(() => { this.paintGroups(); })
.onAreaChange((oldArea: Area, newArea: Area) => {
this.canvasWidth = Number(newArea.width); this.canvasHeight = Number(newArea.height); this.paintGroups();
})
Text('连线表示分组关系,不是输入边的绘制顺序').fontSize(11).fontColor('#9A90AA').width('100%')
Row({ space: 8 }) {
ForEach(this.groupTexts(), (group: string, index: number) => {
Text(`组 ${index + 1} ${group}`).fontSize(13).fontColor(this.nodeColors[index % this.nodeColors.length])
.padding({ left: 12, right: 12, top: 9, bottom: 9 }).backgroundColor('#F5F2F9').borderRadius(5)
})
}.width('100%')
} else {
Text('一份数组,两次区间查询').fontSize(17).fontWeight(FontWeight.Medium).fontColor('#4B3B67').width('100%')
Row({ space: 10 }) {
ForEach(this.values, (value: number, index: number) => {
Column({ space: 10 }) {
Text(index === 2 ? '6 → 20' : value.toString()).fontSize(17).fontWeight(FontWeight.Medium)
.fontColor(index === 2 ? '#C16E24' : '#7155A4')
Column().width('100%').height(50 + value * 6).backgroundColor(index === 2 ? '#F5D3A8' : '#DCD1EF').borderRadius(5)
Text(`索引 ${index}`).fontSize(11).fontColor('#9689A6')
}.layoutWeight(1).height(215).justifyContent(FlexAlign.End)
})
}.width('100%').padding({ top: 24, bottom: 20 })
Text('索引 2 单点更新:6 改为 20').fontSize(13).fontColor('#AF6928').padding(12)
.width('100%').backgroundColor('#FFF3E3').borderRadius(5)
Row({ space: 12 }) {
this.valuePanel('原区间和')
this.valuePanel('更新后区间和')
this.valuePanel('全数组和')
}.width('100%')
}
}.layoutWeight(1).padding(24).backgroundColor('#FFFFFF').borderRadius(14)
Column({ space: 15 }) {
Text(this.mode === 0 ? '输入连线' : '输入闭区间').fontSize(17).fontColor('#4E3A70').fontWeight(FontWeight.Medium).width('100%')
Text(this.mode === 0 ? '节点 0—5,用逗号分开每条连线' : '起点,终点;左右端点均包含').fontSize(12).fontColor('#9183A2').width('100%')
TextInput({ text: this.input, placeholder: this.mode === 0 ? '0-1,1-2' : '1,4' }).id('source-input')
.maxLength(4096).fontSize(15).width('100%').backgroundColor('#F2EDF8')
.onChange((text: string) => { this.input = text; this.refresh(); })
Row({ space: 7 }) { this.sampleButton(0); this.sampleButton(1); }.width('100%')
Row({ space: 7 }) { this.sampleButton(2); this.sampleButton(3); }.width('100%')
Divider().color('#E9E2F2')
Text(this.view.summary).id('summary').fontSize(19).fontColor(this.view.ok ? '#61488C' : '#BE4D51')
.fontWeight(FontWeight.Medium).width('100%')
if (this.mode === 0 && this.view.ok) {
Text(`0 与 5 连通:${this.rowValue('0 与 5 是否连通')}`).fontSize(14).fontColor('#7B6993').width('100%')
}
ForEach(this.view.rows, (item: ResultRow, index: number) => {
if (this.mode === 1 || item.label.startsWith('连线') || !this.view.ok) {
Column({ space: 4 }) {
Text(item.label).fontSize(12).fontColor('#9B8EAD').width('100%')
Text(item.value).id(`result-${index}`).fontSize(14).fontColor('#58466F').width('100%').lineHeight(21)
}.width('100%').padding({ top: 5, bottom: 7 })
}
})
}.width(320).padding(23).backgroundColor('#FFFFFF').borderRadius(14)
}.width('100%').alignItems(VerticalAlign.Top)
Row({ space: 15 }) {
Column({ space: 4 }) {
Text('边界条件也要检查').fontSize(16).fontColor('#51406B').fontWeight(FontWeight.Medium)
Text(this.checkStatus).id('check-status').fontSize(13).fontColor('#91819F')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Button('运行 14 项检查').id('run-checks').fontSize(14).backgroundColor('#6B51A2').onClick(() => {
const checks = JSON.parse(native.runChecks()) as Checks;
this.checkStatus = `设备检查 ${checks.passed}/${checks.total}`; this.details = checks.detail;
})
}.width('100%').padding({ top: 6, bottom: 8 })
if (this.details.length > 0) {
Text(this.details).id('check-details').fontSize(12).fontFamily('monospace').lineHeight(23).fontColor('#705D87')
.padding(20).width('100%').backgroundColor('#FFFFFF').borderRadius(10)
}
}.padding(30).width('100%').constraintSize({ maxWidth: 1260 }).alignItems(HorizontalAlign.Start)
}.width('100%').height('100%').backgroundColor('#F2EDF7')
}
}
7.4 从源码生成与页面一致的应用
按前文官方环境说明配置 JDK 17 与 OHOS Kotlin/Native 编译器,把 JAVA_HOME、KOTLIN_NATIVE_HOME 指向对应安装目录。在当前库目录运行下面这份完整脚本;它读取上面的配置,分别生成两个 ABI 的库和头文件,再复制进示例。脚本将公共源码和平台实现编译到明确的 OHOS 目标,并保留每个阶段的构建日志。
完整文件:harmony/build-native.py。
#!/usr/bin/env python3
"""Compile preserved common sources and the OHOS example bridge."""
from pathlib import Path
import argparse,os,json,subprocess,shutil,hashlib
ROOT=Path(__file__).resolve().parent.parent
CONFIG=json.loads((ROOT/'harmony/native-config.json').read_text(encoding='utf8'))
p=argparse.ArgumentParser();p.add_argument('--compiler',default=os.environ.get('KOTLIN_NATIVE_HOME'));p.add_argument('--sdk-native',default=os.environ.get('OHOS_NATIVE_HOME'));p.add_argument('--install');p.add_argument('--check-inputs',action='store_true');args=p.parse_args()
common=sorted((ROOT/CONFIG['common']).rglob('*.kt'));actual=sorted((ROOT/'harmony/src').rglob('*.kt'))
if CONFIG.get('nativeActual'):actual+=sorted((ROOT/CONFIG['nativeActual']).rglob('*.kt'))
extra=sorted((ROOT/'harmony/dependencies/src').rglob('*.kt')) if CONFIG.get('sourceDependency') else []
assert common and actual
if args.check_inputs:
proof=json.loads((ROOT/'harmony/verification.json').read_text(encoding='utf8'))
for path,digest in proof['native']['coreSourceHashes'].items():assert hashlib.sha256((ROOT/path).read_bytes()).hexdigest()==digest
for target,abi in [('ohos_arm64','arm64-v8a'),('ohos_x64','x86_64')]:
for name in CONFIG['dependencies'][target]:assert (ROOT/'harmony/dependencies'/name).is_file(),name
assert hashlib.sha256((ROOT/'example/harmony/entry/libs'/abi/('lib'+CONFIG['code']+'.so')).read_bytes()).hexdigest()==proof['native']['outputs'][target]
print('Shared source, dependencies and both compiled libraries match verification records');raise SystemExit(0)
if not args.compiler:p.error('Set KOTLIN_NATIVE_HOME or pass --compiler')
compiler=Path(args.compiler).resolve();env=dict(os.environ)
def run(target,phase,argv):
dest=ROOT/'harmony/build'/target;dest.mkdir(parents=True,exist_ok=True)
param=dest/(phase+'.args');param.write_text('\n'.join('"'+x.replace('\\','/')+'"' for x in argv),encoding='utf8')
with (dest/(phase+'.log')).open('w',encoding='utf8') as log:q=subprocess.run(['cmd.exe','/d','/c',str(compiler/'bin/konanc.bat'),'@'+str(param)],env=env,stdout=log,stderr=subprocess.STDOUT)
if q.returncode:raise SystemExit(f'{target} {phase} failed; inspect {phase}.log')
print(target,phase,'passed',flush=True)
outputs={}
for target,abi in [('ohos_arm64','arm64-v8a'),('ohos_x64','x86_64')]:
dest=ROOT/'harmony/build'/target;dest.mkdir(parents=True,exist_ok=True);name=CONFIG['code'];libs=[]
for file in CONFIG['dependencies'][target]:libs+=['-library',str(ROOT/'harmony/dependencies'/file)]
if CONFIG.get('utf8proc'):
if not args.sdk_native:p.error('Unicode backend requires --sdk-native or OHOS_NATIVE_HOME')
from build_utf8proc import prepare
libs+=['-library',str(prepare(ROOT,compiler,env,target,Path(args.sdk_native).resolve()))]
opts=['-target',target,'-produce','library','-Xmulti-platform','-Xexpect-actual-classes','-Xcommon-sources='+','.join(map(str,common+extra)),'-opt-in=kotlin.ExperimentalUnsignedTypes']+CONFIG.get('compilerOptions',[])+libs
plugin=['-Xplugin='+str(ROOT/'harmony/dependencies/serialization-plugin.jar')] if CONFIG.get('serialization') else []
if CONFIG.get('splitCore'):
core=['-library',str(dest/(name+'_core.klib'))]
run(target,'compile-core',list(map(str,common+extra+[a for a in actual if a.name!='Bridge.kt']))+opts+plugin+['-o',str(dest/(name+'_core'))])
run(target,'compile-bridge',[str(ROOT/'harmony/src/Bridge.kt'),'-target',target,'-produce','library','-o',str(dest/name)]+plugin+core+libs)
libs+=core
else:run(target,'compile',list(map(str,common+extra+actual))+opts+plugin+['-o',str(dest/name)])
run(target,'link',['-target',target,'-produce','dynamic','-o',str(dest/name),'-Xinclude='+str(dest/(name+'.klib'))]+libs)
outputs[target]=hashlib.sha256((dest/('lib'+name+'.so')).read_bytes()).hexdigest()
if args.install:
project=Path(args.install).resolve();assert (project/'entry/build-profile.json5').is_file()
for source,to in [(dest/('lib'+name+'.so'),project/'entry/libs'/abi/('lib'+name+'.so')),(dest/(name+'_api.h'),project/'entry/src/main/cpp/include'/abi/(name+'_api.h'))]:
to.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(source,to)
(ROOT/'harmony/build/manifest.json').write_text(json.dumps({'sourceFileCount':len(common),'outputs':outputs},indent=2),encoding='utf8')
python harmony/build-native.py --install example/harmony
完成原生构建后,用 DevEco Studio 打开 example/harmony,同步 entry 的类型依赖,为当前应用包名配置自己的开发者签名,再构建和安装 HAP。仅修改 ArkTS 可以重建应用;改过 Kotlin 或平台 actual 时,先重编两个 ABI 的动态库及匹配头文件。
本文的运行记录对应 API 24、OHOS Kotlin/Native 2.2.21-1.1.0,覆盖前面列出的输入与检查。API 26 和其他目标平台没有在这次记录中实测。更多通用三方库资料见 oh-tpc 社区。
8. 总结:保留数据结构的边界条件
把 Kodvent 接进鸿蒙以后,界面可以直接观察关系合并与区间更新,而算法仍保留在原共享代码里。最容易记住的三个点是:重复合并不会继续减少组数,隔离成员会保留其他成员的连通关系,线段树查询的终点也算在范围内。
这类库往往没有很复杂的平台依赖,适配工作仍然需要对齐编译选项、桥接边界和输入规则。示例与说明可见 Kodvent 适配目录,官方跨平台资料集中在 CPF-KMP-CMP。
并查集与区间示例可以从 总源码索引 找到,相关应用共建项目见 CPF-ApplicationTPC。
更多推荐



所有评论(0)