事件監聽函數的內存泄漏,都給我退散吧!
本文轉載自微信公眾號「云的程序世界」,作者云的世界。轉載本文請聯系云的程序世界公眾號。
前言
內存泄漏是個很嚴肅的問題,可是迄今也沒有一個非常有效的排查方案,本方案就是針對性的單點突破。
工作中,我們會對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
- 通過繼承重寫某些方法,從而可以滿足不同的事件監場景。