事件監(jiān)聽函數(shù)的內(nèi)存泄漏,都給我退散吧!
本文轉(zhuǎn)載自微信公眾號「云的程序世界」,作者云的世界。轉(zhuǎn)載本文請聯(lián)系云的程序世界公眾號。
前言
內(nèi)存泄漏是個很嚴肅的問題,可是迄今也沒有一個非常有效的排查方案,本方案就是針對性的單點突破。
工作中,我們會對window, DOM節(jié)點,WebSoket, 或者單純的事件中心等注冊事件監(jiān)聽函數(shù), 添加了,沒有移除,就會導致內(nèi)存泄漏,如何預(yù)警,收集,排查這種問題呢?
本文是代碼篇,主要講使用和實現(xiàn)。
源碼和demo
源碼:事件分析vem[2]
項目內(nèi)部有豐富的例子。
核心功能
我們解決問題的時機無非為 事前, 事中, 事后。
我們這里主要是 事前 和 事后。
- 事件監(jiān)聽函數(shù)添加前進行預(yù)警
- 事件監(jiān)聽函數(shù)添加后進行統(tǒng)計
了解功能之前,先了解一下四同特性:
1.同一事件監(jiān)聽函數(shù)從屬對象
事件監(jiān)聽總是要注冊到響應(yīng)的對象上的, 比如下面代碼的window, socket, emitter都是事件監(jiān)聽函數(shù)的從屬對象、
- window.addEventListener("resize",onResize)
- socket.on("message", onMessage);
- emitter.on("message", onMessage);
2.同一事件監(jiān)聽函數(shù)類型
這個比較好理解,比如window的 message, resize等,Audio的 play等等
3.同一事件監(jiān)聽函數(shù)內(nèi)容
這里注意一點,事件監(jiān)聽函數(shù)相同,分兩種:
- 函數(shù)引用相同
- 函數(shù)內(nèi)容相同
4.同一事件監(jiān)聽函數(shù)選項
這個可選項,EventTarget系列有這些選項,其他系列沒有。
選項不同,添加和刪除的時候結(jié)果就可能不通。
- window.addEventListener("resize",onResize)
- // 移除事件監(jiān)聽函數(shù)onResize失敗
- window.removeEventListener("resize",onResize, true)
預(yù)警
事件監(jiān)聽函數(shù)添加前,比對四同屬性的事件監(jiān)聽函數(shù),如果有重復,進行報警。
統(tǒng)計高危監(jiān)聽事件函數(shù)
最核心的功能。
統(tǒng)計事件監(jiān)聽函數(shù)從屬對象的所有事件信息,輸出滿足 四同屬性 的事件監(jiān)聽函數(shù)。如果有數(shù)據(jù)輸出,極大概率,你內(nèi)存泄漏了。
統(tǒng)計全部的事件監(jiān)聽函數(shù)
統(tǒng)計事件監(jiān)聽函數(shù)從屬對象的所有事件信息, 可以用于分析業(yè)務(wù)邏輯。
一覽你添加了多少事件, 是不是有些應(yīng)該不存的,還存在呢?
基本使用
初始化參數(shù)
內(nèi)置三個系列:
- new EVM.ETargetEVM(options, et); // EventTarget系列
- new EVM.EventsEVM(options, et); // events 系列
- new EVM.CEventsEVM(options, et); // component-emitter系列
當然,你可以繼承BaseEvm, 自定義出新的系列,因為上面的三個系列也都是繼承BaseEvm而來。
最主要的初始化參數(shù)也就是 options
- options.isSameOptions
是一個函數(shù)。主要是用來判定事件監(jiān)聽函數(shù)的選項。
- options.isInWhiteList
是一個函數(shù)。主要用來判定是否收集。
- options.maxContentLength
是一個數(shù)字。你可以限定統(tǒng)計時,需要截取的函數(shù)內(nèi)容的長度。
EventTarget系列
- EventTarget[3]
- DOM節(jié)點 + 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;
- }
- });
- // 開始監(jiān)聽
- evm.watch();
- // 定期打印極有可能是重復注冊的事件監(jiān)聽函數(shù)信息
- setInterval(async function () {
- // statistics getExtremelyItems
- const data = await evm.getExtremelyItems({ containsContent: true });
- console.log("evm:", data);
- }, 3000)
- </script>
效果截圖
截圖來自我對實際項目的分析 , window對象上message消息的重復添加, 次數(shù)高達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);
- })
- })
效果截圖
事件分析的基本思路
上篇總結(jié)的思路:
- WeakRef建立和target對象的關(guān)聯(lián),并不影響其回收
- 重寫 EventTarget 和 EventEmitter 兩個系列的訂閱和取消訂閱的相關(guān)方法, 收集事件注冊信息
- FinalizationRegistry 監(jiān)聽 target回收,并清除相關(guān)數(shù)據(jù)
- 函數(shù)比對,除了引用比對,還有內(nèi)容比對
對于bind之后的函數(shù),采用重寫bind方法來獲取原方法代碼內(nèi)容
代碼結(jié)構(gòu)
代碼基本結(jié)構(gòu)如下:
具體注釋如下:
- evm
- CEvents.ts // components-emitter系列,繼承自 BaseEvm
- ETarget.ts // EventTarget系列,繼承自 BaseEvm
- Events.ts // events系列,繼承自 BaseEvm
- BaseEvm.ts // 核心邏輯類
- custom.d.ts
- EventEmitter.ts // 簡單的事件中心
- EventsMap.ts // 數(shù)據(jù)存儲的核心
- index.ts // 入口文件
- types.ts // 類型申請
- util.ts // 工具類
核心實現(xiàn)
EventsMap.ts
負責數(shù)據(jù)的存儲和基本的統(tǒng)計。
數(shù)據(jù)存儲結(jié)構(gòu):(雙層Map)
- Map<WeakRef<Object>, Map<EventType, EventsMapItem<T>[]>>();
- interface EventsMapItem<O = any> {
- listener: WeakRef<Function>;
- options: O
- }
內(nèi)部結(jié)構(gòu)的大綱如下:
方法都很好理解,大家可能注意到了,有些方法后面跟著byTarget的字樣,那是因為 其內(nèi)部采用Map存儲,但是key的類型是弱引用WeakRef。
我們增加和刪除事件監(jiān)聽的時候,傳入的對象肯定是普通的target對象,需要多經(jīng)過一個步驟,通過target來查到其對應(yīng)的key,這就是byTarget要表達的意思。
還是羅列一些方法的作用:
- getKeyFromTarget
通過target對象獲得鍵
- keys
獲得所有弱引用的鍵值
- addListener
添加監(jiān)聽函數(shù)
- removeListener
刪除監(jiān)聽函數(shù)
- remove
刪除某個鍵的所有數(shù)據(jù)
- removeByTarget
通過target刪除某個鍵的所有數(shù)據(jù)
- removeEventsByTarget
通過target刪除某個鍵某個事件類型的所有數(shù)據(jù)
- hasByTarget
通過target查詢是否有某個鍵
- has
是否有某個鍵
- getEventsObj
獲得某個target的所有事件信息
- hasListener
某個target是否存在某個事件監(jiān)聽函數(shù)
- getExtremelyItems
獲得高危的事件監(jiān)聽函數(shù)信息
- get data
獲得數(shù)據(jù)
BaseEVM
內(nèi)部結(jié)構(gòu)的大綱如下:
核心實現(xiàn)就是watch和cancel,繼承BaseEVM并重寫這兩個方法,你就可以獲得一個新的系列。
統(tǒng)計的兩個核心方法就是 statistics 和 getExtremelyItems。
還是羅列一些方法的作用:
- innerAddCallback
監(jiān)聽事件函數(shù)的添加,并收集相關(guān)信息
- innerRemoveCallback
監(jiān)聽事件函數(shù)的添加,并清理相關(guān)信息
- checkAndProxy
檢查并執(zhí)行代理
- restoreProperties
恢復被代理屬性
- gc
如果可以,執(zhí)行垃圾回收
- #getListenerContent
統(tǒng)計時,獲取函數(shù)內(nèi)容
- #getListenerInfo
統(tǒng)計時,獲得函數(shù)信息,主要是name和content。
- statistics
統(tǒng)計所有事件監(jiān)聽函數(shù)信息。
- #getExtremelyListeners
統(tǒng)計高危事件
- getExtremelyItems
基于#getExtremelyListeners匯總高危事件信息。
- watch
執(zhí)行監(jiān)聽,需要被重寫的方法
- cancel
取消監(jiān)聽,需要被重寫的方法
- removeByTarget
清理某個對象的所有數(shù)據(jù)
- removeEventsByTarget
清理某個對象某類類型的事件監(jiān)聽
ETargetEVM
我們已經(jīng)提到過,實際上已經(jīng)實現(xiàn)了三個系列,我們就以ETargetEVM為例,看看怎么通過繼承和重寫獲得對某個系列事件監(jiān)聽的收集和統(tǒng)計。
核心就是重寫watch和cancel,分別對應(yīng)了代理和取消相關(guān)代理
checkAndProxy是核心,其封裝了代理過程, 通過自定義第二個參數(shù)(函數(shù)),過濾數(shù)據(jù)。
就這么簡單
- 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("參數(shù)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 = [];
- }
- }
總結(jié)
- 單獨設(shè)計了一套存儲結(jié)構(gòu)EventsMap
- 把基礎(chǔ)的邏輯封裝在BaseEVM
- 通過繼承重寫某些方法,從而可以滿足不同的事件監(jiān)場景。