本文转载自微信公众号「云的程序世界」,作者云的世界。转载本文请联系云的程序世界公众号。
前言
内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。
工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?
本文是代码篇,主要讲使用和实现。
源码和demo
源码:事件分析vem[2]
项目内部有丰富的例子。
核心功能
我们解决问题的时机无非为 事前, 事中, 事后。
我们这里主要是 事前 和 事后。
- 事件监听函数添加前进行预警
- 事件监听函数添加后进行统计
了解功能之前,先了解一下四同特性:
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[3]
- 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[4] 系列
- Nodejs 标准的 events[5]
- MQTT 基于 events[6]库
- socket.io 基于 events[7]库
基本使用
- 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[8] 系列
- 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
- 通过继承重写某些方法,从而可以满足不同的事件监场景。