当前位置: 首页 > 科技观察

事件监听函数的内存泄露,还我!

时间:2023-03-12 13:27:49 科技观察

本文转载自微信公众号“云程序世界”,作者云世界。转载本文请联系云程序世界公众号。前言内存泄漏是一个非常严重的问题,但目前还没有非常有效的故障排除解决方案。该解决方案是有针对性的单点突破。在工作中,我们会注册窗口、DOM节点、WebSocket或者简单的事件中心等事件监听函数。如果我们添加它们,但不删除它们,就会导致内存泄漏。此类问题如何预警、收集、排查?本文为代码篇,主要讲使用和实现。源码和demo源码:事件分析vem[2]项目中有丰富的例子。核心功能我们解决问题的时间不外乎事前、事中和事后。我们主要在这里之前和之后。在添加事件监控功能之前,请先进行预警。添加事件监听功能后,在进行统计和理解功能之前,先了解四个特点:1.同一个事件监听功能下属对象事件监听必须一直注册在对应的对象上,比如下面的代码window,socket,和emitter都是事件监听函数的从属对象,window.addEventListener("resize",onResize)socket.on("message",onMessage);emitter.on("message",onMessage);2、同一个事件监听函数的类型比较容易理解,比如窗口的消息、resize等,Audio的播放等。3、同一个事件监听函数的内容这里要注意。事件监听函数选项是可选的,EventTarget系列有这些选项,其他系列没有。选项不一样,加删的时候可能结果不行。window.addEventListener("resize",onResize)//移除事件监听函数onResizefailedwindow.removeEventListener("resize",onResize,true)在添加预警事件监听函数之前,比较四个相同属性的事件监听函数,如有重复,报警。高危监控事件功能的核心功能。统计事件监听函数下属对象的所有事件信息,输出满足四同属性的事件监听函数。如果有数据输出,很大概率是内存泄漏。所有事件监控功能统计统计事件监控功能下属对象的所有事件信息,可以用来分析业务逻辑。查看您添加了多少事件。有没有不该救的?它们还存在吗?基本上使用初始化参数构建三个series:newEVM.ETargetEVM(options,et);//EventTargetseriesnewEVM.EventsEVM(options,et);//eventsseriesnewEVM.CEventsEVM(options,et);//component-emitterseries当然也可以继承BaseEvm,自定义一个新的series,因为上面三个series也都继承了BaseEvm。最重要的初始化参数是optionsoptions.isSameOptions是一个函数。主要用于确定事件监听函数的选项。options.isInWhiteList是一个函数。主要用来判断是否收集。options.maxContentLength是一个数字。可以限制统计时需要截取的函数内容的长度。EventTarget系列EventTarget[3]DOM节点+窗口+文档XMLHttpRequest继承自EventTargetNativeWebSocket继承自EventTarget其他继承自EventTarget的对象基本都是使用效果截图来自我对实际项目的分析,留言在window对象上重复添加,最多10events[4]Nodejs系列标准事件[5]基于events的MQTT[6]基于eve的库socket.ionts[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-emittersockets中。io-client(也就是socket.io的client)基本都是用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建立与目标对象关联且不影响其回收重写了EventTarget和EventEmitter两个系列的订阅和取消订阅相关方法,收集事件注册信息FinalizationRegistry监听目标回收,清除相关数据。函数对比,除了引用对比,还有bind后函数的内容对比,使用重写bind方法获取原方法代码内容代码结构代码基本结构如下:具体注意事项如下:evmCEvents.ts//components-emitter系列,继承自BaseEvmETarget.ts//EventTarget系列,继承自BaseEvmEvents.ts//事件系列,继承自BaseEvmBaseEvm.ts//核心逻辑类custom.d.tsEventEmitter.ts//简单事件中心EventsMap.ts//数据存储核心index.ts//入口文件types.ts//类型声明请util.ts//工具类核心实现EventsMap.ts负责数据存储和基本统计??数据存储结构:(双层Map)Map,Map[]>>();interfaceEventsMapItem{listener:WeakRef;options:O}内部结构大纲如下:方法很容易理解,你可能注意到有些方法后面是byTarget的话,那是因为内部使用了Map来存储,但是key类型是弱引用WeakRef。当我们添加和删除事件监听器时,传入的对象必须是一个普通的target对象,我们需要多走一步,通过target找到它对应的key,这就是byTarget想要表达的意思。下面列出一些方法的功能:getKeyFromTarget通过target对象获取key键获取所有弱引用key值addListener添加监听函数removeListener删除监听函数remove移除某个key的所有数据removeByTarget通过target删除某个key的所有数据removeEventsByTarget通过target删除某个key和某个事件类型的所有数据hasByTarget通过target查询是否存在某个keyhas是否存在某个keygetEventsObj获取某个target的所有事件信息BaseEVM的内部结构概述如下:核心实现是watch和cancel。继承BaseEVM,重写这两个方法,就可以得到一个新的系列。统计的两个核心方法是statistics和getExtremelyItems。下面列出一些方法的作用:innerAddCallback监听事件函数的添加并收集相关信息innerRemoveCallback监听事件函数的添加并清理相关信息checkAndProxy检查并执行代理restoreProperties恢复代理属性gc如果可能,执行垃圾收集#getListenerContentstatistics,获取函数内容时#getListenerInfo统计,获取函数信息,主要是名称和内容。statistics所有事件监听函数信息的统计。#getExtremelyListeners统计高危事件getExtremelyItems根据#getExtremelyListeners汇总高危事件信息。watch执行监听,需要重写的方法cancel取消监听,需要重写的方法removeByTarget清理一个对象的所有数据removeEventsByTarget清理某类对象的事件监听ETargetEVM我们已经提到了它有实际实现了三个系列之后,我们以ETargetEVM为例,看看如何通过继承和重写获取某系列事件监控的采集和统计。核心是重写watch和cancel,分别对应proxy和cancel相关proxycheckAndProxy为核心,封装了proxy过程,通过自定义第二个参数(函数)过滤数据。就这么简单constDEFAULT_OPTIONS:BaseEvmOptions={isInWhiteList:boolenFalse,isSameOptions:isSameETOptions}constADD_PROPERTIES=["addEventListener"];constREMOVE_PROPERTIES=["removeEventListener"];/***EVMforEventTarget*/exportdefaultclassETargetEVMextendsBaseEvm{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=(target,event,listener,options)=>{constfn=this.#getListenr(listener)if(!isFunction(fnasFunction)){return;}returnsuper.innerAddCallback(target,event,fnasFunction,options);}#innerRemoveCallback:EVMBaseEventListener=(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中,通过继承重写某些方法,满足不同的事件监控场景