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

前言

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

工作中,我们会对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 getExtremelyItemsconst 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 getExtremelyItemsconst 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

具体注释如下:

evmCEvents.ts // components-emitter系列,继承自 BaseEvmETarget.ts // EventTarget系列,继承自 BaseEvmEvents.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);}// removeEventListenerrp = 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.mzph.cn/news/791651.shtml

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

相关文章

count(“0“),split() ,sys.stdin.readline() ,matrix.append, input().strip()

目录 count() 方法主要用于计算一个序列(例如列表、元组或字符串)中某个元素出现的次数

开源软件技术社区方案

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

提高机器人系统稳定性:引入阻尼作为共振后的相位超前

在机器人关节中&#xff0c;引入阻尼作为共振后的相位超前&#xff0c;确实是一种提高系统稳定性的有效策略。机器人关节的振动和共振是影响其性能稳定性的关键因素&#xff0c;特别是在进行高速、高精度操作时。阻尼的引入能够显著减少这些不利因素&#xff0c;提升机器人的整…

c语言--枚举类型(声明、使用、优点)

目录 一、枚举类型的声明二、 枚举类型的优点三、 枚举类型的使用 一、枚举类型的声明 枚举顾名思义就是一一列举。 把可能的取值⼀⼀列举。 比如我们现实生活中&#xff1a; ⼀周的星期⼀到星期日是有限的7天&#xff0c;可以⼀⼀列举 性别有&#xff1a;男、女、保密&#x…

渐进式图片解决前端在页面中使用大图,图片体积过大导致页面出现白屏现象

1、演示 可以看到&#xff0c;图片还在拼命加载的时候&#xff0c; 页面上就已经有内容了 2、什么渐进式图片 图片一开始是模糊的&#xff0c;然后逐渐的开始变的清晰。如果页面上有一些大图&#xff0c;如果直接扔给浏览器的话那么图片的传输时间就会比较长&#xff0c;用户就…

软考高级架构师:校验码概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

【STM32】ST-LINK 下载时遇到的问题

如果出现“ST-Link USB communication error”ST-Link USB通信错误&#xff0c;则需要启动STM32 ST-LINK Utility&#xff0c;点击【ST-LINK】->【Firmaware】更新固件&#xff0c;然后打开Kei&#xff0c;点击魔术棒->->Debug->Settings&#xff0c;开到出现类似“…

电磁兼容故障整改-静电放电抗扰度不合格

电磁兼容故障整改-静电放电抗扰度不合格 ESD干扰机理不合格的主要原因 静电放电对设备的影响有下面几种安装滤波器也对静电放电有所帮助通过良好的搭接与接地防止ESD ESD干扰机理 ESD干扰电流其实是一种共模电流&#xff0c;因为ESD电压总是以参考接地板为基准的。如空击穿放电…

JavaScript变量作用域与解构赋值

在JavaScript中&#xff0c;用var申明的变量实际上是有作用域的。如果一个变量在函数体内部申明&#xff0c;则该变量的作用域为整个函数体&#xff0c;在函数体外不可引用该变量&#xff1a; ‘use strict’; function foo() { var x 1; x x 1; } x x 2; // ReferenceErr…

基于chatGLM在llama index上建立Text2SQL

基于chatGLM在llama index上建立Text2SQL 文中使用了chatglm的llm和embedding modle,利用的智谱的免费token Text2SQL Text2SQL其实就是从文本到SQL,也是NLP中的一种实践,这可以降低用户和数据库交互的门槛,无需懂SQL就可以拿到数据库数据。Text2SQL实现了从自然语言到SQL…

什么是中间人攻击?

中间人攻击&#xff08;Man-in-the-Middle Attack&#xff0c;简称MITM攻击&#xff09;是一种常见的网络安全威胁&#xff0c;攻击者在通信双方之间秘密拦截和转发消息&#xff0c;使双方误以为他们正在直接通信。这种攻击方式可以让攻击者监听、篡改通信内容&#xff0c;甚至…

Vue Mixin混入如何使用?三种方式【plugin插件、全局、局部】

插件写法&#xff1a; 1. 创建一个commonMixin.js文件 2. 文件导出对象 export default {install (Vue) {Vue.mixin({created() {console.log(commonMixin)},methods: {},computed: {}})} }3. 在main.js中引入 import commonMixin from ./common/commonMixin.js Vue.use(commo…

从redux的基本概念渐进式理解redux/toolkit的用法

概念 Redux toolkit是帮助提高redux开发效率的一个库 React-redux 是将React和Redux toolkit绑定在一起的一个库 action 是一个对象,里面有一个type属性 action creator是一个函数,这个函数可以返回上面的action对象。 reducer 是一个函数,接受两个参数(initilastate, acti…

微软邮箱被锁住,该如何解除限制?

ChatGPT账号是微软邮箱注册的&#xff0c;我们在登陆微软邮箱时&#xff0c;http://www.outlook.com&#xff0c;不需要开魔法工具&#xff0c;直接就可以登陆&#xff0c;否则会出现安全验证&#xff0c;限制登陆。 那么如果账号被锁&#xff0c;我们该如何解除限制呢&#x…

mongoDB 优化(2)索引

执行计划 语法&#xff1a; db.collection_xxx_t.find({"param":"xxxxxxx"}).explain(executionStats) 感觉这篇文章写得很好&#xff0c;可以参考 MongoDB——索引&#xff08;单索引&#xff0c;复合索引&#xff0c;索引创建、使用&#xff09;_mongo …

【Python】JSON与jsonpath

JSON JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写。 使用 JSON 函数需要导入 json 库&#xff1a;import json json.dumps json.dumps 用于将 Python 对象编码成 JSON 字符串。 语法 json.dumps(obj, skipkeysFalse, ensu…

asf是什么格式的文件?用手机怎么打开?

由于手机操作系统和硬件的限制&#xff0c;大部分手机并不直接支持asf文件的播放。因此&#xff0c;如果你想在手机上打开asf文件&#xff0c;你可能需要先将文件转换为手机支持的格式&#xff0c;如MP4。可以通过使用一些视频转换软件来实现&#xff0c;比如野葱视频转换器。 …

树状数组学习笔记

树状数组 拜读了大佬的讲解博文&#xff08;树状数组(详细分析应用)&#xff0c;看不懂打死我!&#xff09;&#xff0c;写一篇Python版的笔记巩固消化&#xff0c;附带蓝桥杯历年真题作为例题演示 一、作用 用于快速读取列表中 某个区间内所有元素的和 实现单点修改&#xff…

【VSCode】修改插件地址

不想放在原始C盘下面C:\Users\{用户}\.vscode\extensions为了后续存储空间考虑&#xff0c;想通过添加环境变量创建名为VSCODE_EXTENSIONS的环境变量&#xff0c;内容指向vs Code扩展所在目录即可 直接配置环境变量&#xff0c;不要在有空格的文件夹下面 变量名称&#xff1a;…

jvm的垃圾回收策略

垃圾回收回收的是什么 对象 类元数据&#xff1a; 类的元数据包括类的结构信息、方法信息、字段信息等&#xff0c;它们存储在方法区&#xff08;Metaspace&#xff09;中。当一个类不再被引用时&#xff0c;垃圾回收器会卸载这个类&#xff0c;并释放其在方法区中占用的内存空…