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```