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.……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/294175.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

我们正迎来计算基因的巨大变革,即将到来的不仅是量子技术——

计算机是围绕逻辑构建的&#xff1a;利用电路执行数学运算。逻辑是围绕诸如加法器——这种将两个数字相加的基本电路&#xff0c;而构建的。 今天的微处理器和计算机历史初期的所有微处理器都是如此。这可以追溯到算盘&#xff0c;在某些基本层面上&#xff0c;这和你闪亮的游戏…

前端根据URL地址实现下载(txt,图片,word,xlsx,ppt)

前端根据URL地址实现下载&#xff08;txt&#xff0c;图片&#xff0c;word&#xff0c;xlsx&#xff0c;ppt&#xff09; 一、对于txt,图片类的二、对于word&#xff0c;xlsx&#xff0c;ppt类的1.a标签可以实现下载2. window.open&#xff08;&#xff09; 一、对于txt,图片类…

软件推荐:MobaXterm

介绍 MobaXterm 是远程计算的终极工具箱&#xff0c;它提供了几乎所有重要的远程网络工具&#xff0c;SSH、RDP、FTP、VNC&#xff0c;只要你能想到的&#xff0c;都可以在MobaXterm中找到。除了海量协议外&#xff0c;MobaXterm 还支持安装额外的插件来扩展其功能。 软件官网…

技术查漏补缺(1)Logback

一、下定义&#xff1a;Logback是一个开源的日志组件 二、Logback的maven <!--这个依赖直接包含了 logback-core 以及 slf4j-api的依赖--> <dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><v…

MySQL-数据库概述

数据库相关概念&#xff1a; 数据库(DateBase)简称DB,就是一个存储数据的仓库&#xff0c;数据有组织的进行存储。 数据库分为关系型数据库简称RDBMS和非关系型数据库 关系型数据库简称RDBMS:建立在关系模型的基础上&#xff0c;由多张相互连接的二维表组成的数据库.简单来说…

D6208双向直流马达驱动芯片 用于IPC产品,可兼容BA6208,噪声低 ,工作电源电压范围宽。

D6208 是一块单片双向马达驱动电路&#xff0c;它使用TTL电平的逻辑信号就能控制卡式录音机和其它电子设备中的双向马达。该电路由一个逻辑部分和一个功率输出部分组成。逻辑部分控制马达正、反转向及制动&#xff0c;功率输出部分根据逻辑控制能提供100mA&#xff08;典型值&a…

处理数组,将一维转二维

实现金刚区可滑动效果 由于后端返回的数据是 不符合项目需求&#xff0c;所以需要将它转为为二位数组 // 转为二维数组convertOneDimArray() {let arr [integral, kefu-ermai, coupon, gift, scan, pause-circle, wifi, email, integral,kefu-ermai, coupon, gift, scan, pau…

前端知识体系思维导图

绝大部分分类方法、专业术语&#xff0c;出自专业书籍&#xff0c;如《JavaScript 高级程序设计&#xff08;第 4 版&#xff09;》、《JavaScript 权威指南&#xff08;第 7 版&#xff09;》《CSS 权威指南&#xff08;第四版&#xff09;》、《HTML5 权威指南》、《计算机网…

Postman的七种断言方法+超时设置!

超时的设置 Settings->General->Request Timeout in ms(0 for infinity)&#xff1a;设置请求超时的时间,默认为0 1.postman断言介绍 postman在发送请求后&#xff0c;需要对返回的结果做判断&#xff0c;验证是否符合预期&#xff0c;如状态码、响应头、响应正文等信息…

关于一名资深Java程序员在移动端的进阶之路

今天呢&#xff0c;就借此机会&#xff0c;跟大家聊一聊我的个人职业经历吧&#xff01; 那年刚毕业 刚毕业时候&#xff0c;入职的第一家公司&#xff0c;进去后&#xff0c;说实话&#xff0c;没有太大成长吧&#xff01;基本就是让我做一些可有可无的边缘性的工作&#xff…

提升办公效率:掌握批量文件重命名的技巧

在日常生活和工作中&#xff0c;经常要处理大量的文件&#xff0c;如文档、图片、音频等。在这些情况下&#xff0c;会遇到要批量重命名文件的情况。如果一个一个地重命名&#xff0c;不仅耗时&#xff0c;而且效率低下。今天来讲解一些技巧通过批量重命名文件&#xff0c;从而…

【Python学习】Python学习1

目录 【Python学习】Python学习1 1.前言2.Python安装3.PyCharm安装4.PyCharm插件推荐5.参考 文章所属专区 Python学习 1.前言 Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python 由 Guido van Rossum 于 1989 年底发明&#xff0c;第一个公开发行版发…

go install后仍无法使用全局命令

如下图使用go install命令后人无法使用全局命令 发生这种情况是由于安装Go SDK时&#xff0c;不是一键安装&#xff0c;而是使用者自己区官网下载SDK后配置的环境变量。 使用iso或者应用商城下载就不会出现这种问题。 出现上述的问题是检查GOPATH的环境便利是否配置成功&…

【亲测有效】Win11 卸载MySQL5.7以及安装MySQL8.0.35

目录 一、卸载原来本地的mysql5.7 1.mysql服务部分 1.1停止mysql服务 1.2删除mysql服务 2.卸载 MySQL程序 3.残余文件的清理 3.1删除mysql安装的目录 3.2删除mysql数据存放的目录 3.3删除mysql自定义目录 4.清理注册表 5.删除环境变量配置 二、安装mysql8.0.35 1.…

算法31:针对算法30货币问题进行拓展 + 时间复杂度 + 空间复杂度优化--------从左往右尝试模型

在算法30中&#xff0c;我们说过从左往右尝试模型&#xff0c;口诀就是针对固定集合&#xff0c;值不同&#xff0c;就是讨论要和不要的累加和。 那么对于非固定集合&#xff0c;我们应该怎么做呢&#xff1f; 针对非固定集合&#xff0c;面值固定&#xff0c;张数无限。口诀…

Python中 的函数介绍

函数 在Python中,函数是用来执行某种功能的 函数定义 def function_name(参数列表):代码块[return val]比如 #---------------定义------------- def add_3(num):newnum num3return newnumnum 3 print(add_3(num))函数调用 def add_3(num):newnum num3return newnumnum …

vue3 - Element Plus 切换主题色及el-button hover颜色不生效的解决方法

vue3 - Element Plus 切换主题色及el-button hover颜色不生效的解决方法 GitHub Demo 地址 https://github.com/iotjin/jh-vue3-admin 在线预览 https://iotjin.github.io/jh-vue3-admin/ 如果您想要通过 js 控制 css 变量&#xff0c;可以这样做&#xff1a; // document.do…

【InnoDB数据存储结构】第3章节:区、段、碎片区和表空间

文章目录结构 区、段、碎片区和表空间 什么是区&#xff1f;什么是段&#xff1f;什么是碎片区&#xff1f;什么是表空间&#xff1f; 在上文 InooDB 存储行格式一文中已经大致讲述过&#xff0c;再来回顾一下&#xff0c;直接上图&#xff1a; 名词解释如下&#xff1a; 行…

go构建项目与打包

环境搭建 使用的组件及版本 operator-sdk v1.22.0go 1.20.0 linux/amd64git 1.8.3.1k8s 1.18.5docker 20.10.5 前期配置 安装Git yum install git安装docker yum install docker-ce安装go 官网下载 tar -C /usr/local/ -xvf go1.20.linux-amd64.tar.gz 环境配置 // 将go配置…

【tensorflowflutter】自己写个机器学习模型用在项目上?

背景 拍摄APP项目上线有一阵了&#xff0c;每天的拍摄数据呈现波动上升状态、业务方需要对数据进行加工&#xff0c;如果能有对未来的数据量的预测就好了 。 目标 在端侧展示拍摄数据可视化趋势图等、并能推断数据&#xff08;选择预测日期&#xff09; 简单实现个demo gif背…