本系列文章旨在提供定位与解决OpenHarmony应用与子系统内存泄露的常见手段与思路,将会分成几个部分来讲解。首先我们需要掌握发现内存泄漏问题的工具与方法,以及判断是否可能存在泄漏。接着需要掌握定位泄漏问题的工具,以及抓取trace、分析trace,以确定是否有泄漏问题。如果发现问题的场景过于复杂,需要通过分解问题来简化场景。最后根据trace来找到问题代码并尝试解决。
本篇提供了一些3.2 release内存泄漏的真实案例,旨在提供常见泄漏原因的解决办法。常见的泄漏问题主要分为Native代码泄漏、NAPI代码泄漏、JavaScript代码泄漏以及综合类问题。下面是综合类的案例,一般都是需要结合native、napi代码,与对应的JavaScript对象一起分析的类型。
OnJsRemoteRequest
该案例,是在进行rpc通信时,服务端的占用内容会不断增大,JavaScript代码如下:
class ServiceImpl extends rpc.RemoteObject {
constructor() {
super('test');
}
onRemoteMessageRequest(code, data, reply, option) {
reply.writeString('Hello World');
return true;
}
}
代码中,仅仅只是像reply写入了一个字符串。trace显示如下函数中存在泄漏:
int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{
uv_loop_s *loop = nullptr;
napi_get_uv_event_loop(env_, &loop);
uv_work_t *work = new(std::nothrow) uv_work_t;
work->data = reinterpret_cast<void *>(jsParam);
ZLOGI(LOG_LABEL, "start nv queue work loop");
uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {
ZLOGI(LOG_LABEL, "enter thread pool");
CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);
napi_value onRemoteRequest = nullptr;
napi_value thisVar = nullptr;
napi_get_reference_value(param->env, param->thisVarRef, &thisVar);
napi_get_named_property(param->env, thisVar, "onRemoteMessageRequest", &onRemoteRequest);
napi_valuetype type = napi_undefined;
napi_typeof(param->env, onRemoteRequest, &type);
bool isOnRemoteMessageRequest = true;
napi_value jsCode;
napi_create_uint32(param->env, param->code, &jsCode);
napi_value global = nullptr;
napi_get_global(param->env, &global);
napi_value jsOptionConstructor = nullptr;
napi_get_named_property(param->env, global, "IPCOptionConstructor_", &jsOptionConstructor);
napi_value jsOption;
size_t argc = 2;
napi_value flags = nullptr;
napi_create_int32(param->env, param->option->GetFlags(), &flags);
napi_value waittime = nullptr;
napi_create_int32(param->env, param->option->GetWaitTime(), &waittime);
napi_value argv[2] = { flags, waittime };
napi_new_instance(param->env, jsOptionConstructor, argc, argv, &jsOption);
napi_value jsParcelConstructor = nullptr;
if (isOnRemoteMessageRequest) {
napi_get_named_property(param->env, global, "IPCSequenceConstructor_", &jsParcelConstructor);
} else {
napi_get_named_property(param->env, global, "IPCParcelConstructor_", &jsParcelConstructor);
}
napi_value jsData;
napi_value dataParcel;
napi_create_object(param->env, &dataParcel);
napi_wrap(param->env, dataParcel, param->data,
[](napi_env env, void *data, void *hint) {}, nullptr, nullptr);
size_t argc3 = 1;
napi_value argv3[1] = { dataParcel };
napi_new_instance(param->env, jsParcelConstructor, argc3, argv3, &jsData);
napi_value jsReply;
napi_value replyParcel;
napi_create_object(param->env, &replyParcel);
napi_wrap(param->env, replyParcel, param->reply,
[](napi_env env, void *data, void *hint) {}, nullptr, nullptr);
size_t argc4 = 1;
napi_value argv4[1] = { replyParcel };
napi_new_instance(param->env, jsParcelConstructor, argc4, argv4, &jsReply);
// start to call onRemoteRequest
size_t argc2 = 4;
napi_value argv2[] = { jsCode, jsData, jsReply, jsOption };
napi_value return_val;
napi_status ret = napi_call_function(param->env, thisVar, onRemoteRequest, argc2, argv2, &return_val);
// Reset old calling pid, uid, device id
NAPI_RemoteObject_resetOldCallingInfo(param->env, oldCallingInfo);
do {
if (ret != napi_ok) {
ZLOGE(LOG_LABEL, "OnRemoteRequest got exception");
param->result = ERR_UNKNOWN_TRANSACTION;
break;
}
ZLOGD(LOG_LABEL, "call js onRemoteRequest done");
// Check whether return_val is Promise
bool returnIsPromise = false;//
napi_is_promise(param->env, return_val, &returnIsPromise);
if (!returnIsPromise) {
ZLOGD(LOG_LABEL, "onRemoteRequest is synchronous");
bool result = false;
napi_get_value_bool(param->env, return_val, &result);
if (!result) {
ZLOGE(LOG_LABEL, "OnRemoteRequest res:%{public}s", result ? "true" : "false");
param->result = ERR_UNKNOWN_TRANSACTION;
} else {
param->result = ERR_NONE;
}
break;
}
...
return;
} while (0);
std::unique_lock<std::mutex> lock(param->lockInfo->mutex);
param->lockInfo->ready = true;
param->lockInfo->condition.notify_all();
});
std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);
jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });
int ret = jsParam->result;
delete jsParam;
delete work;
return ret;
}
代码比较长(有做删减),大致为:
- 将下面代码通过uv_event_loop发送到js线程
- 获取onRemoteMessageRequest函数
- 创建code参数
- 构造option参数
- 构造data、reply参数
- 调用JavaScript代码中的onRemoteMessageRequest函数,并将code、data、reply、option等参数传入
- 获取onRemoteMessageRequest的返回值并唤醒线程
napi_handle_scope
首先,napi的各种函数如napi_create_object、napi_call_function等,在创建JavaScript Object或调用JavaScript函数的过程中,会创建各种NativeValue极其子类,如NativeObject、NativeFunction,还有NativeReference等
- NativeValue等对象是通过NativeChunk创建并管理其内存。
- NativeValue对象中,会将对应JS对象的作用域修改为global,也就是不会被gc回收。NativeValue被析构时,会将JS对象从global中移除。
- NativeChunk会通过new与delete管理所有的NativeValue对象。
- NativeValue对象不被回收,对应的JS对象就不会被回收。
- 通过NativeChunk创建的对象不会被主动回收,需要使用napi_handle_scope。
- napi_handle_scope的作用与LocalScope(createDate案例中)类似,只不过napi_handle_scope管理的napi的NativeValue系列对象,而LocalScope是管理Ark运行时中的JavaScript对象。
因此代码修改如下:
int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{
uv_loop_s *loop = nullptr;
napi_get_uv_event_loop(env_, &loop);
uv_work_t *work = new(std::nothrow) uv_work_t;
work->data = reinterpret_cast<void *>(jsParam);
ZLOGI(LOG_LABEL, "start nv queue work loop");
uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {
ZLOGI(LOG_LABEL, "enter thread pool");
CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);
napi_handle_scope scope = nullptr;
napi_open_handle_scope(param->env, &scope);
...
});
std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);
jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });
int ret = jsParam->result;
delete jsParam;
delete work;
return ret;
}
NAPI_MessageParcel
泄漏还未解决完,trace显示使用napi_new_instance构造data与reply时,有对象泄漏,即NAPI_MessageParcel对象。napi_new_instance函数在创建JavaScript对象时,会调用该类的构造函数,对应到NAPI_MessageParcel则是如下函数:
int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{
uv_loop_s *loop = nullptr;
napi_get_uv_event_loop(env_, &loop);
uv_work_t *work = new(std::nothrow) uv_work_t;
work->data = reinterpret_cast<void *>(jsParam);
ZLOGI(LOG_LABEL, "start nv queue work loop");
uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {
ZLOGI(LOG_LABEL, "enter thread pool");
CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);
napi_handle_scope scope = nullptr;
napi_open_handle_scope(param->env, &scope);
...
});
std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);
jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });
int ret = jsParam->result;
delete jsParam;
delete work;
return ret;
}
可以看到,在构造函数中,通过new关键字创建了NAPI_MessageParcel对象,但是在后续的OnJsRemoteRequest函数中,并未有delete的操作。那么问题就在于,客户端也有NAPI_MessageParcel对象,为何没有泄漏?
这里首先看看客户端的JavaScript代码:
let option = new rpc.MessageOption()
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
proxy.sendRequest(1, data, reply, option)
.then(function(result) {
})
.finally(() => {
data.reclaim()
reply.reclaim()
})
在promise的finally中,调用了reclaim函数。其native实现为:
napi_value NAPI_MessageParcel::JS_reclaim(napi_env env, napi_callback_info info)
{
size_t argc = 0;
napi_value thisVar = nullptr;
napi_get_cb_info(env, info, &argc, nullptr, &thisVar, nullptr);
NAPI_MessageParcel *napiParcel = nullptr;
napi_remove_wrap(env, thisVar, (void **)&napiParcel);
NAPI_ASSERT(env, napiParcel != nullptr, "napiParcel is null");
delete napiParcel;
napi_value result = nullptr;
napi_get_undefined(env, &result);
return result;
}
在JS_reclaim函数中,有通过delete释放NAPI_MessageParcel对象。客户端没有泄漏的原因就在于调用了JavaScript的reclaim函数。那服务端是否可以调用呢?可以,但是不能让开发者来修改代码,那样后续维护代价太大。这里需要区分是服务端还是客户端,来判断是否要通过native来释放内存,修改NAPI_MessageParcel::JS_constructor
函数如下:
napi_value NAPI_MessageParcel::JS_constructor(napi_env env, napi_callback_info info)
{
...
status = napi_wrap(
env, thisVar, messageParcel,
[](napi_env env, void *data, void *hint) {},
[](napi_env env, void *data, void *hint) {
NAPI_MessageParcel *messageParcel = reinterpret_cast<NAPI_MessageParcel *>(data);
if (!messageParcel->owner) {
delete messageParcel;
}
},
NAPI_ASSERT(env, status == napi_ok, "napi wrap message parcel failed");
return thisVar;
}
这样,服务端在JavaScript对象data、reply释放后,就能释放NAPI_MessageParcel对象的内存了。
CustomDialogController
在JavaScript中,使用CustomDialogController,会造成页面对象与CustomDialogController无法被销毁,代码如下:
private backDialogController: CustomDialogController = new CustomDialogController({
builder: SimpleComponent({})
});
上述代码会被编译成:
this.backDialogController = new CustomDialogController({
builder: () => {
let jsDialog = new SimpleComponent_1.default("7", this, {});
jsDialog.setController(this.backDialogController);
View.create(jsDialog);
}
}, this);
CustomDialogController对应的NAPI代码如下:
void JSCustomDialogController::JSBind(BindingTarget object)
{
JSClass<JSCustomDialogController>::Declare("CustomDialogController");
JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);
JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);
JSClass<JSCustomDialogController>::Bind(
object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}
new CustomDialogController的实现
JavaScript代码new CustomDialogController
会调用Native的JSCustomDialogController::ConstructorCallback
函数,代码如下:
void JSCustomDialogController::JSBind(BindingTarget object)
{
JSClass<JSCustomDialogController>::Declare("CustomDialogController");
JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);
JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);
JSClass<JSCustomDialogController>::Bind(
object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}
JSRef<JSObject> constructorArg = JSRef<JSObject>::Cast(info[0])
获取的是第一个入参,即带builder函数的对象。JSRef<JSObject> ownerObj = JSRef<JSObject>::Cast(info[1])
获取的是第二个入参,即传入的this,也就是应用的页面View对象。- 接下来通过new关键字创建JSCustomDialogController对象instance。
- 将第一个入参对象中的builder函数,使用instance对象的jsBuilderFunction_属性保存起来,供后续调用。该属性的类型是
RefPtr<JsFunction>
类型,会强持有对应的JavaScript对象。 - 将instance对象通过SetReturnValue设置返回值给JSCallbackInfo对象。
也就是说,JavaScript代码new CustomDialogController
会创建两个对象:
- JavaScript对象CustomDialogController
- Native对象JSCustomDialogController
这两个对象如何关联起来的呢?简单来说,在系统调用了JSCustomDialogController::ConstructorCallback
函数后,通过JSCallbackInfo获取返回值,即JSCustomDialogController对象的指针,并将其通过NativePointer的形式,与JavaScript对象CustomDialogController关联。
JSCustomDialogController对象合适被回收呢?在JSCustomDialogController::DestructorCallback
中,也就是JavaScript对象CustomDialogController销毁时:
void JSCustomDialogController::JSBind(BindingTarget object)
{
JSClass<JSCustomDialogController>::Declare("CustomDialogController");
JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);
JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);
JSClass<JSCustomDialogController>::Bind(
object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}
对象之间的关系
这里涉及到三个对象,分别是:
- View,ui页面对象,也就是ets代码中的this
- CustomDialogController,JavaScript对象
- JSCustomDialogController,native对象
三者关系如下:
- View的成员backDialogController持有了CustomDialogController
- CustomDialogController通过NativePointer与JSCustomDialogController关联
- CustomDialogController销毁时会回收JSCustomDialogController
箭头函数
目前看起来一切正常,只要View能被正常销毁,就不会造成泄漏。那么问题出在哪了呢?我们回顾一下编译后的new CustomDialogController
代码:
this.backDialogController = new CustomDialogController({
builder: () => {
...
}
}, this);
注意这里builder函数被编译为了箭头函数,箭头函数的this会指向最近的上层this,即View。这样问题就来了,JSCustomDialogController对象的jsBuilderFunction_持有了builder函数,builder函数持有了View引用,相当于JSCustomDialogController持有了View的引用。
又因为CustomDialogController与JSCustomDialogController关联,生命周期保持一致,间接的可以看做CustomDialogController持有了View。同时View的成员backDialogController持有了CustomDialogController,造成了循环引用,两个JavaScript对象都无法被销毁。
如何解决呢?很简单,只需要在页面的aboutToDisappear函数中,将backDialogController与View的引用解除即可:
aboutToDisappear() {
delete this.devicesDialogController
this.devicesDialogController = undefined
}
为了能让大家更好的学习鸿蒙 (Harmony OS) 开发技术,这边特意整理了《鸿蒙 (Harmony OS)开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05
《鸿蒙 (Harmony OS)开发学习手册》
入门必看:https://qr21.cn/FV7h05
- 应用开发导读(ArkTS)
- ……
HarmonyOS 概念:https://qr21.cn/FV7h05
- 系统定义
- 技术架构
- 技术特性
- 系统安全
如何快速入门?:https://qr21.cn/FV7h05
- 基本概念
- 构建第一个ArkTS应用
- 构建第一个JS应用
- ……
开发基础知识:https://qr21.cn/FV7h05
- 应用基础知识
- 配置文件
- 应用数据管理
- 应用安全管理
- 应用隐私保护
- 三方应用调用管控机制
- 资源分类与访问
- 学习ArkTS语言
- ……
基于ArkTS 开发:https://qr21.cn/FV7h05
1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……