本系列文章旨在提供定位与解决OpenHarmony应用与子系统内存泄露的常见手段与思路,将会分成几个部分来讲解。首先我们需要掌握发现内存泄漏问题的工具与方法,以及判断是否可能存在泄漏。接着需要掌握定位泄漏问题的工具,以及抓取trace、分析trace,以确定是否有泄漏问题。如果发现问题的场景过于复杂,需要通过分解问题来简化场景。最后根据trace来找到问题代码并尝试解决。

目录:

  1. 发现问题
  2. 定位问题
  3. 分析Trace
  4. 分解问题
  5. 解决问题(Native)
  6. 解决问题(NAPI&JavaScript)
  7. 解决问题(综合)

本篇提供了一些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

接下来看看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并配合if-else动态创建销毁组件时,会造成ObservedPropertyObjectPU对象泄漏。@Prop在IDE编译时,会被编译为SynchedPropertyObjectOneWayPU/SynchedPropertySimpleOneWayPU,如:

@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

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐