1. 三个位置,四个任务

一个任务列表不一定要把所有条目都留下。后台调度、待处理消息或者候选结果,有时只需要保留最重要的几个:新的任务比末位更重要,就让它进来;优先级更差,就继续等在队列外。这个规则看着直观,实际调用时,容量、身份键和优先级之间的关系很容易被忽略。

这次接入的 concurrent-priority-queue 是 Kotlin Multiplatform 库。示例做成一个深色任务看板,右侧始终留出三个队列槽位,第一位用暖色标记为“下一项”;左侧放任务输入和观察模式。输入使用 A:5,B:1,C:8,D:2 这样的格式。字母是任务的身份键,数字是优先级,数字越小越优先。看板下方记录每次入队的变化,再提供一个“更新并出队”模式。

起初最容易让人产生误解的地方,不是排序,而是 add 的返回值。它返回空值时,可能是正常加入,也可能更新了同一个任务,还可能根本拒绝了这次输入。所以下面先看一个容量已满的例子,再从结果倒推接口该怎么用。

2. 运行效果与协程检查

看板的操作演示包括容量淘汰、同键更新、拒绝更差任务,以及提升末位优先级后出队。右侧槽位显示最终队列,轨迹保留每次入队的过程,GIF 可以连续看到任务身份、排序位置与容量变化。设备检查随后执行另外的并发、Flow 与取消分支,它们不只是重复页面排序结果。

运行环境为 API 24 的华为电脑真机(电脑模式、ARM64)与 x86_64 模拟器。两端安装本次编译的签名应用,完成页面操作和 16 项原库检查,均返回 16/16。下面保留连续操作过程。

并发检查启动四个默认调度器协程,每个写入二十个不同身份键,队列容量设为一百。全部结束后检查八十个任务都存在,且优先级按零到七十九排序。这是一次有明确输入和最终断言的功能检查,不是长时间竞争压力测试,也没有比较不同设备的执行速度。

Flow 检查通过库的挂起变换接口加入十个任务,容量为三,最终应只保留优先级一、二、三。取消检查则为变换设置每次一百毫秒延迟,再由三十毫秒超时取消整个调用,确认没有完成变换后的任务被继续加入。这个条件针对当前测试路径,不能扩展成取消能撤销任意已入队任务的承诺。

3. D 入队以后,C 到哪去了

先加入 A,优先级为五;再加入 B,优先级为一;最后加入 C,优先级为八。按比较器排序,队列成为 B:1 → A:5 → C:8,已经到达容量三。第四个任务 D 的优先级为二,比末位 C 更好,所以它能够进入,同时 C 被淘汰。

三个槽位会显示 B、D、A,下方轨迹显示 B:1 → D:2 → A:5,并标出“淘汰 C:8”。这次 add 的返回值就是 C,而不是刚刚加入的 D。需要统计因容量限制而被移出的任务,可以读取这个返回值;需要显示当前待办,则应该读取 items.value,两者承担的用途不同。

再换成 A:1,B:2,C:3,D:9。前三个已经占满容量,D 不比末位 C 更好,它就不会进入。队列仍然是 A、B、C,容量也没有超过三。这里的“没有发生淘汰”与“任务成功加入”不能划等号,第四次调用正好说明了原因。

比较器决定“好”的方向。示例使用 compareBy { it.priority },因此小数字更优先。若业务把大数字定义为紧急程度,就需要使用反向比较器;只把文字改成“大数字更紧急”,而保留原比较器,列表行为就会与提示相反。界面副标题明确写出排序规则,输入也不替用户猜测方向。

4. add 返回 null,究竟发生了哪一种情况

查看原实现,add 只在容量被挤出一个末位元素时返回被淘汰项,其余路径返回空值。空队列加入第一个任务,不需要淘汰,返回空;同键优先级成功提升,没有增加数量,也返回空;满容量下拒绝一个更差的任务,仍然返回空。

所以如果调用后马上弹出“加入成功”,依据不能只有返回值为空。这个示例每次调用后重新读取当前快照,并同时显示任务键与优先级。用户可以从完整队列看到该任务是否存在、位置如何变化。对业务接口来说,也可以额外查询 containsKey 或按键读取对应任务,但应事先明确自己想确认的是存在性、更新结果还是淘汰结果。

这并不意味着 API 没有规则,而是返回值服务于淘汰通知。把不同结果混成一个成功布尔值,反而会让拒绝路径消失。特别是用户提交较低优先级任务时,界面如果总提示成功,随后又找不到条目,会比直接说明未进入队列更令人困惑。

这里还有并发上的边界:一次查询读到的是当时的快照,别的调用可能随后改变队列。示例的交互演示是一次操作后显示结果,没有拿“读前再读后”的差异去构造普遍可靠的并发事务判断。若业务必须原子地知道一次更新是否接受,就要根据接口设计另外讨论这个需求。

5. 同一个 A 能更新,但不能降低优先级

输入 A:5,A:1,B:3。第二次出现的 A 与第一次具有相同身份键,库不会保留两个 A,而是用更好的优先级一替换原优先级五。最终数量为二,显示 A 与 B。这可以避免同一任务重复占用有限的容量。

反过来,先加入 A 一,再加入 A 五,原任务会保留。相同或者更差的优先级不会覆盖已有值。这里的更新不是通常数据库语境中“同键就写入新对象”的无条件替换。需要降低某个任务优先级的业务,不能期待再次 add 就自动达到目的,而要明确使用删除后重建等适合自身状态的流程。

“更新并出队”模式先完成输入中的逐次加入,再找到当前末位的身份键,将它提升到优先级零。队列重新排序后,调用 poll(),刚提升的任务被取出。默认样例里这一过程作用于 A,最后留下 B 与 D。这段演示把同键更新、重排和出队放在一起,能观察到它们之间的顺序。

6. 一份快照与一次出队

items 暴露为 StateFlow<List<T>>,页面读取 items.value 得到优先级顺序的列表。检查中会先保存一份只有 A 的快照,再加入 B,确认旧列表仍然只有一项而当前队列有两项。这个检查针对快照内容,不把它描述为业务对象的深拷贝。

读取首项不会删除,poll() 才会取出最高优先级任务并移除对应键。空队列出队返回空值,因此页面明确显示“空队列”,不会尝试把不存在的任务继续拆成键和优先级。按键删除、谓词删除以及清空也有独立的检查,避免演示只覆盖不断加入的单一路径。

看板不是把输入再排序一次得到的。它从原库返回的“当前任务”行拆出身份键与优先级,再按返回顺序摆进三个槽位。任务较少时,剩余位置显示空位;操作轨迹则保留每次调用后的队列文本,淘汰与出队记录用暖色突出。这样不用在一长段结果中寻找某个字母,队首是谁、容量还剩多少都能直接看到。

上游内部使用持久化 Treap 与状态流原子更新,适配保持这些代码原样。可以从源码看到它的实现策略,但本文没有据此声称测出了设备上的无锁性能,也没有给出每秒吞吐量。数据结构的设计、并发下的功能检查和性能基准,各自需要不同证据。

7. 接到鸿蒙:依赖和桥接代码

环境仍参考 CPF-KMP-CMP 快速开始。与没有外部依赖的小算法库相比,这个库还需要协程、原子操作和不可变集合的鸿蒙制品。适配分别准备对应两个架构的 KLIB,记录下载地址、版本和哈希,再将它们与上游七份共享源码一起编译链接。

使用的依赖基线是官方鸿蒙协程版本 1.10.2-1.1.0-02、atomicfu 0.31.0-1.1.0-02 和 immutable 0.4.0-1.1.0-02。除了主制品,还要带上协程和 atomicfu 对应版本的 cinterop-interop KLIB。每种架构各有五个依赖文件,不能只根据三个 Maven 名称就认为原生依赖已经齐全。示例构建时,动态库和依赖必须与目标一致,这里没有用 JVM 包冒充 Native 依赖,也没有删掉协程接口以绕过编译。

这一步确实踩了坑。第一次设备检查,前十三项队列行为通过,三个协程分支却报告找不到 coroutines_atomic_store_release。主 KLIB 的清单还引用了独立的 CInterop 制品;没有显式带入时,不完整链接可以留下运行时才报错的调用。因此修复时补齐同版本的 CInterop 依赖,同时关闭不完整链接,让缺失符号直接在构建阶段暴露。这里保留了原来的三个协程检查,没有把它们删掉来凑通过数。

新增内容放在 harmony 目录,构建脚本负责两个目标,桥接文件负责解析输入、调用真实队列与输出 JSON。原仓库 example/harmony 提供 ArkTS 界面和 N-API 调用示例。检查入口通过 N-API 后台工作执行,再用 Promise 回传结果;页面等待检查时仍能响应操作,避免把包含挂起与超时的检查放在界面线程里。普通任务观察仍是同步调用。下面是容量淘汰检查中的实际调用,队列实现来自原库。

下面是 harmony/src/Bridge.kt 的完整内容。队列的比较器、身份键、容量、输入解析、入队轨迹和快照都在这里定义。完整 runChecks 还包含四协程入队、Flow 转换和取消,前三者之外的检查不能省掉,否则上次缺少 CInterop 依赖的问题就不会暴露。页面观察和后台检查分别调用同一份原库实现。

package harmony
import io.github.karloti.cpq.ConcurrentPriorityQueue
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
private data class Task(val id:String,val priority:Int)
private fun queue(size:Int=3)=ConcurrentPriorityQueue<Task,String>(compareBy{it.priority},size,keySelector={it.id})
private fun display(q:ConcurrentPriorityQueue<Task,String>)=q.items.value.joinToString(" → "){"${it.id}:${it.priority}"}
fun evaluate(input:String,mode:Int):String=try{
    val q=queue();val rows=mutableListOf<String>()
    input.split(',',';','\n').map{it.trim()}.filter{it.isNotEmpty()}.forEachIndexed{i,s->
        val p=s.split(':');require(p.size==2){"任务格式 A:5,B:1,C:8,D:2"};val task=Task(p[0].trim(),p[1].trim().toInt())
        val evicted=q.add(task);rows+=row("入队 ${i+1} · ${task.id}:${task.priority}",display(q)+(if(evicted!=null) " · 淘汰 ${evicted.id}:${evicted.priority}" else ""))
    }
    if(mode==1){val id=q.last()?.id?:"A";q.add(Task(id,0));rows+=row("提高 $id 的优先级",display(q));val out=q.poll();rows+=row("最高优先级出队",out?.let{"${it.id}:${it.priority}"}?:"空队列")}
    rows+=row("当前任务",display(q).ifEmpty{"空队列"});rows+=row("容量", "${q.size} / 3",q.size/3.0)
    result(if(mode==0) "容量 3 · 数字越小越优先" else "同键优先级更新与出队",rows)
}catch(e:Exception){result("任务输入有误",listOf(row("提示",e.message?:"请检查任务")),false)}
fun runChecks():String=checks(listOf(
    "空队列出队为空" to {queue().poll()==null},
    "数字小的先出队" to {val q=queue();q.addAll(listOf(Task("A",5),Task("B",1)));q.poll()?.id=="B"},
    "容量满时淘汰末位" to {val q=queue();q.addAll(listOf(Task("A",5),Task("B",1),Task("C",8)));q.add(Task("D",2))?.id=="C"&&q.size==3},
    "更差任务被拒绝" to {val q=queue();q.addAll(listOf(Task("A",1),Task("B",2),Task("C",3)));q.add(Task("D",9));!q.containsKey("D")},
    "同键更好优先级更新" to {val q=queue();q.add(Task("A",5));q.add(Task("A",1));q.size==1&&q["A"]?.priority==1},
    "同键更差优先级拒绝" to {val q=queue();q.add(Task("A",1));q.add(Task("A",5));q["A"]?.priority==1},
    "按键删除" to {val q=queue();q.add(Task("A",1));q.removeByKey("A")&&q.isEmpty()},
    "不存在的键不删除" to {!queue().removeByKey("missing")},
    "清空队列" to {val q=queue();q.add(Task("A",1));q.clear();q.size==0},
    "快照不随之后入队改变" to {val q=queue();q.add(Task("A",1));val old=q.items.value;q.add(Task("B",2));old.size==1&&q.size==2},
    "谓词移除" to {val q=queue();q.addAll(listOf(Task("A",1),Task("B",7)));q.removeIf{it.priority>3}==1&&q.size==1},
    "构建器生成独立队列" to {val q=queue();q.add(Task("A",1));val next=q.mutate{add(Task("B",2))};q.size==1&&next.size==2},
    "零容量拒绝" to {try{queue(0);false}catch(_:IllegalArgumentException){true}},
    "四协程并发入队" to {runBlocking{withTimeout(5000){val q=queue(100);coroutineScope{repeat(4){worker->launch(Dispatchers.Default){repeat(20){q.add(Task("$worker-$it",worker*20+it))}}}};q.size==80&&q.items.value.map{it.priority}==(0..79).toList()}}},
    "Flow 变换保持容量及排序" to {runBlocking{withTimeout(5000){val q=queue();q.addAll((1..10).asFlow()){delay(1);Task("T$it",it)};q.items.value.map{it.priority}==listOf(1,2,3)}}},
    "取消停止 Flow 后续处理" to {runBlocking{val q=queue(20);var cancelled=false;try{withTimeout(30){q.addAll((1..20).asFlow()){delay(100);Task("T$it",it)}}}catch(_:TimeoutCancellationException){cancelled=true};cancelled&&q.isEmpty()}}
))

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)+"}"
}

把接口定义和应用配置放到一起看,库到页面的调用次序才完整。下表中 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 文件包含同步 evaluate 和异步 runChecks。ExecuteChecks 只在后台取得结果,CompleteChecks 再回到环境中完成 Promise,并清理工作对象。重复触发会被 checking 拦住;创建或排队失败也有对应释放路径,不能只复制正常完成回调。

完整文件:example/harmony/entry/src/main/cpp/napi_init.cpp。

#include "napi/native_api.h"
#include "concurrentpriorityqueue_api.h"
#include <atomic>
#include <string>

namespace {
std::atomic<bool> checking{false};
struct CheckWork {
    napi_async_work work{};
    napi_deferred deferred{};
    std::string result;
};

napi_value Text(napi_env env, const std::string& text) {
    napi_value value{};
    napi_create_string_utf8(env, text.c_str(), text.size(), &value);
    return value;
}

std::string CopyAndFree(const char* value) {
    if (!value) return "{}";
    std::string result(value);
    concurrentpriorityqueue_symbols()->DisposeString(value);
    return result;
}

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 input(length + 1, '\0');
    napi_get_value_string_utf8(env, args[0], input.data(), input.size(), &length);
    input.resize(length);
    return Text(env, CopyAndFree(concurrentpriorityqueue_symbols()->kotlin.root.harmony.evaluate(input.c_str(), mode)));
}

// Kotlin runBlocking belongs on a background worker, never the ArkUI thread.
void ExecuteChecks(napi_env, void* data) {
    auto* item = static_cast<CheckWork*>(data);
    item->result = CopyAndFree(concurrentpriorityqueue_symbols()->kotlin.root.harmony.runChecks());
}

void CompleteChecks(napi_env env, napi_status status, void* data) {
    auto* item = static_cast<CheckWork*>(data);
    if (status == napi_ok) {
        napi_resolve_deferred(env, item->deferred, Text(env, item->result));
    } else {
        napi_reject_deferred(env, item->deferred, Text(env, "Native check work did not complete"));
    }
    checking.store(false);
    napi_delete_async_work(env, item->work);
    delete item;
}

napi_value Check(napi_env env, napi_callback_info) {
    if (checking.exchange(true)) {
        napi_throw_error(env, nullptr, "A check is already running");
        return nullptr;
    }
    auto* item = new CheckWork;
    napi_value promise{};
    if (napi_create_promise(env, &item->deferred, &promise) != napi_ok) {
        checking.store(false);
        delete item;
        napi_throw_error(env, nullptr, "Unable to create check promise");
        return nullptr;
    }
    napi_value name = Text(env, "PriorityQueueChecks");
    if (napi_create_async_work(env, nullptr, name, ExecuteChecks, CompleteChecks, item, &item->work) != napi_ok ||
        napi_queue_async_work(env, item->work) != napi_ok) {
        checking.store(false);
        napi_reject_deferred(env, item->deferred, Text(env, "Unable to queue checks"));
        if (item->work) napi_delete_async_work(env, item->work);
        delete item;
    }
    return promise;
}

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;
}
napi_module module = {1, 0, nullptr, Init, "entry", nullptr, {0}};
}
extern "C" __attribute__((constructor)) void Register() { napi_module_register(&module); }

7.2 后台检查完成后再更新状态

runChecks 返回 Promise,页面先显示正在检查,再在完成回调中解析通过数和详情;失败走错误回调。检查期间按钮状态也受 checking 控制。普通入队演示仍是短小的同步调用。完整文件保留了两条调用路径,不能把 Promise 当字符串直接交给 JSON.parse。

先给出 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: () => Promise<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; }
interface QueueItem { id: string; priority: string; }

@Entry
@Component
struct Index {
  @State input: string = 'A:5,B:1,C:8,D:2';
  @State mode: number = 0;
  @State selectedSample: number = 0;
  @State view: Result = { ok: true, summary: '', rows: [] };
  @State tasks: QueueItem[] = [];
  @State traces: ResultRow[] = [];
  @State checkStatus: string = '16 项队列与协程检查待运行';
  @State details: string = '';
  @State checking: boolean = false;
  private slots: number[] = [0, 1, 2];
  private samples: string[] = ['A:5,B:1,C:8,D:2', 'A:5,A:1,B:3', 'A:1,B:2,C:3,D:9', 'A:7,B:2,C:4'];
  private sampleLabels: string[] = ['容量淘汰', '同键更新', '拒绝低优先级', '任务全入队'];

  aboutToAppear(): void { this.refresh(); }

  private refresh(): void {
    this.view = JSON.parse(native.evaluate(this.input, this.mode)) as Result;
    const cards: QueueItem[] = [];
    const logs: ResultRow[] = [];
    for (const item of this.view.rows) {
      if (item.label === '当前任务' && item.value !== '空队列') {
        const entries = item.value.split(' → ');
        for (const entry of entries) {
          const separator = entry.lastIndexOf(':');
          if (separator >= 0) cards.push({ id: entry.substring(0, separator), priority: entry.substring(separator + 1) });
        }
      } else if (item.label.startsWith('入队') || item.label.startsWith('提高') || item.label === '最高优先级出队') {
        logs.push(item);
      }
    }
    this.tasks = cards;
    this.traces = logs;
  }

  private chooseSample(index: number): void {
    this.selectedSample = index;
    this.input = this.samples[index];
    this.refresh();
  }

  private capacity(): string {
    for (const row of this.view.rows) {
      if (row.label === '容量') return row.value;
    }
    return '— / 3';
  }

  private async runVerification(): Promise<void> {
    if (this.checking) return;
    this.checking = true;
    this.checkStatus = '后台检查进行中';
    this.details = '';
    try {
      const checks = JSON.parse(await native.runChecks()) as Checks;
      this.checkStatus = `设备检查 ${checks.passed}/${checks.total}`;
      this.details = checks.detail;
    } catch (error) {
      this.checkStatus = '检查未完成';
      this.details = String(error);
    } finally {
      this.checking = false;
    }
  }

  @Builder
  taskSlot(index: number) {
    if (index < this.tasks.length) {
      Column({ space: 12 }) {
        Row() {
          Text(`TASK ${this.tasks[index].id}`).fontSize(14).fontWeight(FontWeight.Bold)
            .fontColor(index === 0 ? '#ECA872' : '#C3CAD6').layoutWeight(1)
          Text(index === 0 ? '下一项' : `第 ${index + 1} 位`).fontSize(11).fontColor(index === 0 ? '#F0AF79' : '#8793A7')
        }.width('100%')
        Text(this.tasks[index].priority).fontSize(55).fontWeight(FontWeight.Bold).fontFamily('monospace')
          .fontColor(index === 0 ? '#F0AF79' : '#E3E9F2').width('100%')
        Text('优先级 · 数字越小越优先').fontSize(11).fontColor('#8C99AA').width('100%')
      }.layoutWeight(1).padding(20).height(175).backgroundColor(index === 0 ? '#342C27' : '#232D3B')
        .border({ width: 1, color: index === 0 ? '#A77954' : '#334052' }).borderRadius(8)
    } else {
      Column({ space: 12 }) {
        Text(`SLOT ${index + 1}`).fontSize(12).fontFamily('monospace').fontColor('#57677C')
        Text('空位').fontSize(24).fontColor('#536378')
        Text('等待任务进入').fontSize(11).fontColor('#536378')
      }.layoutWeight(1).height(175).justifyContent(FlexAlign.Center).backgroundColor('#1B2533')
        .border({ width: 1, color: '#2C394B', style: BorderStyle.Dashed }).borderRadius(8)
    }
  }

  build() {
    Scroll() {
      Column({ space: 24 }) {
        Row({ space: 14 }) {
          Column({ space: 6 }) {
            Text('任务调度台').fontSize(31).fontWeight(FontWeight.Bold).fontColor('#EDF1F7')
            Text('Concurrent Priority Queue · 保留最重要的三项').fontSize(13).fontColor('#8797AB')
          }.alignItems(HorizontalAlign.Start).layoutWeight(1)
          Column({ space: 4 }) {
            Text(this.capacity()).id('queue-capacity').fontSize(26).fontFamily('monospace').fontWeight(FontWeight.Bold).fontColor('#F0AF79')
            Text('当前数量 / 容量').fontSize(10).fontColor('#8D9CAC')
          }.alignItems(HorizontalAlign.End)
        }.width('100%')

        Row({ space: 24 }) {
          Column({ space: 17 }) {
            Text('任务输入').fontSize(17).fontWeight(FontWeight.Medium).fontColor('#D2DCE9').width('100%')
            Text('身份键:优先级,用逗号分开').fontSize(12).fontColor('#8797AB').width('100%')
            TextInput({ text: this.input, placeholder: 'A:5,B:1,C:8' }).id('source-input')
              .maxLength(4096).fontSize(14).fontColor('#E0E8F2').backgroundColor('#273448')
              .onChange((text: string) => { this.input = text; this.selectedSample = -1; this.refresh(); })
            Column({ space: 8 }) {
              ForEach(this.sampleLabels, (label: string, index: number) => {
                Button({ type: ButtonType.Normal }) {
                  Row() {
                    Text(label).fontSize(13).fontColor(this.selectedSample === index ? '#F3B882' : '#A6B4C7')
                    Blank()
                    Text(`0${index + 1}`).fontSize(11).fontFamily('monospace').fontColor('#74859C')
                  }.width('100%')
                }.id(`sample-${index}`).width('100%').height(40).padding({ left: 13, right: 13 })
                  .backgroundColor(this.selectedSample === index ? '#3C3029' : '#223044').borderRadius(5)
                  .onClick(() => { this.chooseSample(index); })
              })
            }.width('100%')
            Divider().color('#354153')
            Button('观察逐次入队').id('mode-0').type(ButtonType.Normal).width('100%').height(42).fontSize(13)
              .backgroundColor(this.mode === 0 ? '#B77B49' : '#29374B').fontColor(this.mode === 0 ? '#FFFFFF' : '#A0B0C4').borderRadius(5)
              .onClick(() => { this.mode = 0; this.refresh(); })
            Button('更新优先级,再出队').id('mode-1').type(ButtonType.Normal).width('100%').height(42).fontSize(13)
              .backgroundColor(this.mode === 1 ? '#B77B49' : '#29374B').fontColor(this.mode === 1 ? '#FFFFFF' : '#A0B0C4').borderRadius(5)
              .onClick(() => { this.mode = 1; this.refresh(); })
            Text('同键只接受更好的优先级;满容量时拒绝更差任务。').fontSize(12).lineHeight(21).fontColor('#8596AC').width('100%')
          }.width(285).padding(20).backgroundColor('#1B2839').borderRadius(10)

          Column({ space: 22 }) {
            Row() {
              Text('等待处理').fontSize(18).fontWeight(FontWeight.Medium).fontColor('#DCE5F0').layoutWeight(1)
              Text('PRIORITY →').fontSize(10).letterSpacing(2).fontColor('#8396AD')
            }.width('100%')
            Row({ space: 12 }) {
              ForEach(this.slots, (index: number) => { this.taskSlot(index); })
            }.width('100%')
            Text(this.view.summary).id('summary').fontSize(16).fontColor(this.view.ok ? '#D6A374' : '#F19391').width('100%')
            Row() {
              Text('操作轨迹').fontSize(16).fontColor('#C7D4E6').fontWeight(FontWeight.Medium).layoutWeight(1)
              Text('结果来自原队列').fontSize(11).fontColor('#70859F')
            }.width('100%').padding({ top: 3 })
            Column({ space: 0 }) {
              ForEach(this.traces, (item: ResultRow, index: number) => {
                Row({ space: 14 }) {
                  Text((index + 1).toString()).width(25).fontSize(14).fontFamily('monospace').fontColor('#A97853')
                  Column({ space: 7 }) {
                    Text(item.label).fontSize(13).fontColor('#91A3BA').width('100%')
                    Text(item.value).id(`trace-${index}`).fontSize(15).fontFamily('monospace')
                      .fontColor(item.value.includes('淘汰') || item.label.includes('出队') ? '#F1B17A' : '#D0DBE9')
                      .lineHeight(24).width('100%')
                  }.layoutWeight(1).alignItems(HorizontalAlign.Start)
                }.width('100%').padding({ top: 15, bottom: 15 }).border({ width: { bottom: 1 }, color: '#2D3B4E' })
              })
              if (!this.view.ok && this.view.rows.length > 0) {
                Text(this.view.rows[0].value).fontSize(14).fontColor('#F4A09B').padding(16).width('100%')
              }
            }.width('100%')
          }.layoutWeight(1).padding(24).backgroundColor('#182332').borderRadius(10)
        }.width('100%').alignItems(VerticalAlign.Top)

        Row({ space: 18 }) {
          Column({ space: 5 }) {
            Text('队列、Flow 与取消').fontSize(15).fontColor('#B7C6D9').fontWeight(FontWeight.Medium)
            Text(this.checkStatus).id('check-status').fontSize(13).fontColor('#778BA5')
          }.alignItems(HorizontalAlign.Start).layoutWeight(1)
          Button(this.checking ? '后台检查中' : '运行原库检查').id('run-checks').fontSize(14)
            .backgroundColor('#B77B49').enabled(!this.checking).onClick(() => { this.runVerification(); })
        }.width('100%')
        if (this.details.length > 0) {
          Text(this.details).id('check-details').fontSize(12).fontFamily('monospace').lineHeight(23).fontColor('#A7BBCF')
            .padding(20).width('100%').backgroundColor('#1B2839').borderRadius(8)
        }
      }.padding(30).width('100%').constraintSize({ maxWidth: 1310 }).alignItems(HorizontalAlign.Start)
    }.width('100%').height('100%').backgroundColor('#101A27')
  }
}

7.3 源集、依赖和双架构链接配置

共享源码位置是 library/src/commonMain/kotlin。构建配置把 ARM64 与 X64 分开;每个目标当前需要 5 份显式 KLIB 依赖,文件名中的目标架构与版本都要对应。依赖来源和摘要已保存在平台目录。 其中包括协程和 atomicfu 的 CInterop 制品,并关闭不完整链接,避免把缺符号问题留到设备运行时。

harmony/native-config.json 完整内容:

{
  "common": "library/src/commonMain/kotlin",
  "code": "concurrentpriorityqueue",
  "dependencies": {
    "ohos_arm64": [
      "kotlinx-coroutines-core-ohosarm64-1.10.2-1.1.0-02.klib",
      "atomicfu-ohosarm64-0.31.0-1.1.0-02.klib",
      "kotlinx-collections-immutable-ohosarm64-0.4.0-1.1.0-02.klib",
      "kotlinx-coroutines-core-ohosarm64-1.10.2-1.1.0-02-cinterop-interop.klib",
      "atomicfu-ohosarm64-0.31.0-1.1.0-02-cinterop-interop.klib"
    ],
    "ohos_x64": [
      "kotlinx-coroutines-core-ohosx64-1.10.2-1.1.0-02.klib",
      "atomicfu-ohosx64-0.31.0-1.1.0-02.klib",
      "kotlinx-collections-immutable-ohosx64-0.4.0-1.1.0-02.klib",
      "kotlinx-coroutines-core-ohosx64-1.10.2-1.1.0-02-cinterop-interop.klib",
      "atomicfu-ohosx64-0.31.0-1.1.0-02-cinterop-interop.klib"
    ]
  },
  "compilerOptions": [
    "-Xpartial-linkage=disable"
  ]
}

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}/libconcurrentpriorityqueue.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.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'))]+CONFIG.get('compilerOptions',[])+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. 总结:先把队列的规则看明白

这次鸿蒙接入保留了原共享实现与协程依赖,页面上的排序、淘汰、同键更新和出队都来自实际库调用。最该记住的规则是:数字大小由比较器解释,同键只能提升优先级,而 add 返回空值不等于每一次输入都被接受。

只有把任务身份、容量和观察快照的含义对齐,队列才容易接进实际应用。至于更大任务量、持续并发和吞吐比较,应在这些规则已经确认后另外设计测量。完整示例可见 并发优先队列适配目录,鸿蒙跨平台工具与文档在 CPF-KMP-CMP。

其他适配示例见 开源合集,应用与三方库共建资料见 CPF-ApplicationTPC。

Logo

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

更多推荐