OpenHarmony内存泄漏指南 - 解决问题(NAPI&JavaScript)
本系列文章旨在提供定位与解决OpenHarmony应用与子系统内存泄露的常见手段与思路,将会分成几个部分来讲解。首先我们需要掌握发现内存泄漏问题的工具与方法,以及判断是否可能存在泄漏。接着需要掌握定位泄漏问题的工具,以及抓取trace、分析trace,以确定是否有泄漏问题。如果发现问题的场景过于复杂,需要通过分解问题来简化场景。
本系列文章旨在提供定位与解决OpenHarmony应用与子系统内存泄露的常见手段与思路,将会分成几个部分来讲解。首先我们需要掌握发现内存泄漏问题的工具与方法,以及判断是否可能存在泄漏。接着需要掌握定位泄漏问题的工具,以及抓取trace、分析trace,以确定是否有泄漏问题。如果发现问题的场景过于复杂,需要通过分解问题来简化场景。最后根据trace来找到问题代码并尝试解决。
目录:
本篇提供了一些3.2 release内存泄漏的真实案例,旨在提供常见泄漏原因的解决办法。常见的泄漏问题主要分为Native代码泄漏、NAPI代码泄漏、JavaScript代码泄漏以及综合类问题。下面是NAPI与JavaScript代码中的常见泄漏场景。
NAPI
NAPI属于Native代码,其提供了一系列可以操作JavaScript对象的api,在使用时如果不注意,很容易造成Native与JavaScript内存的泄漏。
createDate
先来看一个简单的案例,trace显示通过NativeEngine创建JavaScript的Date对象,会造成Date对象无法被回收,相关napi代码如下:
NativeValue* ArkNativeEngineImpl::CreateDate(NativeEngine* engine, double value)
{
return ArkValueToNativeValue(static_cast<ArkNativeEngine*>(engine), DateRef::New(vm_, value));
}
DateRef::New(vm_, value)
函数的作用,简单的理解是会创建Js的Date对象。- 该函数创建的JS对象,会被HandleStorage持有,不会被gc回收,需要使用LocalScope才能被正常回收。
- LocalScope对象在被创建后,会记录后续创建的JS对象,并且在析构时,会将这些对象从HandleStorage中移除
因此这个案例的问题就是没有使用LocalScope,直接调用了DateRef::New
,导致Date对象无法被回收,只需要加上LocalScope即可:
NativeValue* ArkNativeEngineImpl::CreateDate(NativeEngine* engine, double value)
{
LocalScope scope(vm_);
return ArkValueToNativeValue(static_cast<ArkNativeEngine*>(engine), DateRef::New(vm_, value));
}
代码参考:https://gitee.com/openharmony/arkui_napi/commit/45b4913e4b581839f307f044c5d94792f8cc4d08
NapiAccountAMUserAuth
trace显示napi_create_async_work
创建的NativeAsyncWork对象泄漏,如下:
napi_value NapiAccountIAMUserAuth::GetProperty(napi_env env, napi_callback_info info)
{
napi_value result = nullptr;
GetPropertyContext *context = new (std::nothrow) GetPropertyContext(env);
...
std::unique_ptr<GetPropertyContext> contextPtr(context);
...
napi_value resourceName = nullptr;
NAPI_CALL(env, napi_create_string_utf8(env, "GetProperty", NAPI_AUTO_LENGTH, &resourceName));
NAPI_CALL(env, napi_create_async_work(env, nullptr, resourceName,
[](napi_env env, void *data) {
...
},
[](napi_env env, napi_status status, void *data) {
delete reinterpret_cast<GetPropertyContext *>(data);
},
reinterpret_cast<void *>(context), &context->work));
NAPI_CALL(env, napi_queue_async_work(env, context->work));
contextPtr.release();
return result;
}
这里可以查看一下napi_create_async_work
的源码。该函数主要通过NativeEngineInterface::CreateAsyncWork
来实现:
NativeAsyncWork* NativeEngineInterface::CreateAsyncWork(NativeEngine* engine, NativeValue* asyncResource,
NativeValue* asyncResourceName, NativeAsyncExecuteCallback execute, NativeAsyncCompleteCallback complete,
void* data)
{
(void)asyncResource;
(void)asyncResourceName;
char name[NAME_BUFFER_SIZE] = {0};
if (asyncResourceName != nullptr) {
auto nativeString = reinterpret_cast<NativeString*>(
asyncResourceName->GetInterface(NativeString::INTERFACE_ID));
size_t strLength = 0;
nativeString->GetCString(name, NAME_BUFFER_SIZE, &strLength);
}
return new NativeAsyncWork(engine, execute, complete, name, data);
}
可以看到,在函数最后直接通过new创建了一个NativeAsyncWork对象,但是在后续并没有delete。为此NAPI提供了napi_delete_async_work来专门释放NativeAsyncWork。代码修改如下:
napi_value NapiAccountIAMUserAuth::GetProperty(napi_env env, napi_callback_info info)
{
napi_value result = nullptr;
GetPropertyContext *context = new (std::nothrow) GetPropertyContext(env);
...
std::unique_ptr<GetPropertyContext> contextPtr(context);
...
napi_value resourceName = nullptr;
NAPI_CALL(env, napi_create_string_utf8(env, "GetProperty", NAPI_AUTO_LENGTH, &resourceName));
NAPI_CALL(env, napi_create_async_work(env, nullptr, resourceName,
[](napi_env env, void *data) {
...
},
[](napi_env env, napi_status status, void *data) {
GetPropertyContext* context = reinterpret_cast<GetPropertyContext *>(data);
napi_delete_async_work(env, context->work);
delete context;
},
reinterpret_cast<void *>(context), &context->work));
NAPI_CALL(env, napi_queue_async_work(env, context->work));
contextPtr.release();
return result;
}
详细代码参考:https://gitee.com/openharmony/account_os_account/pulls/1255/files
TCPSocket
该案例的场景是,在应用中反复调用TCPSocket.on
函数,会导致Native与JavaScript内存同步增长。trace显示EventListener类存在泄漏,泄漏的对象为NapiUtils::CreateReference
函数创建的NativeReference对象:
EventListener::EventListener(const EventListener &listener)
{
env_ = listener.env_;
type_ = listener.type_;
once_ = listener.once_;
asyncCallback_ = listener.asyncCallback_;
if (listener.callbackRef_ == nullptr) {
callbackRef_ = nullptr;
return;
}
napi_value callback = NapiUtils::GetReference(listener.env_, listener.callbackRef_);
callbackRef_ = NapiUtils::CreateReference(env_, callback);
}
在Napi中,一旦NativeReference对象泄漏,那么被其引用的JavaScript对象也一定会泄漏,即上述代码中的callback。从EventListener的构造函数来看,可能是某些情况下EventListener对象没有被释放造成的。
因此为EventListener的构造、析构均加上日志,看是否EventListener存在部分不释放的情况,结果是构造与析构的次数能完全对应上,因此问题应该出在别处。这时我们需要换个思路,来分析下TCPSocket.on
函数的实现:
void EventManager::AddListener(napi_env env, const std::string &type, napi_value callback, bool once,
bool asyncCallback)
{
auto it = std::remove_if(listeners_.begin(), listeners_.end(),
[type](const EventListener &listener) -> bool { return listener.MatchType(type); });
if (it != listeners_.end()) {
listeners_.erase(it, listeners_.end());
}
listeners_.emplace_back(EventListener(env, type, callback, once, asyncCallback));
}
初步一看,没有任何问题,listeners_在下一次调用on的时候会将以前重复的删除,不会保留在内存中。那么问题出在哪里?关键在于std::remove_if
。该函数会查找类别重复的监听器并删除。其会通过调用对象的operator=
重载来移动元素的位置:
EventListener &EventListener::operator=(const EventListener &listener)
{
env_ = listener.env_;
type_ = listener.type_;
once_ = listener.once_;
asyncCallback_ = listener.asyncCallback_;
if (listener.callbackRef_ == nullptr) {
callbackRef_ = nullptr;
return *this;
}
napi_value callback = NapiUtils::GetReference(listener.env_, listener.callbackRef_);
callbackRef_ = NapiUtils::CreateReference(env_, callback);
return *this;
}
可以看到,EventListener的=重载函数中,有调用CreateReference,很有可能是由此造成的。通过分析代码逻辑,确实是此处第二次创建了NativeReference但未释放第一次(构造函数中)创建的NativeReference造成的。代码修改如下:
EventListener &EventListener::operator=(const EventListener &listener)
{
env_ = listener.env_;
type_ = listener.type_;
once_ = listener.once_;
asyncCallback_ = listener.asyncCallback_;
if (callbackRef_ != nullptr) {
NapiUtils::DeleteReference(env_, callbackRef_);
}
if (listener.callbackRef_ == nullptr) {
callbackRef_ = nullptr;
return *this;
}
napi_value callback = NapiUtils::GetReference(listener.env_, listener.callbackRef_);
callbackRef_ = NapiUtils::CreateReference(env_, callback);
return *this;
}
代码参考:https://gitee.com/openharmony/communication_netstack/pulls/571/files
JavaScript
AppStorage.Link
接下来看看JavaScript语言的案例,trace显示如下代码会造成SynchedPropertyObjectTwoWay对象泄漏:
linkItemData() {
Log.showDebug(TAG, `linkItemData, keyId: ${this.keyId}`);
this.mItemData = AppStorage.Link('ControlCenter_' + this.keyId).get();
Log.showDebug(TAG, `linkItemData, mItemData: ${this.keyId} ${this.mItemData.label} ${this.mItemData.iconUrl}`);
}
代码非常普通,我们需要看一下AppStorage.Link
的实现,代码位于ace_engine下:
// frameworks/bridge/declarative_frontend/state_mgmt/src/lib/sdk/app_storage.ts
public static Link<T>(key: string, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> {
return AppStorage.GetOrCreate().link(key, linkUser, subscribersName);
}
Link函数会调用父类LocalStorage的link函数:
public link<T>(propName: string, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> | undefined {
var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName);
if (p == undefined) {
stateMgmtConsole.warn(`${this.constructor.name}: link: no property ${propName} error.`);
return undefined;
}
let linkResult = p.createLink(linkUser, propName);
linkResult.setInfo(subscribersName);
return linkResult;
}
createLink函数根据不同的数据类型有不同的实现,这里查看object的实现:
public createLink(subscribeOwner?: IPropertySubscriber,
linkPropName?: PropertyInfo): ObservedPropertyAbstract<T> {
return new SynchedPropertyObjectTwoWay(this, subscribeOwner, linkPropName);
}
SynchedPropertyObjectTwoWay继承自ObservedPropertyAbstract,其构造函数中:
constructor(subscribeMe?: IPropertySubscriber, info?: PropertyInfo) {
super();
...
SubscriberManager.Add(this);
...
}
通过SubscriberManager.Add
将自身存入一个全局map中,导致页面被销毁时,SynchedPropertyObjectTwoWay对象也不会被销毁。解决办法要么是在页面销毁时手动调用该对象的aboutToBeDeleted方法,要么将AppStorage.Link换成AppStorage.Get。
代码参考:https://gitee.com/openharmony/applications_systemui/pulls/345/files
@Prop
@Prop name: string;
编译后:
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
...
this.__name = new SynchedPropertySimpleOneWayPU(params.name, this, "name");
...
}
get name() {
return this.__name.get();
}
set name(newValue) {
this.__name.set(newValue);
}
查看SynchedPropertySimpleOneWayPU的构造函数源码:
constructor(source: ObservedPropertyAbstract<C> | C,
owningChildView: IPropertySubscriber,
thisPropertyName: PropertyInfo) {
super(owningChildView, thisPropertyName);
if (source && (typeof (source) === "object") && ("notifyHasChanged" in source) && ("subscribeMe" in source)) {
// code path for @(Local)StorageProp
this.source_ = source as ObservedPropertyAbstract<C>;
// subscribe to receive value change updates from LocalStorage source property
this.source_.subscribeMe(this);
} else {
// code path for @Prop
...
this.source_ = new ObservedPropertyObjectPU<C>(source as C, this, thisPropertyName);
}
// deep copy source Object and wrap it
this.setWrapperValue(this.source_.get());
}
可以发现,在@Prop的else分支内,创建了一个ObservedPropertyObjectPU对象,并赋值给了成员变量source_。查看ObservedPropertyObjectPU的继承链发现,其最终会继承自ObservedPropertyAbstract。在上一个案例中已经说明,所有继承自ObservedPropertyAbstract的对象都需要注意,在构造函数中会将this push到一个全局单例map中,因此需要手动调用对象的aboutToBeDeleted函数。
接下来我们就可以看看SynchedPropertySimpleOneWayPU的aboutToBeDeleted中是否调用了成员变量source_的aboutToBeDeleted函数:
aboutToBeDeleted() {
if (this.source_) {
this.source_.unlinkSuscriber(this.id__());
this.source_ = undefined;
}
super.aboutToBeDeleted();
}
可以看到source_的aboutToBeDeleted方法并未被调用,解决办法也很简单,在对象是由@Prop修饰的情况下,增加source_..aboutToBeDeleted()
。
具体代码可查看:https://gitee.com/openharmony/arkui_ace_engine/commit/8e2128bb5281edffeb2475e5aff2387806f5183e
更多推荐
所有评论(0)