【代码篇】事件监听函数的内存泄漏,都给我退散吧!

news2024/10/11 12:25:56

前言

内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。

工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?

本文是代码篇,主要讲使用和实现。

更多理论知识,请阅读理论篇 【方案篇】事件监听函数的内存泄漏,帮你搞定!

源码和demo

源码: 事件分析vem

项目内部有丰富的例子。

核心功能

我们解决问题的时机无非为 事前事中事后

我们这里主要是 事前事后

  • 事件监听函数添加前进行预警
  • 事件监听函数添加后进行统计

了解功能之前,先了解一下四同特性:

  1. 同一事件监听函数从属对象
    事件监听总是要注册到响应的对象上的, 比如下面代码的window, socket, emitter都是事件监听函数的从属对象、

    window.addEventListener("resize",onResize)
    
    socket.on("message", onMessage);
    
    emitter.on("message", onMessage);
    
  2. 同一事件监听函数类型
    这个比较好理解,比如window的 message, resize等,Audio的 play等等

  3. 同一事件监听函数内容
    这里注意一点,事件监听函数相同,分两种:

    • 函数引用相同
    • 函数内容相同
  4. 同一事件监听函数选项
    这个可选项,EventTarget系列有这些选项,其他系列没有。
    选项不同,添加和删除的时候结果就可能不通。

    window.addEventListener("resize",onResize)
    // 移除事件监听函数onResize失败
    window.removeEventListener("resize",onResize, true)
    

预警

事件监听函数添加前,比对四同属性的事件监听函数,如果有重复,进行报警。

统计高危监听事件函数

最核心的功能。
统计事件监听函数从属对象的所有事件信息,输出满足 四同属性 的事件监听函数。
如果有数据输出,极大概率,你内存泄漏了。

统计全部的事件监听函数

统计事件监听函数从属对象的所有事件信息, 可以用于分析业务逻辑。

一览你添加了多少事件, 是不是有些应该不存的,还存在呢?

基本使用

初始化参数

内置三个系列:

new EVM.ETargetEVM(options, et); // EventTarget系列
new EVM.EventsEVM(options, et); // events 系列
new EVM.CEventsEVM(options, et); // component-emitter系列

当然,你可以继承BaseEvm, 自定义出新的系列,因为上面的三个系列也都是继承BaseEvm而来。

最主要的初始化参数也就是 options

  • options.isSameOptions
    是一个函数。主要是用来判定事件监听函数的选项。
  • options.isInWhiteList
    是一个函数。主要用来判定是否收集。
  • options.maxContentLength
    是一个数字。你可以限定统计时,需要截取的函数内容的长度。

EventTarget系列

  • EventTarget
  • DOM节点 + windwow + document
  • XMLHttpRequest 其继承于 EventTarget
  • 原生的WebSocket 其继承于 EventTarget
  • 其他继承自EventTarget的对象
基本使用
<script src="http://127.0.0.1:8080/dist/evm.js?t=5"></script>
<script>
    const evm = new EVM.ETargetEVM({
        // 白名单,因为DOM事件的注册可能
        isInWhiteList(target, event, listener, options) {
            if (target === window && event !== "error") {
                return true;
            }
            return false;
         }
    });
    // 开始监听
    evm.watch();

    // 定期打印极有可能是重复注册的事件监听函数信息
    setInterval(async function () {
        // statistics getExtremelyItems
        const data = await evm.getExtremelyItems({ containsContent: true });
        console.log("evm:", data);
    }, 3000)
</script>
效果截图

截图来自我对实际项目的分析 , window对象上message消息的重复添加, 次数高达10
image.png

events 系列

  • Nodejs 标准的 events
  • MQTT 基于 events库
  • socket.io 基于 events库
基本使用
import { EventEmitter } from "events";

const evm = new win.EVM.EventsEVM(undefined, EventEmitter);
evm.watch();
setTimeout(async function () {
    // statistics getExtremelyItems
    const data = await evm.getExtremelyItems();
    console.log("evm:", data);
}, 5000)
效果截图

截图来自我对实际项目的分析 ,APP_ACT_COM_HIDE_ 系列事件重复添加
image.png

component-emitter 系列

  • component-emitter
  • socket.io-client(即socket.io的客户端)
基本使用
const Emitter = require('component-emitter');
const emitter = new Emitter();

const EVM = require('../../dist/evm');

const evm = new EVM.CEventsEVM(undefined, Emitter);
evm.watch();

// 其他代码

evm.getExtremelyItems()
    .then(function (res) {
        console.log("res:", res.length);
        res.forEach(r => {
            console.log(r.type, r.constructor, r.events);
        })
    })
效果截图

image.png

事件分析的基本思路

上篇总结的思路:

  1. WeakRef建立和target对象的关联,并不影响其回收
  2. 重写 EventTargetEventEmitter 两个系列的订阅和取消订阅的相关方法, 收集事件注册信息
  3. FinalizationRegistry 监听 target回收,并清除相关数据
  4. 函数比对,除了引用比对,还有内容比对
  5. 对于bind之后的函数,采用重写bind方法来获取原方法代码内容

代码结构

代码基本结构如下:

image.png

具体注释如下:

evm
    CEvents.ts // components-emitter系列,继承自 BaseEvm
    ETarget.ts // EventTarget系列,继承自 BaseEvm
    Events.ts  // events系列,继承自 BaseEvm
BaseEvm.ts  // 核心逻辑类
custom.d.ts 
EventEmitter.ts // 简单的事件中心
EventsMap.ts // 数据存储的核心
index.ts // 入口文件
types.ts // 类型申请
util.ts // 工具类

核心实现

EventsMap.ts

负责数据的存储和基本的统计。

数据存储结构:(双层Map)

 Map<WeakRef<Object>, Map<EventType, EventsMapItem<T>[]>>();
 
interface EventsMapItem<O = any> {
    listener: WeakRef<Function>;
    options: O
}

内部结构的大纲如下:
image.png

方法都很好理解,大家可能注意到了,有些方法后面跟着byTarget的字样,那是因为
其内部采用Map存储,但是key的类型是弱引用WeakRef

我们增加和删除事件监听的时候,传入的对象肯定是普通的target对象,需要多经过一个步骤,通过target来查到其对应的key,这就是byTarget要表达的意思。

还是罗列一些方法的作用:

  • getKeyFromTarget
    通过target对象获得键
  • keys
    获得所有弱引用的键值
  • addListener
    添加监听函数
  • removeListener
    删除监听函数
  • remove
    删除某个键的所有数据
  • removeByTarget
    通过target删除某个键的所有数据
  • removeEventsByTarget
    通过target删除某个键某个事件类型的所有数据
  • hasByTarget
    通过target查询是否有某个键
  • has
    是否有某个键
  • getEventsObj
    获得某个target的所有事件信息
  • hasListener
    某个target是否存在某个事件监听函数
  • getExtremelyItems
    获得高危的事件监听函数信息
  • get data
    获得数据

BaseEVM

内部结构的大纲如下:

image.png

核心实现就是watchcancel,继承BaseEVM并重写这两个方法,你就可以获得一个新的系列。

统计的两个核心方法就是 statisticsgetExtremelyItems

还是罗列一些方法的作用:

  • innerAddCallback
    监听事件函数的添加,并收集相关信息
  • innerRemoveCallback
    监听事件函数的添加,并清理相关信息
  • checkAndProxy
    检查并执行代理
  • restoreProperties
    恢复被代理属性
  • gc
    如果可以,执行垃圾回收
  • #getListenerContent
    统计时,获取函数内容
  • #getListenerInfo
    统计时,获得函数信息,主要是name和content。
  • statistics
    统计所有事件监听函数信息。
  • #getExtremelyListeners
    统计高危事件
  • getExtremelyItems
    基于#getExtremelyListeners汇总高危事件信息。
  • watch
    执行监听,需要被重写的方法
  • cancel
    取消监听,需要被重写的方法
  • removeByTarget
    清理某个对象的所有数据
  • removeEventsByTarget
    清理某个对象某类类型的事件监听

ETargetEVM

我们已经提到过,实际上已经实现了三个系列,我们就以ETargetEVM为例,看看怎么通过继承和重写获得对某个系列事件监听的收集和统计。

  1. 核心就是重写watch和cancel,分别对应了代理和取消相关代理
  2. checkAndProxy是核心,其封装了代理过程, 通过自定义第二个参数(函数),过滤数据。
  3. 就这么简单
const DEFAULT_OPTIONS: BaseEvmOptions = {
    isInWhiteList: boolenFalse,
    isSameOptions: isSameETOptions
}

const ADD_PROPERTIES = ["addEventListener"];
const REMOVE_PROPERTIES = ["removeEventListener"];

/**
 * EVM for EventTarget
 */
export default class ETargetEVM extends BaseEvm<TypeListenerOptions> {

    protected orgEt: any;
    protected rpList: {
        proxy: object;
        revoke: () => void;
    }[] = [];
    protected et: any;

    constructor(options: BaseEvmOptions = DEFAULT_OPTIONS, et: any = EventTarget) {
        super({
            ...DEFAULT_OPTIONS,
            ...options
        });

        if (et == null || !isObject(et.prototype)) {
            throw new Error("参数et的原型必须是一个有效的对象")
        }
        this.orgEt = { ...et };
        this.et = et;

    }

    #getListenr(listener: Function | ListenerWrapper) {
        if (typeof listener == "function") {
            return listener
        }
        return null;
    }

    #innerAddCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => {
        const fn = this.#getListenr(listener)
        if (!isFunction(fn as Function)) {
            return;
        }
        return super.innerAddCallback(target, event, fn as Function, options);
    }

    #innerRemoveCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => {
        const fn = this.#getListenr(listener)
        if (!isFunction(fn as Function)) {
            return;
        }
        return super.innerRemoveCallback(target, event, fn as Function, options);
    }


    watch() {
        super.watch();
        let rp;
        // addEventListener 
        rp = this.checkAndProxy(this.et.prototype, this.#innerAddCallback, ADD_PROPERTIES);
        if (rp !== null) {
            this.rpList.push(rp);
        }
        // removeEventListener
        rp = this.checkAndProxy(this.et.prototype, this.#innerRemoveCallback, REMOVE_PROPERTIES);
        if (rp !== null) {
            this.rpList.push(rp);
        }

        return () => this.cancel();
    }

    cancel() {
        super.cancel();
        this.restoreProperties(this.et.prototype, this.orgEt.prototype, ADD_PROPERTIES);
        this.restoreProperties(this.et.prototype, this.orgEt.prototype, REMOVE_PROPERTIES);
        this.rpList.forEach(rp => rp.revoke());
        this.rpList = [];
    }
}

总结

  • 单独设计了一套存储结构EventsMap
  • 把基础的逻辑封装在BaseEVM
  • 通过继承重写某些方法,从而可以满足不同的事件监场景。

写在最后

技术交流群请到 这里来。 或者添加我的微信 dirge-cloud,带带我,一起学习。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络爬虫:爬取假数据

网络爬虫&#xff1a;爬取假数据 文章目录 网络爬虫&#xff1a;爬取假数据前言一、项目介绍&#xff1a;二、项目来源&#xff1a;三、架构图&#xff1a;&#xff08;流程图&#xff09;四、使用了什么技术&#xff1a;&#xff08;知识点&#xff09;五、结果示意图&#xf…

网络安全 | 什么是单点登录SSO?

关注WX&#xff1a;CodingTechWork SSO-概念 单点登录 (SSO) 是一种身份认证方法&#xff0c;用户一次可通过一组登录凭证登入会话&#xff0c;在该次会话期间无需再次登录&#xff0c;即可安全访问多个相关的应用和服务。SSO 通常用于管理一些环境中的身份验证&#xff0c;包…

obs直播推流 + ffmpeg参数

OBS 启动参数设为 --startstreaming &#xff0c; 可以让它启动后自动开始直播 对应ffmpeg参数&#xff1a; echo off :loop ffmpeg -re -i a.mp4 -r 24 -c:v libx264 -preset ultrafast -profile:v baseline -g 24 -keyint_min 24 -x264-params nal-hrdcbr -b:v 2500k -minr…

线上研讨会 | 应对汽车毫米波雷达设计中的电磁挑战

智能汽车、新能源汽车最近几年一直是汽车行业关注的热点&#xff0c;随着5G技术越来越普及&#xff0c;汽车智能化发展将越来越迅速。从传统汽车到智能汽车&#xff0c;不是简单功能的增强&#xff0c;而是从单一功能的交通工具变成可移动的办公和娱乐空间&#xff0c;成为物联…

蓝桥杯python组真题练习1

目录 1.单词分析 2.成绩统计 3.门牌制作 4.卡片 5.跑步训练 6.蛇形填数 7.时间显示 1.单词分析 1.单词分析 - 蓝桥云课 (lanqiao.cn) s list(input()) maxx 0 for i in s:num s.count(i)if num>maxx:sm imaxx numif num maxx:if ord(sm)>ord(i):sm i print…

AcWing-孤独的照片

4261. 孤独的照片 - AcWing题库 所需知识&#xff1a;贡献法 整体思路&#xff1a;首先想到暴力枚举所有区间&#xff0c;判断每个区间内是否有一种牛的数量是一只&#xff08;提前用前缀和存放每个位置及以前的牛的数量&#xff09; C代码&#xff1a;&#xff08;过不了&a…

【科研笔记】知识星球不可选择内容爬虫

知识星球不可选择内容爬虫 1 背景2 实现3 拓展遗留问题1 背景 针对与知识星球中,电脑打开网页不可选择复制粘贴的问题,进行爬虫处理,获取网页的内容,并保存在本地 2 实现 需要下载python,和爬虫的第三方库selenium,可以查看博客中有关selenium的内容进行回顾。当前使用…

安装Pillow库的方法最终解答!_Python第三方库

安装Python第三方库Pillow 我的环境&#xff1a;Window10&#xff0c;Python3.7&#xff0c;Anaconda3&#xff0c;Pycharm2023.1.3 pillow库 Pillow库是一个非常强大的图像处理库。它提供了广泛的图像处理功能&#xff0c;让我们可以轻松地读取和保存图像、创建缩略图和合并到…

开源软件技术社区方案

开源软件技术社区是一个由开发者、贡献者、用户和维护者组成的共享平台&#xff0c;主要目的是打造技术、软件产品良性互动、开源技术安全可控的软件生态环境&#xff0c;实现可复用应用或服务的快速部署与使用、完成资源与能力的高度共享、促进社区成员的共建共赢&#xff0c;…

利用Python和Selenium实现定时任务爬虫

网络爬虫在信息获取、数据分析等领域发挥着重要作用&#xff0c;而定时爬虫则可以实现定期获取网站数据的功能&#xff0c;为用户提供持续更新的信息。在Python中&#xff0c;结合Selenium技术可以实现定时爬虫的功能&#xff0c;但如何设置和优化定时爬虫的执行时间是一个关键…

4.7 数组的读取和写入,type指令和一些杂项

4.7 数组的读取和写入&#xff0c;type指令和一些杂项 可以通过word ptr将db转为dw&#xff0c;然后按照dw的方式去存储数据 1. 段名也可以把其地址赋给变量 assume cs:codesg,ds:data,ss:stack data segmentdb 12,34dw 12,34db hello world data ends stack segmentdb 10 dup…

Android JNI基础

目录 一、JNI简介1.1 什么是JNI1.2 用途1.3 优点 二、初探JNI2.1 新建cpp\cmake2.2 build.gradle配置2.3 java层配置2.4 cmake和c 三、API详解3.1 JNI API3.1.1 数据类型3.1.2 方法 3.2 CMake脚本 四、再探JNI 一、JNI简介 1.1 什么是JNI JNI&#xff08;Java Native Interfa…

医疗器械网络安全 | 美国FDA审批程序和欧盟合格评定程序的区别

要进入美国与欧洲市场&#xff0c;均需要通过评定程序审批。 两者的审批流程核心区别在于&#xff1a;所有在美国上市流通的医疗器械产品必须经过FDA的审核认证&#xff0c;才能投放市场。而欧盟市场&#xff0c;医疗器械制造商只需要自证设备合规性&#xff0c;并有指定机构干…

5.2 通用代码,数组求和,拷贝数组,si配合di翻转数组

5.2 通用代码&#xff0c;数组求和&#xff0c;拷贝数组&#xff0c;si配合di翻转数组 1. 通用代码 通用代码类似于一个用汇编语言写程序的一个框架&#xff0c;也类似于c语言的头文件编写 assume cs:code,ds:data,ss:stack data segmentdata endsstack segmentstack endsco…

超文本传输协议HTTP

HTTP协议 在网络通信中&#xff0c;我们可以自己进行定制协议&#xff0c;但是也有许多已经十分成熟的应用层协议&#xff0c;比如我们下面说的HTTP协议。 HTTP协议简介 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;协议又叫做超文本传输协议&#xff0c;是一…

前端html+css+js常用总结快速入门

&#x1f525;博客主页&#xff1a; A_SHOWY&#x1f3a5;系列专栏&#xff1a;力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_ 学习前端全套所有技术性价比低下且容易忘记&#xff0c;先入门学会所有基础的语法&#xff08;cssjsheml&#xff09;&#xff…

LabVIEW太赫兹波扫描成像系统

LabVIEW太赫兹波扫描成像系统 随着科技的不断发展&#xff0c;太赫兹波成像技术因其非电离性、高穿透性和高分辨率等特点&#xff0c;在生物医学、材料质量无损检测以及公共安全等领域得到了广泛的应用。然而&#xff0c;在实际操作中&#xff0c;封闭性较高的信号采集软件限制…

使用ffmpeg将视频解码为帧时,图像质量很差

当使用ffmpeg库自带的ffmpeg.exe对对视频进行解帧或合并时&#xff0c;结果质量很差。导致这种原因的是在使用ffmpeg.exe指令进行解帧或合并时使用的是默认的视频码率&#xff1a;200kb/s。 如解帧指令&#xff1a; ffmpeg.exe -i 600600pixels.avi -r 2 -f image2 img/%03d.…

AI绘图:Stable Diffusion WEB UI 详细操作介绍:进阶-面部修复和调参

结合两篇文章完成了本地部署和基础操作,现在我们来介绍下进阶内容:面部修复,高清修复和调参区。 一:脸部修复 面部修复的适用在画真人、三次元的场景,特别是在画全身的时候 一般在画全身,由于脸部占比的空间比较小,那么绘制出来的效果就会比较差 1.面部修复 SD 支持…

日志服务 HarmonyOS NEXT 日志采集最佳实践

作者&#xff1a;高玉龙&#xff08;元泊&#xff09; 背景信息 随着数字化新时代的全面展开以及 5G 与物联网&#xff08;IoT&#xff09;技术的迅速普及&#xff0c;操作系统正面临前所未有的变革需求。在这个背景下&#xff0c;华为公司自主研发的鸿蒙操作系统&#xff08…