红蓝配尺码,还是四选二?用 kotlin-itertools 做一个鸿蒙组合工具
1. 把规格选择交给迭代器
商品有红、蓝两个颜色,尺码有小、中、大三档。要把所有规格放到列表里,六条数据手写起来不难;再增加一组材质和一组包装,手写就开始容易漏项。同样是“从选项中生成结果”,组队、排班和商品规格的规则其实也有区别:有的允许顺序变化,有的只要选中的成员相同就算一组。
我想把这几种规则做成一个可以直接点的小工具。左侧设置区保留两组输入框,上面填写候选项,下面根据模式填写另一组选项或者选择数量。右侧不是一长列文本,而是一面组合卡片墙,每张卡片放一组结果。按钮切换之后,卡片跟着变化,计数栏也写清楚当前是哪一种运算。这样拿同一组数据试一遍,比只背组合和排列的定义容易记住。
kotlin-itertools 是 Kotlin Multiplatform 的迭代器与组合序列工具库。这里使用它原有的 product、combinations 和 permutations,增加鸿蒙 Native 构建,并通过 ArkUI 展示结果。适配沿用 CPF-KMP-CMP 的工具链,公共算法完整保留;本文先讲怎么选 API,再说明平台文件为什么这样组织。
2. 第一题:两个颜色和三个尺码
商品规格用笛卡尔积。第一组选项的每一项都要与第二组每一项搭配,因此红色配小、中、大,蓝色也配小、中、大。输出是成对的结果,并且保留输入顺序。这种行为很适合先生成规格草稿,再让业务补价格、库存和图片。
示例默认就是红、蓝与三个尺码,结果区为每一组加编号,并把两项以乘号连接。这里的乘号只是展示格式,不参与计算。真正的组合来自 product 返回的 Sequence<Pair<A,B>>,随后再把 Pair 转为统一的列表结构交给界面。
如果左侧或右侧是空集合,笛卡尔积就没有结果。这符合“没有可选尺码,就生成不了规格”的含义。不过输入界面主动拦截空组选项,给出更容易理解的提示;库层检查仍然覆盖空集合,这样可以分清业务输入要求与算法本身的行为。
另一件事是重复选项。输入解析会去掉每项两端的空格与完全为空的项,但没有替用户自动去重。红色填写两次,就可能出现内容相同的规格结果。业务需要唯一规格时,应先定义重复数据的处理策略,不能看到生成列表长了就盲目加 distinct,把用户有意保留的选项也一起删掉。
3. 第二题:四种饮料,选两种
四选二用 combinations。咖啡和茶是一组,茶和咖啡不再单独算另一组;它关心选了谁,忽略排列先后。四项里面选两项会得到六组,卡片按照输入元素顺序生成。选择数量不是额外一组选项,而是下方输入框中的整数,这一点在页面模式提示里写清楚。

选零项这个边界很有意思:它得到一组空组合,而不是完全没有组合。对于后续的组合拼接,这组空结果有自己的意义;页面把它显示成明确的空组合标记,避免一行空白让人以为渲染出了问题。选中全部元素则只有一种组合,直接保留原来的顺序。
超出选项数量的输入由桥接层拦截,提示可用范围。这样用户输入九,而第一行只有红和蓝时,不会等待一次没有业务意义的计算。整数格式转换也使用可失败的解析方式,文字或小数输入会得到原因,不会被默默转成另一个数量。
4. 第三题:前端、后端、测试,安排顺序
排列用 permutations。前端排第一、后端排第二,与后端排第一、前端排第二,是两种不同的安排。三个选项选两个,会得到六种排列;把四种饮料从组合切到排列,结果数量则从六变成十二。两个模式放在相邻按钮里,变化能够在同一页直接观察。
排列仍然不重复使用同一个输入位置。需要放回选择的场景,应使用库里的相应重复组合工具,或者另外明确业务规则。页面只做三种常见入口,避免按钮很多却把含义藏在一串缩写中。检查代码另外覆盖 combinationsWithReplacement,方便确认这类边界能力仍然来自原库。
下面这段就是三种入口的完整选择逻辑。输入范围属于示例的约定,生成器属于原库。生成结果统一为列表后,后面的预览和界面显示可以共用,业务含义仍由当前模式说明。
下面是 harmony/src/Bridge.kt 的完整内容。交叉规格、无序组合和有序排列都从 evaluate 进入原库,随后统一限制预览数量。这里同时给出 tokens、结果编码和十五项断言,避免只复制 product 或 permutations 那一行以后,还不知道两组选项怎样输入、如何停止消费序列、错误怎样返回。
package harmony
import com.github.michaelbull.itertools.*
private fun tokens(text:String)=text.split(',').map { it.trim() }.filter { it.isNotEmpty() }
fun evaluate(input:String,mode:Int):String=try {
require(mode in 0..2) { "未知模式" };require(input.length<=1024) { "输入过长" }
val lines=input.lines();val items=tokens(lines.getOrElse(0){""});require(items.size in 1..8) { "第一行需要 1 到 8 个逗号分隔的选项" }
val output:Sequence<List<String>>
val label:String
when(mode){
0->{val right=tokens(lines.getOrElse(1){""});require(right.size in 1..8) { "规格组合需要第二组选项" };output=items.product(right).map { listOf(it.first,it.second) };label="商品规格的笛卡尔积"}
1->{val k=lines.getOrElse(1){"2"}.trim().toIntOrNull()?:throw IllegalArgumentException("选择数量必须是整数");require(k in 0..items.size) { "选择数量应在 0 到 ${items.size} 之间" };output=items.combinations(k);label="无重复组合,忽略顺序"}
else->{val k=lines.getOrElse(1){"2"}.trim().toIntOrNull()?:throw IllegalArgumentException("排列长度必须是整数");require(k in 0..items.size) { "排列长度应在 0 到 ${items.size} 之间" };output=items.permutations(k);label="排列,顺序不同单独计数"}
}
val preview=output.take(121).toList();val truncated=preview.size>120
result(label+if(truncated)" · 仅预览前 120 项" else " · ${preview.size} 项",preview.take(120).mapIndexed { i,tuple->row("${i+1}",if(tuple.isEmpty())"〈空组合〉" else tuple.joinToString(" × ")) })
} catch(e:Throwable) { result("选项输入有问题",listOf(row("原因",e.message?:"无效输入")),false) }
fun runChecks():String=checks(listOf(
"笛卡尔积数量" to { listOf("红","蓝").product(listOf("S","M","L")).count()==6 },
"笛卡尔积顺序" to { listOf(1,2).product(listOf(3,4)).toList()==listOf(1 to 3,1 to 4,2 to 3,2 to 4) },
"空右集无结果" to { listOf(1).product(emptyList<Int>()).none() },
"组合数量" to { listOf(1,2,3,4).combinations(2).count()==6 },
"组合不重复顺序" to { listOf(1,2,3).combinations(2).toList()==listOf(listOf(1,2),listOf(1,3),listOf(2,3)) },
"零长组合" to { listOf(1,2).combinations(0).toList()==listOf(emptyList<Int>()) },
"全长组合" to { listOf(1,2,3).combinations(3).toList()==listOf(listOf(1,2,3)) },
"排列数量" to { listOf(1,2,3).permutations(2).count()==6 },
"不同顺序都保留" to { val values=listOf(1,2).permutations(2).toList();listOf(1,2) in values && listOf(2,1) in values },
"可重复组合" to { listOf(1,2).combinationsWithReplacement(2).count()==3 },
"幂集包含空集" to { listOf(1,2,3).powerset().count()==8 },
"循环受 take 限制" to { listOf("A","B").cycle().take(5).toList()==listOf("A","B","A","B","A") },
"序列可再次迭代" to { val values=listOf(1,2,3).combinations(2);values.toList()==values.toList() },
"错误长度被拦截" to { evaluate("红,蓝\n9",1).contains("\"ok\":false") },
"错误后正常恢复" to { evaluate("\n2",1).contains("\"ok\":false") && evaluate("红,蓝\n1",1).contains("\"ok\":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)+"}"
}

5. 运行效果:数量变化和错误恢复一起看
运行环境为 API 24 的华为电脑真机(电脑模式、ARM64)与 x86_64 模拟器。两端安装本次编译的签名应用,完成页面操作和 15 项原库检查,均返回 15/15。下面保留连续操作过程。


操作过程从颜色和尺码开始,再转到四选二,随后切换到有顺序的排列。错误预设用超出范围的数量,最后重新选正常输入。这样的顺序能够观察算法模式、数量变化、错误提示和恢复行为,避免只留下默认页面的一张截图。
十五项内置检查还包括笛卡尔积顺序、空右集、零长度组合、全长组合、排列顺序、可重复组合、幂集、有限循环和序列重复遍历。每项都真正消费序列并核对结果。当前示例是小规模同步演示,运行记录没有包含大输入下的耗时或界面响应基准;预览上限是演示的保护条件,不是库处理能力的上限。
6. 结果变多时,先别把列表全塞满
惰性序列不会在创建时立即保存所有组合,但遍历时仍然会产生结果。如果界面最后使用 toList,把整个排列空间都变成列表,内存与等待时间照样可能增长。惰性只是提供按需消费的机会,使用者还得决定消费到哪里停下。
这里限制第一组选项最多八项,预览只展示前一百二十项。为了判断还有没有更多内容,先取一百二十一项:得到额外一项,就知道列表被截断,再显示明确的预览提示。这样不会为了算一个精确总数而遍历完整序列,也不会让“一百二十项”看起来像所有排列已经列完。
结果只有六条或十二条时,示例可以直接显示真实数量;结果超过预览限制时,标题只说明目前显示的上限。业务如果需要总量,可以根据问题类型单独计算组合数,并处理整数溢出。页面预览的大小和数学上的完整结果数量属于两个指标,最好分别表达。
库中 cycle 会创建可反复循环的序列。检查里配合 take 消费有限五项,避免无限循环进入一次同步调用。它可以用于轮换提示或重复测试数据,但不能直接把无限序列交给 toList。这个约束同样适用于后续用序列写业务代码,消费端比创建语句更值得多看一眼。
7. 鸿蒙适配文件,比算法改动更少
环境搭建参考 CPF-KMP-CMP 官方快速开始。这次完整保留上游 commonMain 的十二份算法文件,新增 harmony 目录放平台配置、示例桥接和构建脚本。库本身没有额外运行依赖,平台编译不需要为组合算法引入一套新的实现。
独立配置添加 ARM64 和 X64 两个鸿蒙目标,编译后分别输出动态库及 C 头文件。命令行脚本记录真实编译过程,并核对共享源码与固定上游提交的摘要。它让原库来源和新增平台入口可以分开检查,后续升级时也能看出改动发生在哪个目录。
Bridge 包含输入整理、三种模式、预览边界与十五项检查。N-API 检查传入参数,调用导出函数并复制结果字符串;复制后释放原生分配的返回值。ArkTS 只读结果、显示错误和响应按钮,没有在页面重写一个排列生成器。修改 Kotlin 代码后重新生成库与头文件,再构建应用,保证调用者和导出接口一致。
这次编译遇到的是示例检查中的泛型推断:空列表期望值没有指定元素类型,编译器无法推断。显式写成整数空列表后解决,原库算法没有改动。这个小问题值得记录,因为它发生在检查代码,不应写成“为了适配鸿蒙改写了组合算法”,两者的改动范围不同。
把接口定义和应用配置放到一起看,库到页面的调用次序才完整。下表中 entry 路径均位于 example/harmony 中;生成的 C 头文件由原生构建脚本输出,不需要手工猜测导出结构。
| 文件 | 在调用链中的职责 |
|
| 原库调用、返回编码与设备检查 |
|
| 共享源集、双 ABI 依赖和编译选项 |
|
| 编译 KLIB、链接动态库并生成匹配头文件 |
|
| 参数检查、原生调用与返回值生命周期 |
|
| 按 ABI 选择动态库和头文件 |
|
| ArkTS 能看到的接口类型 |
|
| 完整输入、运行反馈与独立界面 |
7.1 把序列结果排成选择卡片
控件只决定输入的两组选项、抽取数量与三种模式,结果卡片按原生返回顺序展示。页面不会再生成一遍组合,也不会把显示数量当成全部组合数量。完整文件包括选择栏、预览卡片和检查入口,直接对应画面里的三种问题。
先给出 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 CombinationRow { label: string; value: string; metric: number; }
interface CombinationResult { ok: boolean; summary: string; rows: CombinationRow[]; }
interface CheckResult { passed: number; total: number; detail: string; }
@Entry
@Component
struct Index {
@State first: string = '红,蓝';
@State second: string = 'S,M,L';
@State mode: number = 0;
@State summary: string = '';
@State rows: CombinationRow[] = [];
@State ok: boolean = true;
@State checked: string = '尚未执行设备检查';
@State details: string = '';
private samples: string[] = ['红,蓝\nS,M,L', '咖啡,茶,果汁,汽水\n2', '前端,后端,测试\n2', '红,蓝\n9'];
private sampleNames: string[] = ['颜色和尺码', '四选二', '安排顺序', '超出范围'];
private modeNames: string[] = ['规格组合', '不计顺序', '计入顺序'];
private colors: string[] = ['#F7DFC8', '#DFE8FA', '#F0E1FA', '#D6ECDD', '#FAEBC4', '#F8DADE'];
aboutToAppear(): void { this.generate(); }
private generate(): void {
const response = JSON.parse(native.evaluate(`${this.first}\n${this.second}`, this.mode)) as CombinationResult;
this.ok = response.ok; this.summary = response.summary; this.rows = response.rows;
}
private sample(index: number): void {
const parts: string[] = this.samples[index].split('\n'); this.first = parts[0]; this.second = parts[1]; this.generate();
}
private runChecks(): void {
const report = JSON.parse(native.runChecks()) as CheckResult;
this.checked = `设备检查 ${report.passed}/${report.total}`; this.details = report.detail;
}
build() {
Scroll() {
Column({ space: 22 }) {
Row() {
Column({ space: 6 }) {
Text('把选项,排成一张卡片墙').fontSize(32).fontWeight(FontWeight.Bold).fontColor('#533F35')
Text('kotlin-itertools / 商品规格 · 组合 · 排列').fontSize(14).fontColor('#9B7A62')
}.alignItems(HorizontalAlign.Start)
Blank()
Text('×').fontSize(65).fontWeight(FontWeight.Lighter).fontColor('#D8B396').width(80).textAlign(TextAlign.Center)
}.width('100%')
Row({ space: 20 }) {
Column({ space: 16 }) {
Text('选项设置').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#533F35').width('100%')
Text(this.mode === 0 ? '第一组:颜色或类别' : '候选项').fontSize(14).fontColor('#8E6C54').width('100%')
TextInput({ text: this.first, placeholder: '红,蓝' }).id('source-input').maxLength(384)
.height(48).fontSize(18).backgroundColor('#FFF7EF').borderRadius(8)
.onChange((value: string) => this.first = value)
Text(this.mode === 0 ? '第二组:尺码或规格' : '每组选择几项').fontSize(14).fontColor('#8E6C54').width('100%')
TextInput({ text: this.second, placeholder: this.mode === 0 ? 'S,M,L' : '2' }).id('second-input').maxLength(384)
.height(48).fontSize(18).backgroundColor('#FFF7EF').borderRadius(8)
.onChange((value: string) => this.second = value)
ForEach(this.modeNames, (title: string, index: number) => {
Button(title + (this.mode === index ? ' ✓' : '')).id(`mode-${index}`).width('100%').height(42)
.fontSize(15).borderRadius(8).backgroundColor(this.mode === index ? '#A76036' : '#F6E8DD')
.fontColor(this.mode === index ? Color.White : '#7C543A')
.onClick(() => { this.mode = index; this.generate(); })
})
Button('生成卡片').id('evaluate').width('100%').height(50).fontSize(18)
.backgroundColor('#533F35').fontColor(Color.White).borderRadius(8).onClick(() => this.generate())
Text('每组选项最多 8 项,结果最多预览 120 项。').fontSize(12).fontColor('#9B7A62').width('100%').lineHeight(21)
}.width(285).padding(22).backgroundColor(Color.White).borderRadius(18)
Column({ space: 16 }) {
Row() {
Text('结果卡片').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#533F35')
Blank()
Text(this.ok ? '由原库生成' : '请修改输入').fontSize(13).fontColor(this.ok ? '#6F8858' : '#B04F42')
}.width('100%')
Text(this.summary).id('result-summary').fontSize(19).fontWeight(FontWeight.Medium)
.fontColor(this.ok ? '#8B532F' : '#B04F42').width('100%').lineHeight(27)
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.sampleNames, (title: string, index: number) => {
Button(title).id(`sample-${index}`).fontSize(12).backgroundColor('#F2E5D8').fontColor('#865A3D')
.borderRadius(15).margin({ right: 8, bottom: 8 }).onClick(() => this.sample(index))
})
}.width('100%')
Grid() {
ForEach(this.rows, (item: CombinationRow, index: number) => {
GridItem() {
Column({ space: 9 }) {
Text(this.ok ? '#' + item.label : item.label).fontSize(13).fontColor('#9A785F').width('100%')
Text(item.value).fontSize(19).fontWeight(FontWeight.Medium).fontColor('#654832')
.width('100%').lineHeight(26).copyOption(CopyOptions.LocalDevice)
}.width('100%').height('100%').padding(16)
}.backgroundColor(this.ok ? this.colors[index % this.colors.length] : '#FCE2DE').borderRadius(12)
})
}.width('100%').height(Math.max(1, Math.ceil(this.rows.length / 3)) * 110)
.columnsTemplate('1fr 1fr 1fr').columnsGap(12).rowsGap(12)
}.layoutWeight(1).padding({ top: 6 })
}.width('100%').alignItems(VerticalAlign.Top)
Row({ space: 18 }) {
Text(this.checked).fontSize(14).fontColor('#9B7A62').layoutWeight(1)
Button('核对组合规则').id('run-checks').fontSize(14).fontColor('#7C543A').backgroundColor('#EDDDCD')
.onClick(() => this.runChecks())
}.width('100%')
if (this.details.length > 0) {
Text(this.details).fontSize(13).fontFamily('monospace').lineHeight(22).fontColor('#795B45')
.width('100%').padding(20).backgroundColor('#F3E8DC').borderRadius(14)
}
}.width('100%').constraintSize({ maxWidth: 1260 }).padding(30)
}.width('100%').height('100%').backgroundColor('#FCF5EE')
}
}
7.2 组合器怎样导出给页面
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 "kotlinitertools_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) kotlinitertools_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,kotlinitertools_symbols()->kotlin.root.harmony.evaluate(text.c_str(),mode));
}
static napi_value Check(napi_env env,napi_callback_info info){return Copy(env,kotlinitertools_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.3 源集、依赖和双架构链接配置
共享源码位置是 src/commonMain/kotlin。构建配置把 ARM64 与 X64 分开;每个目标当前需要 0 份显式 KLIB 依赖。没有外部依赖不代表可以使用桌面编译器,目标仍须是 OHOS。
harmony/native-config.json 完整内容:
{
"code": "kotlinitertools",
"common": "src/commonMain/kotlin",
"dependencies": {
"ohos_arm64": [],
"ohos_x64": []
}
}
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}/libkotlinitertools.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": "kotlin-itertools · 组合、排列与商品规格",
"main": "",
"author": "",
"license": "",
"dependencies": {
"libentry.so": "file:./src/main/cpp/types/libentry"
}
}
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. 总结:先说清顺序算不算,再去列结果
这份工具最值得保留的,是三种问题到三种 API 的对应关系。商品规格让两组选项交叉搭配,组合忽略顺序,排列保留顺序。输入范围、重复项处理和预览上限再由应用决定。把这些约定写清楚,同一个迭代器工具库就能继续用于套餐、组队、排班或测试数据生成。
新增 harmony 保存平台接入,example/harmony 提供可以运行的鸿蒙应用。原来的迭代器实现保留,因此后续改界面或增加一种业务展示,也能沿用这条调用链。正式功能若涉及很大的结果空间,应先设计分页、取消或逐批消费,再考虑列表样式;让用户知道目前看到了多少,往往比一次堆出所有结果更实用。
项目源码:总仓库 · kotlin-itertools 目录。其他序列工具用法见 上游文档,平台源码组织与应用示例可参考 CPF-KMP-CMP 和 CPF-ApplicationTPC。
更多推荐



所有评论(0)