【代码篇】事件监听函数的内存泄漏,都给我退散吧!

简介: 内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?

1.JPG


前言




内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。


工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?


本文是代码篇,主要讲使用和实现。


更多理论知识,请阅读理论篇 【方案篇】事件监听函数的内存泄漏,帮你搞定!


源码和demo



源码: 事件分析vem

项目内部有丰富的例子。


核心功能



我们解决问题的时机无非为 事前事中事后


我们这里主要是 事前事后


  • 事件监听函数添加前进行预警
  • 事件监听函数添加后进行统计


了解功能之前,先了解一下四同特性:


  1. 同一事件监听函数从属对象
    事件监听总是要注册到响应的对象上的, 比如下面代码的window, socket, emitter都是事件监听函数的从属对象、


window.addEventListener("resize",onResize)
socket.on("message", onMessage);
emitter.on("message", onMessage);
复制代码
  1. 同一事件监听函数类型
    这个比较好理解,比如window的 message, resize等,Audio的 play等等


  1. 同一事件监听函数内容这里注意一点,事件监听函数相同,分两种:
  • 函数引用相同
  • 函数内容相同


  1. 同一事件监听函数选项
    这个可选项,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
  • 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 2.JPG


events 系列




基本使用


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_  系列事件重复添加

3.JPG


component-emitter 系列



  • 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);
        })
    })
复制代码


效果截图


4.JPG


事件分析的基本思路



上篇总结的思路:


  1. WeakRef建立和target对象的关联,并不影响其回收
  2. 重写 EventTargetEventEmitter 两个系列的订阅和取消订阅的相关方法, 收集事件注册信息
  3. FinalizationRegistry 监听 target回收,并清除相关数据
  4. 函数比对,除了引用比对,还有内容比对
  5. 对于bind之后的函数,采用重写bind方法来获取原方法代码内容


代码结构


代码基本结构如下:


5.JPG


具体注释如下:


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
}
复制代码


内部结构的大纲如下:


6.JPG


方法都很好理解,大家可能注意到了,有些方法后面跟着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


内部结构的大纲如下:


7.JPG


核心实现就是watchcancel,继承BaseEVM并重写这两个方法,你就可以获得一个新的系列。


统计的两个核心方法就是 statisticsgetExtremelyItems


还是罗列一些方法的作用:


  • innerAddCallback
    监听事件函数的添加,并收集相关信息
  • innerRemoveCallback
    监听事件函数的添加,并清理相关信息
  • checkAndProxy
    检查并执行代理
  • restoreProperties
    恢复被代理属性
  • gc
    如果可以,执行垃圾回收
  • #getListenerContent
    统计时,获取函数内容
  • #getListenerInfo
    统计时,获得函数信息,主要是name和content。
  • statistics
    统计所有事件监听函数信息。
  • #getExtremelyListeners
    统计高危事件
  • getExtremelyItems
    基于#getExtremelyListeners汇总高危事件信息。
  • watch
    执行监听,需要被重写的方法
  • cancel
    取消监听,需要被重写的方法
  • removeByTarget
    清理某个对象的所有数据
  • removeEventsByTarget
    清理某个对象某类类型的事件监听


ETargetEVM


我们已经提到过,实际上已经实现了三个系列,我们就以ETargetEVM为例,看看怎么通过继承和重写获得对某个系列事件监听的收集和统计。


  1. 核心就是重写watch和cancel,分别对应了代理和取消相关代理
  2. checkAndProxy是核心,其封装了代理过程, 通过自定义第二个参数(函数),过滤数据。
  3. 就这么简单


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
  • 通过继承重写某些方法,从而可以满足不同的事件监场景。


写在最后



技术交流群请到 这里来。 或者添加我的微信 dirge-cloud,带带我,一起学习。

相关文章
|
16天前
|
程序员 C语言
C语言库函数 — 内存函数(含模拟实现内存函数)
C语言库函数 — 内存函数(含模拟实现内存函数)
26 0
|
27天前
|
编译器 C语言 C++
【C语言】memset()函数(内存块初始化函数)
【C语言】memset()函数(内存块初始化函数)
26 0
|
27天前
|
编译器 C语言 C++
【C语言】memcpy()函数(内存块拷贝函数)
【C语言】memcpy()函数(内存块拷贝函数)
42 0
|
1月前
|
IDE Linux 开发工具
内存泄漏检测工具Valgrind:C++代码问题检测的利器(一)
内存泄漏检测工具Valgrind:C++代码问题检测的利器
87 0
|
4天前
|
编译器 C语言
字符串与内存函数
字符串与内存函数
19 0
|
1天前
|
C语言
C语言:内存函数(memcpy memmove memset memcmp使用)
C语言:内存函数(memcpy memmove memset memcmp使用)
|
27天前
|
编译器 C语言 C++
【C语言】calloc()函数详解(动态内存开辟函数)
【C语言】calloc()函数详解(动态内存开辟函数)
25 0
|
27天前
|
存储 前端开发 编译器
【C语言】memmove()函数(拷贝重叠内存块函数详解)
【C语言】memmove()函数(拷贝重叠内存块函数详解)
33 1
|
30天前
|
安全 程序员 C++
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
【C++ 基本知识】现代C++内存管理:探究std::make_系列函数的力量
101 0