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 头文件由原生构建脚本输出,不需要手工猜测导出结构。

文件

在调用链中的职责

harmony/src/Bridge.kt

原库调用、返回编码与设备检查

harmony/native-config.json

共享源集、双 ABI 依赖和编译选项

harmony/build-native.py

编译 KLIB、链接动态库并生成匹配头文件

entry/src/main/cpp/napi_init.cpp

参数检查、原生调用与返回值生命周期

entry/src/main/cpp/CMakeLists.txt

按 ABI 选择动态库和头文件

entry/src/main/cpp/types/libentry/index.d.ts

ArkTS 能看到的接口类型

entry/src/main/ets/pages/Index.ets

完整输入、运行反馈与独立界面

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。

Logo

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

更多推荐