OpenHarmony内存泄漏指南 - 解决问题(综合)

本系列文章旨在提供定位与解决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会创建两个对象:

  1. JavaScript对象CustomDialogController
  2. 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

  1. 应用开发导读(ArkTS)
  2. ......

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

如何快速入门?:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. 构建第一个JS应用
  4. ......

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ......

基于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.......

相关推荐
desssq2 小时前
嘉立创黄山派下载watch ui demo 教程(sf32)
ui·嵌入式·嘉立创·黄山派
哎呦薇4 小时前
一篇文章说明白web前端性能优化
性能优化
写不出来就跑路4 小时前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui
请叫我小蜜蜂同学4 小时前
【鸿蒙】鸿蒙操作系统发展综述
华为·harmonyos
HMS Core5 小时前
借助HarmonyOS SDK,《NBA巅峰对决》实现“分钟级启动”到“秒级进场”
华为·harmonyos
塞尔维亚大汉8 小时前
鸿蒙内核源码分析(文件句柄篇) | 你为什么叫句柄?
源码·harmonyos
别说我什么都不会8 小时前
【OpenHarmony】鸿蒙开发之FlexSearch
harmonyos
HarmonyOS小助手10 小时前
在鸿蒙中造梦的开发者,一边回答,一边前行
harmonyos·鸿蒙·harmonyos next·鸿蒙生态
HarmonyOS_SDK12 小时前
用AI重塑游戏体验:《诛仙2》携手HarmonyOS SDK实现性能与功耗双赢
harmonyos
别说我什么都不会12 小时前
【OpenHarmony】鸿蒙开发之epublib
harmonyos