尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

HarmonyOS 7 新特性实战(25):Native 强引用的受控持有与释放

发布时间:2026/9/29 20:08:15

资讯中心
01
ARTICLE

HarmonyOS 7 新特性实战(25):Native 强引用的受控持有与释放

HarmonyOS 7 新特性实战(25):Native 强引用的受控持有与释放
进入展厅、退出再进入内存持续升高。页面已经消失模型对象却可能仍被 Native 回调或全局句柄持有。此时只检查 ArkTS 页面是否置空往往找不到最后一条引用链。DevEco Profiler 的跨语言分析能力可以帮助观察 Native 与 ArkTS 之间的持有关系。实验应从一条可重复操作路径开始先找到谁拥有对象再修复释放时机最后用相同路径比较。增长不一定就是泄漏图片缓存、分配器保留内存和首次加载都会造成曲线上升。真正值得调查的是重复进入退出后已经无业务用途的对象仍被保留并形成可解释的持有路径。先规定测试次数、每次等待时间和结束条件区分首次预热与后续循环。仅截取两个不同时刻的总内存值无法判断对象是否仍然可达。故障样本只放在实验模块可以设计一个 Native 回调注册后未解除的样本让它持有页面传入的对象。实验对象应很小且可控不在生产路径中故意制造大规模泄漏。// 伪代码说明所有权不对应某个完整 N-API 实现。 class CallbackOwner { public: void Attach(PageCallback callback); void Detach(); void Close(); private: bool closed false; // 注册回调持有页面对象Detach 必须解除该引用。 };对真实 N-API 实现需要分别管理引用、异步任务和回调生命周期。页面对象置空不会自动解除所有 Native 持有销毁 Native 对象也必须遵守线程和环境有效性要求。从存活对象追到持有者固定执行进入页面、注册回调、触发一次操作、退出页面。采集后查找仍存活的目标对象沿引用链确认 Native 分配栈与关联句柄区分 LocalHandle、GlobalHandle 或其他资源的实际情况。工具版本和支持对象范围影响结果展示应保存版本信息和采集配置。没有出现某条关联不代表它不存在也可能是该版本或采集方式不覆盖。按所有权修复释放顺序合理顺序通常是停止新任务、解除回调、等待在途工作安全结束再释放持有对象的资源。具体次序依据接口合同调整不能在工作线程仍使用回调时直接删除引用。页面离开 → 会话停止接收请求 → 取消/等待在途任务 → 解除Native回调 → 释放引用 → 销毁所属资源失败路径与成功路径都要执行清理。初始化完成一半就抛错时只释放已经成功创建的对象重复关闭应保持幂等不引入双重释放。修复后重放同一操作序列对照包使用同一设备、同一素材和同样次数。比较目标对象存活数量、引用链是否消失与内存趋势。对象释放正确而进程内存没有立刻下降可能来自分配器行为应结合对象证据解释。不要为了让曲线下降而每次退出清空全局业务缓存。那会改变性能与功能掩盖实际的所有权问题。修复应针对泄漏资源不影响收藏和阅读记录。将结果沉淀为资源合同最小 Demo 需要故障版与修复版文章展示持有链、关键释放变化和同条件复验结果。截图应能辨认目标对象与分配来源不能只放一个泛内存曲线。后续把同样规则应用到模型、播放器和图片处理服务谁创建、谁关闭、异步结果如何失效、回调由谁解除。一次清晰的跨语言实验可以减少多个功能中重复出现的生命周期错误。释放正确性与曲线回落分别下结论修复后观察可以得出的结论目标引用链消失对象不再存活该持有关系已解除进程内存暂未回落还需区分分配器保留与其他对象多轮仍增加同类存活对象继续查找漏掉的创建或清理路径强制触发清理后截取最低点容易掩盖真实生命周期应保持故障包与修复包采集条件一致。实施先构造一条小对象保留路径再取得分配与持有证据最后修改所有权并重放。增加多种泄漏样本会扩大排查范围首篇只保留一个可解释问题。参考Profiler 跨语言内存分析官方介绍、DevEco 工具文档入口。强引用、外部内存与释放入口的归属实验模块通过 napi_create_reference 创建计数为 1 的强引用。每个对象带有 8 KiB 的 ArrayBuffer一次只创建一个对象最多持有 32 个避免为了观察泄漏无限增长内存。引用保存在模块实例状态中releaseAll 逐一调用 napi_delete_reference释放失败时保留待释放引用允许后续重试。重复释放空集合返回零环境销毁钩子执行兜底清理。页面退出默认释放实验引用。#include napi/native_api.h #include vector struct State { napi_env env; std::vectornapi_ref refs; }; static napi_value Fail(napi_env env, const char* message) { napi_throw_error(env, nullptr, message); return nullptr; } static napi_value Number(napi_env env, size_t value) { napi_value result nullptr; if (napi_create_uint32(env, static_castuint32_t(value), result) ! napi_ok) return Fail(env, number creation failed); return result; } static napi_value Retain(napi_env env, napi_callback_info info) { void* data nullptr; napi_value args[1]; size_t argc 1; if (napi_get_cb_info(env, info, argc, args, nullptr, data) ! napi_ok || argc ! 1) return Fail(env, one object required); auto* state static_castState*(data); napi_valuetype type; if (napi_typeof(env, args[0], type) ! napi_ok || type ! napi_object) return Fail(env, object required); if (state-refs.size() 32) return Fail(env, lab reference cap reached); napi_ref ref nullptr; if (napi_create_reference(env, args[0], 1, ref) ! napi_ok) return Fail(env, create reference failed); state-refs.push_back(ref); return Number(env, state-refs.size()); } static napi_value Count(napi_env env, napi_callback_info info) { void* data nullptr; if (napi_get_cb_info(env, info, nullptr, nullptr, nullptr, data) ! napi_ok) return Fail(env, callback read failed); return Number(env, static_castState*(data)-refs.size()); } static napi_value Release(napi_env env, napi_callback_info info) { void* data nullptr; if (napi_get_cb_info(env, info, nullptr, nullptr, nullptr, data) ! napi_ok) return Fail(env, callback read failed); auto* state static_castState*(data); while (!state-refs.empty()) { if (napi_delete_reference(env, state-refs.back()) ! napi_ok) return Fail(env, delete reference failed); state-refs.pop_back(); } return Number(env, 0); } static void Cleanup(void* data) { auto* state static_castState*(data); for (napi_ref ref : state-refs) napi_delete_reference(state-env, ref); delete state; } static napi_value Init(napi_env env, napi_value exports) { auto* state new State{env, {}}; state-refs.reserve(32); if (napi_add_env_cleanup_hook(env, Cleanup, state) ! napi_ok) { delete state; return Fail(env, cleanup registration failed); } napi_property_descriptor properties[] { {retain, nullptr, Retain, nullptr, nullptr, nullptr, napi_default, state}, {releaseAll, nullptr, Release, nullptr, nullptr, nullptr, napi_default, state}, {count, nullptr, Count, nullptr, nullptr, nullptr, napi_default, state} }; if (napi_define_properties(env, exports, 3, properties) ! napi_ok) return Fail(env, export registration failed); return exports; } static napi_module module {1, 0, nullptr, Init, reference_lab, nullptr, {0}}; extern C __attribute__((constructor)) void RegisterReferenceLab() { napi_module_register(module); }ArkTS 侧保留与释放的调用如下每次显式操作仅创建一个实验对象classRefPayload{label:stringarticlelab_retained_object;buffer:ArrayBuffernewArrayBuffer(8192);}constheld:numberreferenceLab.retain(newRefPayload());constremaining:numberreferenceLab.releaseAll();把释放动作放进真实页面销毁路径只把应用退到后台再次打开页面可能仍留在路由栈中。这个动作无法触发预期的组件销毁。实验增加了一个独立页面一次持有四个 8 KiB 对象再用 replaceUrl 将它替换为结果页。销毁回调负责释放引用结果页负责重新读取 Native 计数。aboutToDisappear():void{constremaining:numberreferenceLab.releaseAll();AppStorage.setOrCreate(referenceLifecycleRemaining,remaining);AppStorage.setOrCreate(referenceLifecycleDisposed,true);}这里的 releaseAll 管理的是实验模块中唯一一组引用。生产应用若有多个页面共享 Native 会话应按所有者或句柄释放不能照搬一个全局清空方法否则可能释放另一个页面仍在使用的资源。在 HarmonyOS 7.0.0.106、API 26 模拟器上进入时计数为 0持有后为 4销毁完成后再次读取为 0。结果页首次 onPageShow 可能发生在旧页 aboutToDisappear 之前因此界面保留“读取销毁与引用状态”按钮让回读动作发生在页面切换完成后。这个实验验证的是应用已经删除自己持有的强引用。对象是否还有其他根引用、何时被 GC 回收以及 RSS 是否下降需要分别观察。三阶段快照如何安排对照顺序应固定为“持有前→持有后→释放后”。每个阶段只发起一次采集等待成功或失败后才允许下一步。服务层也要限制并发避免两个页面同时触发昂贵的堆导出。阶段页面动作与引用计数分析时要回答的问题baseline进入独立页面计数 0预热后同类对象的基线数量是多少held持有四个对象计数 4新对象是否能追溯到实验创建的 Native 强引用released销毁旧页并回读计数 0采集前 GC 后对象是否消失若仍存在谁还在持有constsourceawaithidebug.dumpJsRawHeapData(true,true);constdestination${filesDir}/refs_${phase}_${Date.now()}.rawheap;awaitfileIo.copyFile(source,destination);constsizefileIo.statSync(destination).size;if(size0){thrownewError(empty raw heap snapshot);}调用外围使用 try/finally 恢复采集状态并将失败原因显示在页面上。该接口返回 rawheap 路径后续按官方转换工具说明处理再在分析工具中观察对象与引用关系。实验每次请求 GC 并清理 nodeId 缓存比较时应依据对象类型与引用关系不能假定三份快照中的节点 ID 保持一致。处理完毕后清理实验快照避免副本累积。同步调用 dumpJsHeapData 会阻塞调用线程。本例首次在 UI 线程采集触发了 THREAD_BLOCK_6S因此改为异步导出。6 GiB 和 32 GiB 数据盘均返回 11400110后者日志报告剩余 31,803,654,144 字节。问题在于检查的是剩余空间OpenHarmony 官方实现的 CheckDumpDiskSpace要求应用 FILE 目录所在文件系统剩余空间严格大于 30 GiB即 32,212,254,720 字节。32 GiB 总容量扣除已用空间后仍差约 389.7 MiB。保留旧实例备份后将实验实例初始化为 64 GiB 数据盘df 可用空间约 57 G。重新安装相同 HAP三个阶段均成功导出。开源实现与商业镜像不能直接视为同一版本但这次容量对照与源码门槛相符排查时应同时核对剩余字节数与接口日志。ArkTS 原始堆应使用 SDK 中的 rawheap_translator或按工具支持范围直接导入 Profiler。js_rawheap_translator 面向 ArkWeb/JSVM选择工具时要确认虚拟机来源不能仅凭相同的 .rawheap 扩展名混用。从三份快照得到对象级证据在同一应用进程中依次采集 baseline、held、released使用 SDK 的 rawheap_translator 转成 heapsnapshot得到以下结果阶段原始快照字节数LifecyclePayload 实例数持有关系baseline14,986,3950没有目标实例held14,995,0594四个实例均可追到 GlobalHandleRootreleased15,013,9310采集时已没有目标实例统计时限定对象类型为 object、类名来自 ReferenceLifecyclePage 中的 LifecyclePayload并要求拥有 label 字符串属性和指向 ArrayBuffer 的 buffer 属性。这能排除同名构造函数与原型。此次快照中的字符串内容为空不能靠搜索标签文本识别对象。held 快照中GlobalHandleRoot[4248] 通过编号 42164219 的四条 element 边分别持有这四个实例。编号仅属于该快照不应跨文件关联。实例自身 self_size 为 80 字节不包含所引用的 8 KiB 缓冲区不能把它当成对象的完整内存占用。释放后目标实例由 4 变为 0说明采集时这组对象已经不在堆中。与此同时整个快照的节点数从 138,813 增至 139,047文件也略大界面状态和采集过程仍会产生其他对象因此不能用整个文件大小判断这组引用是否释放。这三份快照的 trace_function_count 均为 0无法提供分配栈。本实验完成了对象数量和全局句柄持有链对照Native 分配栈还需使用支持该采集信息的 Profiler 配置单独验证。这份证据已经说明什么尚未说明什么观察到的事实可以写入的结论不能据此推导的结论baseline、held、released 的目标实例为 0→4→0实验模块创建并解除了一组可识别的 Native 强引用进程 RSS 必然立刻回落held 的 4 条路径均从GlobalHandleRoot到目标实例持有阶段存在全局句柄根链释放后目标实例消失已取得 Native 分配栈三份快照trace_function_count0本次 rawheap 没有可用的分配调用栈不存在其他业务资源泄漏DevEco Profiler 插件已在 D 盘独立配置下加载过但其ArkHeapSnapshotProcessor.startTakeHeapSnapshot要求设备键、PID 与实时会话 ID它不能把这里已经导出的离线 rawheap 直接当作实时采集结果。现有验收覆盖对象数量、根链与释放后消失Profiler 工具窗口中的实时采集、Native 分配栈和 RSS 趋势留到下一阶段不能用一张堆快照代替这三项证据。查看官方内存泄漏检测说明时应把对象保留、Native 引用释放和进程内存曲线分开记录引用归零后如果同类对象仍持续累积继续沿引用链查找其他持有者如果对象已回收而 RSS 暂未下降则再分析分配器与缓存行为。当前包的真机回归2026-09-23 在 HBN-AL80API 26执行 baseline → 持有 4 个对象 → held → 销毁页。baseline rawheap 成功导出大小为 14,703,355 B。首次回归发现replaceUrl后仅依赖aboutToDisappear没有完成释放页面回读仍为 4因此把释放操作移到跳转前显式执行离页钩子只作为幂等兜底。重新构建、签名、安装后复测结果为销毁回调true回调释放后0当前引用0。这验证当前实验页的引用计数释放闭环和 rawheap 导出可用。截至9月23日尚未把当日导出的 rawheap 转换并重做对象链统计也没有取得 Profiler 实时窗口或 Native 分配栈所以这些更强结论仍保持未验收。9月24日补齐当前三阶段对象链在同一API 26真机重新采集并转换三份rawheapLifecyclePayload对象数为0→4→0。持有阶段四条非弱element边来自GlobalHandleRoot[4248]编号42154218释放后目标实例消失。原始文件分别为14,704,171、14,711,483、14,728,715字节整份堆的节点总数仍可能增加不能用文件变大判断释放失败。三份原件已独立归档并校验哈希详见采集与复算记录、原件清单和对象分析结果。这些材料补齐了当前包的对象级证据但traceFunctionCount仍全部为0Profiler实时界面、Native分配栈和RSS趋势没有对应结果因此结论只覆盖受控对象的持有与释放。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。