前言
内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。
工作中,我们会对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 系列
- 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_ 系列事件重复添加
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,带带我,一起学习。