前言
内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。
工作中,我们会对window
, DOM
节点,WebSoket
, 或者单纯的事件中心
等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?
本文是代码篇,主要讲使用和实现。
更多理论知识,请阅读理论篇 【方案篇】事件监听函数的内存泄漏,帮你搞定!
源码和demo
源码: 事件分析vem
项目内部有丰富的例子。
核心功能
我们解决问题的时机
无非为 事前, 事中, 事后。
我们这里主要是 事前
和 事后
。
- 事件监听函数添加前进行预警
- 事件监听函数添加后进行统计
了解功能之前,先了解一下四同特性:
- 同一事件监听函数从属对象
事件监听总是要注册到响应的对象上的, 比如下面代码的window
,socket
,emitter
都是事件监听函数的从属对象、
window.addEventListener("resize",onResize) socket.on("message", onMessage); emitter.on("message", onMessage); 复制代码
- 同一事件监听函数类型
这个比较好理解,比如window的message
,resize
等,Audio的play
等等
- 同一事件监听函数内容这里注意一点,事件监听函数相同,分两种:
- 函数引用相同
- 函数内容相同
- 同一事件监听函数选项
这个可选项,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
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_ 系列事件重复添加
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); }) }) 复制代码
效果截图
事件分析的基本思路
上篇总结的思路:
WeakRef
建立和target
对象的关联,并不影响其回收- 重写
EventTarget
和EventEmitter
两个系列的订阅和取消订阅的相关方法, 收集事件注册信息 - FinalizationRegistry 监听
target
回收,并清除相关数据 - 函数比对,除了引用比对,还有内容比对
- 对于bind之后的函数,采用重写bind方法来获取原方法代码内容
代码结构
代码基本结构如下:
具体注释如下:
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 } 复制代码
内部结构的大纲如下:
方法都很好理解,大家可能注意到了,有些方法后面跟着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
内部结构的大纲如下:
核心实现就是watch
和cancel
,继承BaseEVM并重写这两个方法,你就可以获得一个新的系列。
统计的两个核心方法就是 statistics
和 getExtremelyItems
。
还是罗列一些方法的作用:
- innerAddCallback
监听事件函数的添加,并收集相关信息 - innerRemoveCallback
监听事件函数的添加,并清理相关信息 - checkAndProxy
检查并执行代理 - restoreProperties
恢复被代理属性 - gc
如果可以,执行垃圾回收 - #getListenerContent
统计时,获取函数内容 - #getListenerInfo
统计时,获得函数信息,主要是name和content。 statistics
统计所有事件监听函数信息。- #getExtremelyListeners
统计高危事件 getExtremelyItems
基于#getExtremelyListeners汇总高危事件信息。watch
执行监听,需要被重写的方法cancel
取消监听,需要被重写的方法- removeByTarget
清理某个对象的所有数据 - removeEventsByTarget
清理某个对象某类类型的事件监听
ETargetEVM
我们已经提到过,实际上已经实现了三个系列,我们就以ETargetEVM
为例,看看怎么通过继承和重写获得对某个系列事件监听的收集和统计。
- 核心就是重写watch和cancel,分别对应了代理和取消相关代理
checkAndProxy
是核心,其封装了代理过程, 通过自定义第二个参数(函数),过滤数据。- 就这么简单
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,带带我,一起学习。