• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 内存泄漏相关问题汇总
2<!--Kit: NDK-->
3<!--Subsystem: arkcompiler-->
4<!--Owner: @xliu-huanwei; @shilei123; @huanghello-->
5<!--Designer: @shilei123-->
6<!--Tester: @kirl75; @zsw_zhushiwei-->
7<!--Adviser: @fang-jinxu-->
8
9## 当前是否有机制来检查是否有泄漏的napi_ref
10
11- 具体问题:napi_create_reference可以创建对js对象的引用,保持js对象不释放,正常来说使用完需要使用napi_delete_reference进行释放,但怕漏delete导致js对象内存泄漏,当前是否有机制来检查/测试是否有泄漏的napi_ref?
12- 检测方式:
13可使用 DevEco Studio(IDE)提供的 Allocation 工具进行检测。
14[基础内存分析:Allocation分析](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-session-allocations)
15napi_create_reference这个接口内部实现会new一个C++对象,因此,如果忘记使用napi_delete_reference接口,那这个new出来的C++对象也会泄漏,这时候就可以用Allocation工具来进行检测,这个工具会把未释放的对象的分配栈都打印出来,如果napi_ref泄漏了,可以在分配栈上看出来
16
17## napi开发过程中遇见内存泄漏问题要怎么定位解决
18
19点击按钮时内存增加,即使主动触发GC也无法回收。如何在Node-API开发过程中定位和解决内存泄漏问题?
20
21- 解决建议:
22需先了解Node-API生命周期机制,相关材料如下:
23[使用Node-API接口进行生命周期相关开发](use-napi-life-cycle.md)
24使用Node-API时导致内存泄漏的常见原因:
251. napi_value不在napi_handle_scope管理中,导致napi_value持有的ArkTS对象无法释放,该问题常见于直接使用uv_queue_work的场景中。解决方法是添加napi_open_handle_scope和napi_close_handle_scope接口。
26此类泄漏可通过snapshot分析定位原因,泄漏的ArkTS对象distance为1,即不知道被谁持有,这种情况下一般就是被native(napi_value是个指针,指向native持有者)持有了,且napi_value不在napi_handle_scope范围内,可参考[易错API的使用规范](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615)
272. 使用 `napi_create_reference` 为 ArkTS 对象创建了强引用(`initial_refcount` 参数大于 0),且一直未删除,导致 ArkTS 对象无法被回收。`napi_create_reference` 接口内部会创建一个 C++ 对象,因此这种泄漏通常表现为ArkTS对象与Native对象的双重泄漏。可以使用 Allocation 工具捕获Native对象泄漏栈,检查是否存在 `napi_create_reference` 相关的栈帧。
28[基础内存分析:Allocation分析](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-session-allocations)
29
303. 被其它存活的ArkTS对象持有时,使用snapshot查看泄漏对象的持有者。
31
32## napi_threadsafe_function内存泄漏应该如何处理
33
34`napi_threadsafe_function`(下文简称tsfn)在使用时,常常会调用 `napi_acquire_threadsafe_function` 来更改tsfn的引用计数,确保tsfn不会意外被释放。但在使用完成后,应该及时使用 `napi_tsfn_release` 模式调用 `napi_release_threadsafe_function` 方法,以确保在所有调用回调都执行完成后,其引用计数能回归到调用 `napi_acquire_threadsafe_function` 方法之前的水平。当其引用计数归为0时,tsfn才能正确的被释放。
35
36当env即将退出,但tsfn的引用计数未归零时,应使用 `napi_tsfn_abort` 模式调用 `napi_release_threadsafe_function` 方法,确保env释放后不再持有或使用tsfn。env退出后继续持有tsfn将导致未定义行为,可能引发崩溃。
37
38如下代码将展示通过注册 `env_cleanup` 钩子函数的方式,以确保在env退出后不再继续持有tsfn。
39
40```cpp
41//napi_init.cpp
42#include <hilog/log.h> // hilog, 输出日志, 需链接 libhilog_ndk.z.so
43#include <thread> // 创建线程
44#include <unistd.h> // 线程休眠
45
46// 定义输出日志的标签和域
47#undef LOG_DOMAIN
48#undef LOG_TAG
49#define LOG_DOMAIN 0x2342
50#define LOG_TAG "MY_TSFN_DEMO"
51
52/*
53  为构建一个 env 生命周期短于 native 生命周期的场景,
54  本示例需要使用worker, taskpool 或 napi_create_ark_runtime 等方法,
55  创建非主线程的ArkTS运行环境,并人为的提前结束掉该线程
56*/
57
58
59// 定义一个数据结构,模拟存储tsfn的场景
60class MyTsfnContext {
61public:
62// 因使用了Node-API方法, MyTsfnContext 应当只在ArkTS线程被构造
63MyTsfnContext(napi_env env, napi_value workName) {
64    // 注册env销毁钩子函数
65    napi_add_env_cleanup_hook(env, Cleanup, this);
66    // 创建线程安全函数
67    if (napi_create_threadsafe_function(env, nullptr, nullptr, workName, 1, 1, this,
68            TsfnFinalize, this, TsfnCallJs, &tsfn_) != napi_ok) {
69        OH_LOG_INFO(LOG_APP, "tsfn is created failed");
70        return;
71    };
72};
73
74~MyTsfnContext() { OH_LOG_INFO(LOG_APP, "MyTsfnContext is deconstructed"); };
75
76napi_threadsafe_function GetTsfn() {
77    std::unique_lock<std::mutex> lock(mutex_);
78    return tsfn_;
79}
80
81bool Acquire() {
82    if (GetTsfn() == nullptr) {
83        return false;
84    };
85    return (napi_acquire_threadsafe_function(GetTsfn()) == napi_ok);
86};
87
88bool Release() {
89    if (GetTsfn() == nullptr) {
90        return false;
91    };
92    return (napi_release_threadsafe_function(GetTsfn(), napi_tsfn_release) == napi_ok);
93};
94
95bool Call(void *data) {
96    if (GetTsfn() == nullptr) {
97        return false;
98    };
99    return (napi_call_threadsafe_function(GetTsfn(), data, napi_tsfn_blocking) == napi_ok);
100};
101
102private:
103// 保护多线程读写tsfn的准确性
104std::mutex mutex_;
105napi_threadsafe_function tsfn_ = nullptr;
106
107// napi_add_env_cleanup_hook 回调
108static void Cleanup(void *data) {
109    MyTsfnContext *that = reinterpret_cast<MyTsfnContext *>(data);
110    napi_threadsafe_function tsfn = that->GetTsfn();
111    std::unique_lock<std::mutex> lock(that->mutex_);
112    that->tsfn_ = nullptr;
113    lock.unlock();
114    OH_LOG_WARN(LOG_APP, "cleanup is called");
115    napi_release_threadsafe_function(tsfn, napi_tsfn_abort);
116};
117
118// tsfn 释放时的回调
119static void TsfnFinalize(napi_env env, void *data, void *hint) {
120    MyTsfnContext *ctx = reinterpret_cast<MyTsfnContext *>(data);
121    OH_LOG_INFO(LOG_APP, "tsfn is released");
122    napi_remove_env_cleanup_hook(env, MyTsfnContext::Cleanup, ctx);
123    // cleanup 提前释放线程安全函数, 为避免UAF, 将释放工作交给调用方
124    if (ctx->GetTsfn() != nullptr) {
125        OH_LOG_INFO(LOG_APP, "ctx is released");
126        delete ctx;
127    }
128};
129
130// tsfn 发送到 ArkTS 线程执行的回调
131static void TsfnCallJs(napi_env env, napi_value func, void *context, void *data) {
132    MyTsfnContext *ctx = reinterpret_cast<MyTsfnContext *>(context);
133    char *str = reinterpret_cast<char *>(data);
134    OH_LOG_INFO(LOG_APP, "tsfn is called, data is: \"%{public}s\"", str);
135    // 业务逻辑已省略
136};
137};
138
139// 该方法需注册到模块Index.d.ts, 注册名为 myTsfnDemo, 接口描述如下
140// export const myTsfnDemo: () => void;
141napi_value MyTsfnDemo(napi_env env, napi_callback_info info) {
142    OH_LOG_ERROR(LOG_APP, "MyTsfnDemo is called");
143    napi_value workName = nullptr;
144    napi_create_string_utf8(env, "MyTsfnWork", NAPI_AUTO_LENGTH, &workName);
145    MyTsfnContext *myContext = new MyTsfnContext(env, workName);
146    if (myContext->GetTsfn() == nullptr) {
147        OH_LOG_ERROR(LOG_APP, "failed to create tsfn");
148        delete myContext;
149        return nullptr;
150    };
151    char *data0 = new char[]{"Im call in ArkTS Thread"};
152    if (!myContext->Call(data0)) {
153        OH_LOG_INFO(LOG_APP, "call tsfn failed");
154    };
155
156    // 创建一个线程,模拟异步场景
157    std::thread(
158        [](MyTsfnContext *myCtx) {
159            if (!myCtx->Acquire()) {
160                OH_LOG_ERROR(LOG_APP, "acquire tsfn failed");
161                return;
162            };
163            char *data1 = new char[]{"Im call in std::thread"};
164            // 非必要操作, 仅用于异步流程tsfn仍有效
165            if (!myCtx->Call(data1)) {
166                OH_LOG_ERROR(LOG_APP, "call tsfn failed");
167            };
168            // 休眠 5s, 模拟耗时场景, env退出后, 异步任务仍未执行完成
169            sleep(5);
170            // 此时异步任务已执行完成, 但tsfn已被释放并置为 nullptr
171            char *data2 = new char[]{"Im call after work"};
172            if (!myCtx->Call(data2) && !myCtx->Release()) {
173                OH_LOG_ERROR(LOG_APP, "call and release tsfn failed");
174                delete myCtx;
175            }
176        },
177        myContext)
178        .detach();
179    return nullptr;
180};
181```
182
183以下内容为主线程逻辑,主要用于创建 worker 线程并通知其执行任务。
184
185```ts
186// 主线程 Index.ets
187import worker, { MessageEvents } from '@ohos.worker';
188
189const mWorker = new worker.ThreadWorker('../workers/Worker');
190mWorker.onmessage = (e: MessageEvents) => {
191    const action: string | undefined = e.data?.action;
192    if (action === 'kill') {
193        mWorker.terminate();
194    }
195}
196
197// 触发方式的注册已省略
198mWorker.postMessage({action: 'tsfn-demo'});
199
200```
201
202以下内容为Worker线程逻辑,主要用于触发Native任务。
203
204```ts
205// worker.ets
206import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker';
207import napiModule from 'libentry.so'; // libentry.so: Node-API 库的模块名称
208
209const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
210
211workerPort.onmessage = (e: MessageEvents) => {
212    const action: string | undefined = e.data?.action;
213    if (action === 'tsfn-demo') {
214        // 触发 C++ 层的 tsfn demo
215        napiModule.myTsfnDemo();
216        // 通知主线程结束 worker
217        workerPort.postMessage({action: 'kill'});
218    };
219}
220```