HarmonyOS 7 FAST HashMap 只存指针:覆盖旧值和删除条目后,内存到底谁释放

API 26.0.0 的 FAST Kit 增加了面向单线程场景的 HashMap。接口看着像常见的 insert/find/erase,最容易被忽略的却是它只保存键和值的指针,不替调用方管理那两块内存。如果把临时变量地址塞进去后跨作用域继续查,或者以为 Clear 会替你 delete,问题并不在哈希算法,而在指针的生命周期。

本文讨论的是 C/C++ 侧 fast_collections_hashmap.h,不是 ArkTS 的 Map,也不对它和 std::unordered_map 下性能结论。两个案例一个用函数内的稳定变量解释覆盖,一个用堆内存解释删除;每个动作后都明确谁仍拥有指针。本文根据华为开发者 API 26.0.0 文档和示例编写;本机没有 API 26.0.0 NDK,也没有真机编译/性能数据,不能把下文当成已经跑通的 FAST Kit 工程。

指针生命周期图

先看接口语义,不要猜所有权

官方文档列出 HMS_FAST_Hashmap_Create、Insert、TryInsert、Find、Erase、Clear、Destroy 等接口。Insert 遇到同名键会覆盖原值,并通过 originValue 返回旧值地址;TryInsert 遇到同名键则不覆盖。Erase 可以通过 originKey、originValue 把被删除条目的地址交还给调用方。Find 的输出参数只有在返回成功后才有效。

最重要的前提是:表里保存的是调用方提供的指针。键和值所指向的内存,必须至少活到条目从表中移除;如果是动态分配的,调用方还要在正确时机释放。官方同时明确该 HashMap 设计用于单线程;跨线程同时读写不要靠它碰运气,要选并发容器或自行建立严格同步边界。

案例一:覆盖同一个键,旧值不会自动消失

先用栈变量做一个最容易核对的示例。所有变量和 HashMap 都在同一函数作用域里,销毁表在函数返回之前,因此不会把局部变量的地址留给更长寿命的容器。下面是核心流程节选,完整工程仍需按目标工程添加 NAPI 入口和 FAST Kit 链接配置。

#include "FASTKit/fast_collections_hashmap.h"
#include <functional>

uint64_t hash_int(const FAST_HashmapKeyPtr p) {
    return std::hash<int>{}(*static_cast<const int*>(p));
}

int32_t equal_int(const FAST_HashmapKeyPtr a, const FAST_HashmapKeyPtr b) {
    return *static_cast<const int*>(a) == *static_cast<const int*>(b);
}

void overwrite_case() {
    FAST_HashmapHandle map;
    if (HMS_FAST_Hashmap_Create(&map, hash_int, equal_int) != FAST_ERROR_CODE_SUCCESS) {
        return;
    }

    int key = 42;
    int first = 10;
    int second = 20;
    FAST_HashmapValuePtr oldValue = nullptr;
    FAST_ErrorCode rc = HMS_FAST_Hashmap_Insert(map, &key, &first, nullptr);
    if (rc == FAST_ERROR_CODE_SUCCESS) {
        rc = HMS_FAST_Hashmap_Insert(map, &key, &second, &oldValue);
    }
    // 成功后 oldValue 指向 first;表中同一个键的值现在指向 second。
    // first/second 是栈变量,这里不能 delete oldValue。
    HMS_FAST_Hashmap_Destroy(map);
}

FAST_ERROR_CODE_SUCCESS 的名称需以当前 SDK 头文件核对。把示例改为动态分配时,覆盖成功后才应考虑释放返回的旧值,且要确认它没有被其他对象共享。一个常见错误是在 Insert 之前先 delete first:这时表里还保存着旧指针,若覆盖失败,表便成了悬空指针。另一个错误是把 oldValue 当成新值再释放,随后 Find 读到的地址就不可信。

上面这段代码是生命周期演示,不是可直接忽略错误码的封装。实际检查应写清每步的成功条件:第一次 Insert 成功;第二次成功且 oldValue == &first;Find(map, &key, &found) 成功且 found == &second;最后再销毁表。只有调用成功才读取输出参数。若第二次覆盖失败,旧值仍可能在表里,必须保留原地址直到移除条目或销毁表;不能因为尝试覆盖过就释放它。

复现办法:在 API 26.0.0 工程里插入键 42 的值 10,再覆盖成 20,逐次检查返回码、表大小、originValue 和 Find 结果。只检查“最终能查到 20”是不够的,还要核对旧值地址如何回收。

案例二:删除条目以后,谁负责释放堆上的键和值

上一个案例中的数据会随函数退出自动结束生命周期。真正容易漏的是堆内存:new int 后把地址给表,Erase 只是删除映射关系;删除成功后,调用方仍需释放拿回的键和值。

void erase_case() {
    FAST_HashmapHandle map;
    if (HMS_FAST_Hashmap_Create(&map, hash_int, equal_int) != FAST_ERROR_CODE_SUCCESS) {
        return;
    }

    int* key = new int(7);
    int* value = new int(70);
    FAST_ErrorCode rc = HMS_FAST_Hashmap_Insert(map, key, value, nullptr);
    if (rc != FAST_ERROR_CODE_SUCCESS) {
        delete key;
        delete value;
        HMS_FAST_Hashmap_Destroy(map);
        return;
    }

    int lookupKey = 7;
    FAST_HashmapKeyPtr removedKey = nullptr;
    FAST_HashmapValuePtr removedValue = nullptr;
    rc = HMS_FAST_Hashmap_Erase(map, &lookupKey, &removedKey, &removedValue);
    // 成功时应核对 removedKey == key、removedValue == value。
    // 无论 Erase 是否成功,都先结束表的生命周期,再统一释放各一次。
    HMS_FAST_Hashmap_Destroy(map);
    delete key;
    delete value;
}

这里还有一条异常路径:如果 Erase 失败,示例不应直接释放仍可能存放在表里的指针;先记录失败,等表销毁后再释放自己保存的 key、value。若成功返回的 removedKey、removedValue 与原指针不一致,更要暂停并查明所有权,不能一边释放返回地址一边再释放原地址。生产代码可建立统一的“遍历并释放键值,然后清空/销毁”封装,但不要假设 Clear 或 Destroy 会替你 delete 动态分配的对象。官方给出的 EraseIf 带有释放回调,适合集中清理符合条件的条目;回调期间不应阻塞或重新进入同一 HashMap API。

复现办法:向表插入堆上的 (7,70),用另一个值相同但地址不同的 lookupKey 删除。若哈希/相等函数按整数值实现,删除应命中;如果错误地按指针地址比较,它就找不到。再分别测重复删除、空表删除和插入失败时的释放路径,配合内存检测工具找泄漏或二次释放。

分支表是否仍持有地址谁负责释放要核对的结果
插入失败否调用方立即释放 key/value两块内存各释放一次
插入成功,删除失败可能是先销毁表,再由调用方释放原地址不在表仍可访问时释放
插入和删除都成功否调用方核对返回地址后释放removedKey == key 且 removedValue == value
直接 Clear 或 Destroy映射被清除调用方仍要掌握此前分配的地址不把容器销毁误当对象析构

以上是按官方接口语义推导的预期断言,并非本机 API 26 FAST Kit 的运行日志。若项目有多处插入、删除和清空,单独包装所有权比在每个页面散落 delete 更可靠;可考虑持有分配记录或使用 EraseIf 的释放回调,但回调在容器内部锁持有期间执行,不可阻塞或重入同一个容器。

为避免只在纸面讨论所有权,我另外用 Windows 上的 MSVC C++17 编译运行了一个标准容器指针模型:它以 std::unordered_map<Key*, Value*> 保存原始地址,用按值比较的哈希/相等函数,逐次断言插入、另一地址的等值键查找、覆盖旧值、删除后归还地址与分别释放。运行输出为 PASS: insert, equal-value lookup, overwrite, erase, ownership release。这个结果只证明测试代码里的所有权流程自洽,不证明 FAST Kit 的 API 26 头文件能编译,更不证明 FAST Kit 的实际覆盖/删除行为。两者采用的容器实现不同,不能拿模型测试替换目标设备测试。

该选哪种处理方式

单线程且生命周期清楚时,可以使用这组接口,但建议把创建、插入、覆盖、删除和销毁都集中在一个所有权封装里。跨线程共享则不要直接用这个单线程 HashMap;高并发场景需要并发容器或同步设计。键如果是可变对象,插入后也不要随意修改参与哈希/相等比较的字段,否则之后按原值查找的行为就难以解释。

我会把回归检查至少分成四组:插入后查找、同键覆盖旧值归还、按值相等的不同地址删除、失败路径资源释放。性能部分另行做同设备、同数据量、同线程数的基准;文档说“高性能”不等于任何业务上都快,更不能凭一个本地桌面模拟器就写出鸿蒙设备的提升百分比。

版本与验证边界

该能力从 API 26.0.0 开始,官方页面更新于 2026-09-04。将其接入应用时,CMake 需要查找并链接 fast_collection,然后用目标 SDK 的头文件确认类型和错误码,在 HarmonyOS 7.0 设备上验证函数返回值及内存行为。本文代码是基于官方签名的审查用节选;本机缺少 API 26.0.0 NDK,未完成真实编译、真机运行或性能测试。这一步完成前,不能宣称示例“开箱即跑”或“比标准容器快”。

find_library(lib_fast_collection NAMES fast_collection)
target_link_libraries(entry PRIVATE ${lib_fast_collection})

换上 API 26 工具链后,先让编译器检查头文件、回调函数签名和链接库,再用整数键依次跑“插入、同键覆盖、查找、不同地址同值删除、重复删除”五步。只对返回成功的输出参数做断言;每步记录地址和值,最后用目标环境可用的内存检测方式检查泄漏与二次释放。没有这组结果时,本文的结论只限于文档和所有权分析。

参考:华为开发者文档《Using HashMap to Manage Key-Value Data》:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/fast-hashmap ;《FAST Kit简介》:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/fast-introduction 。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐