本文转载自微信公众号「云的程序世界」,作者云的世界。转载本文,请联系云程序世界微信官方账号。
前言
内存泄漏是一个非常严重的问题,但到目前为止还没有非常有效的调查计划,这是一个有针对性的单点突破。
在工作中,我们是对的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)预警
在添加事件监听函数之前,比较四个相同属性的事件监听函数,如有重复,报警。
统计高危监听事件函数
核心功能。
统计事件监控函数从属对象的所有事件信息,输出满足 四属性 的事件监控函数。如果有数据输出,很有可能你的内存会泄露。
统计所有事件监听函数
所有务逻辑进行分析。
你添加了多少事件,有些不应该存在,还存在吗?
基本使用
初始化参数
内置三个系列:
newEVM.ETargetEVM(options,et);//EventTarget系列newEVM.EventsEVM(options,et);//events系列newEVM.CEventsEVM(options,et);//component-emitter系列当然,你可以继承BaseEvm,自定义新系列,因为以上三个系列也是继承的BaseEvm而来。
最主要的初始化参数也就是 options
是函数。主要用于判断事件监控函数的选项。
是函数。主要用于判断是否收集。
这是一个数字。您可以限制统计时需要截取的函数内容的长度。
EventTarget系列
- EventTarget[3]
- DOM节点 windwow document
- XMLHttpRequest 继承于 EventTarget
- 原生的WebSocket 继承于 EventTarget
- 其他继承自EventTarget的对象
基本使用
<scriptsrc="http://127.0.0.1:8080/dist/evm.js?t=5"></script><script>constevm=newEVM.ETargetEVM({因为DOM可以注册事件isInWhiteList(target,event,listener,options){if(target===window&&event!=="error"){returntrue;}returnfalse;}});//开始监听evm.watch();///定期打印很可能是重复注册事件监听函数信息setInterval(asyncfunction(){//statisticsgetExtremelyItemsconstdata=awaitevm.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";constevm=newwin.EVM.EventsEVM(undefined,EventEmitter);evm.watch();setTimeout(asyncfunction(){//statisticsgetExtremelyItemsconstdata=awaitevm.getExtremelyItems();console.log("evm:",data);},5000)效果截图
截图来自我对实际项目的分析 ,APP_ACT_COM_HIDE_ 重复添加系列事件
component-emitter[8] 系列
- component-emitter
- socket.io-client(即socket.io的客户端)
基本使用
constEmitter=require('component-emitter');constemitter=newEmitter();constEVM=require('../../dist/evm');constevm=newEVM.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获取原始代码内容的方法
代码结构
代码的基本结构如下:
具体说明如下:
evmCEvents.ts//components-emitter系列,继承自己BaseEvmETarget.ts//EventTarget系列,继承自己BaseEvmEvents.ts//events系列,继承自己BaseEvmBaseEvm.ts//核心逻辑类custom.d.tsEventEmitter.ts//简单事件中心EventsMap.ts///数据存储的核心index.ts//入口文件types.ts//类型申请util.ts//工具类核心实现
EventsMap.ts
负责数据存储和基本统计。
数据存储结构:(双层Map)
Map<WeakRef<Object>,Map<EventType,EventsMapItem<T>[]>>();interfaceEventsMapItem<O=any>{listener:WeakRef<Function>;options:O}内部结构的大纲如下:
所有的方法都很容易理解,你可能会注意到,有些方法会跟着。byTarget因为 内部使用的字样Map存储,但是key类型为弱引用WeakRef。
当我们增加和删除事件监控时,引入的对象必须是普通的target对象需要通过一个步骤target找到对应的key,这就是byTarget表达的意思。
或者列出一些方法的作用:
通过target对象获得键
获得所有弱引用的键值
添加监听函数
删除监听函数
删除键中的所有数据
通过target删除键中的所有数据
通过target删除某个键某个事件类型的所有数据
通过target查询是否有键
有没有键?
获得某个target所有事件信息
某个target是否存在事件监听函数
获取高危事件监听函数信息
获得数据
BaseEVM
内部结构的大纲如下:
核心实现就是watch和cancel,继承BaseEVM并重写这两种方法,你可以得到一个新的系列。
统计的两个核心方法是 statistics 和 getExtremelyItems。
或者列出一些方法的作用:
添加监控事件函数,收集相关信息
监听添加事件函数并清理相关信息
检查并执行代理
恢复代理属性
如果可能的话,实施垃圾回收
统计时,获取函数内容
在统计中,获取函数信息主要是name和content。
对所有事件监听函数信息进行统计。
统计高危事件
基于#getExtremelyListeners汇总高危事件信息。
执行监控需要重写
取消监控,需要重写的方法
清理对象的所有数据
清理某一对象某一类型的事件监控
ETargetEVM
我们已经提到,实际上已经实现了三个系列,所以ETargetEVM例如,看看如何通过继承和重写收集和统计某一系列事件。
核心是重写watch和cancel,代理和取消相关代理对应
checkAndProxy它包装了代理过程,通过自定义第二个参数(函数)来过滤数据。
就这么简单
constDEFAULT_OPTIONS:BaseEvmOptions={isInWhiteList:boolenFalse,isSameOptions:isSameETOptions}constADD_PROPERTIES=["addEventListener"];constREMOVE_PROPERTIES=["removeEventListener"];/***EVMforEventTarget*/exportdefaultclassETargetEVMextendsBaseEvm<TypeListenerOptions>{protectedorgEt:any;protectedrpList:{proxy:object;revoke:()=>void;}[]=[];protectedet:any;constructor(options:BaseEvmOptions=DEFAULT_OPTIONS,et:any=EventTarget){super({...DEFAULT_OPTIONS,...options});if(et==null||!isObject(et.prototype)){thrownewError("参数et的原型必须是一个有效的对象")}this.orgEt={...et};this.et=et;}#getListenr(listener:Function|ListenerWrapper){if(typeoflistener=="function"){returnlistener}returnnull;}#innerAddCallback:EVMBaseEventListener<void,string>=(target,event,listener,options)=>{constfn=this.#getListenr(listener)if(!isFunction(fnasFunction)){return;}returnsuper.innerAddCallback(target,event,fnasFunction,options);}#innerRemoveCallback:EVMBaseEventListener<void,string>=(target,event,listener,options)=>{constfn=this.#getListenr(listener)if(!isFunction(fnasFunction)){return;}returnsuper.innerRemoveCallback(target,event,fnasFunction,options);}watch(){super.watch();letrp;//addEventListenerrp=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
- 通过继承和重写某些方法,可以满足不同的事件监管场景。