移动端vue项目内存泄漏问题排查指南

背景

        近期收到相关的反馈表示App偶现渲染异常、闪退等问题,严重影响用户体验。为此,我们对App端webview页面进行了梳理、重构和部分逻辑的优化,在排除了webview模块本身的影响后,经排查,发现多个H5项目存在内存泄漏的问题。由架构直接维护的mobile-system模块已定位并修复了引起内存泄漏的主要问题,鉴于上述问题的可能在多个业务组普遍存在,本文总结了我们排查和解决这一问题的思路和方法,希望通过此文档,帮助业务定位和解决项目的内存泄漏问题提供借鉴和参考。

基础概念

    1、什么是内存泄露?

        内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 谈到内存泄露就不得不提到另一个重要的概念:"垃圾回收" 机制。Javascript是一种高级语言,它不像C语言那样要手动申请内存,然后手动释放,js在声明变量的时候自动会分配内存,普通的类型比如Number,一般放在栈内存里,对象放在堆内存里,声明一个变量,就分配一些内存,然后定时进行垃圾回收。

 

    2、什么是垃圾回收(GC:Garbage Collecation) ?

JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收(GC:Garbage Collecation) 。 JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。

其原理是垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。 到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除引用计数,这里不作过多的阐述,文章下方附上更详细的资料了解,感兴趣的同学可自行了解。

 

    小结

        有了上述概念,我们很容易理解,js的内存泄露就是指new了一块内存,但无法被释放或者被垃圾回收。new了一个对象之后,它申请占用了一块堆内存,当把这个对象指针置为null时或者离开作用域导致被销毁,那么这块内存没有人引用它了在JS里面就会被自动垃圾回收。言归正传,既然js会分配内存并定时进行垃圾回收,那为什么还会产生内存泄漏的情况呢? 

 

内存泄漏的产生原因?

        如官方解释的那样:由于JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放“, 这个“自动”是混乱的根源,并让JavaScript(和其他类似机制的高级语言)开发者错误的感觉他们可以不关心内存管理。因此,代码中一些不规范的写法如变量的不合理声明和引用、滥用闭包、未清除 dom 元素的引用 dom 元素移除但对 dom 元素的引用没有解除等情况均可能引发内存泄漏的情况。概括的说,如果一个不再使用的对象指针没有被置为null,且代码里面没办法再获取到这个对象指针了,就会导致无法释放掉它指向的内存,也就是说发生了内存泄露。举个vue中的例子:

 

 

        上述代码在实例挂载后,监听了document的click事件,且回调函数中存在对$refs节点的引用,会导致被引用的真实DOM节点在销毁时不仍存在引用,从而导致分配的内存未被回收,就发生了内存泄露 。

首先要明白一点,如滥用闭包是导致内存泄漏的罪魁祸首,但并不代表所有的闭包都是有害的,比如特定情况下的闭包,就是利用了内存引用不会被释放的形式,完成一些特殊功能,因此,我们应着重关注导致内存持续激增且失效后仍不能被回收的点。 下面结合实际的操作,介绍排查内存泄漏问题的方法。

 

如何排查内存泄漏问题?

        对于js的内存泄漏问题,Chrome提供了Memory 内快照的记录功能为我们分析内存问题提供参考,内存分析主要使用Chrome的debug工具Profiles面板,Profiles可以追踪网页程序的内存问题。打开控制台,选择Memory选项, 如图,左侧是Profiles的start等操作选项,右侧是Profiles提供的功能选项

 

 

Profiles提供了3个功能项

Heap snapshot

通过创建堆快照可以查看创建快照时网页上的JS对象和DOM节点的内存分布情况,使用该工具可以创建JS的堆快照、内存分析图、对比堆快照帮助定位内存泄漏问题。

 

Allocation instrumentation on timeline

从整个Heap角度记录内存分配的时间轴信息。

点击Start按钮之后,执行可能会引起内存泄漏的操作,操作之后点击左上角的Stop按钮即可。在蓝色竖线上通过缩放过滤构造器窗格来显示在指定的时间帧内被分配的对象信息。在录制过程中,在时间线上会出现一些蓝色竖条,这些蓝色竖条代表一个新的内存分配。

 

Allocation sampling

用于分析网页上的JS函数在执行过程中的CPU消耗信息。

点击Start按钮,执行你想要去深入分析的页面操作,当你完成你的操作后点击Stop按钮。然后会显示一个按JS函数进行内存分配的分解图,默认的视图是Heavy,该视图会把最消耗内存的函数显示在最顶端。

 

内存快照只是为了帮助我们能更具体定位发生泄漏问题的点 ,但是这里并不建议直接拍摄内存快照进行排查。 根据排查内存问题的经验,建议大家打开Chrome devtools,并开启’性能监控器 ‘定位到导致内存持续激增且未被及时释放的操作,这能为你的排查工作节省大量的不必要操作,如下图所示:

 

根据性能监控器的反馈,我们可以很快的定位到内存激增时的操作,如点击了某个按钮或加载了某个组件的操作,这样,我们就能尽快的将问题复现出来,并方便使用堆快照进行分析。

利用谷歌浏览器调试工具的内存快照分析问题引发的点,具体操作如下:

1、拍摄初始状态的堆快照

 

2、找到使你内存增加的业务场景(如打开某个组件内存上升,关闭该组件时内存却没有释放下降),截取两段快照(如第一段为组件打开前,第二段为组件关闭后),对比内存大小这样就可以知道这个组件打开一次造成了多少内存泄漏。

 

3、选择第二段快照,右边选择Comparison,比对。

 

由上图可以看到,VueComponent组件

新增了#New 538

释放了#Deleted 41

泄漏值 538– 41 = 137 泄漏了 497个组件

泄漏内存大小Alloc Size 3286 B 字节

释放的内存 Freed Size 0 B 字节

 

 

 

 

同理也可以观察其他部分的情况,比如Object Array 有多少新增,释放了多少,泄漏了多少。

重点:我们最终还是要注意VueComponent的泄漏值,因为VueComponent是会挂载对象、数据、事件的,所以那些Object Array产生泄漏值也很大可能是VueComponent造成的。

根据经验,vue项目中最容易出现内存泄漏的情况如下:

(1)监听在window/body/document等事件没有解绑

(2)绑在EventBus的事件没有解绑

(3)Vuex的$store watch了之后没有unwatch

(4)模块形成的闭包内部变量使用完后没有置成null

(5)使用第三方库创建,没有调用正确的销毁函数

我们在处理泄漏的时候最好尽可能保证VueComponent的泄漏值尽可能减少, 然后再依次排查js闭包、事件监听 循环定时器方面的影响。

 

内存泄漏问题如何解决?

        定位到了具体的问题,那么修改起来也就相对容易一些了,我们回顾上面的例子:

 

 

        上述代码在实例挂载后,定位到事件监听引发的内存泄露,只需要将函数声明到实例上,然后在beforeDestroy时机移除事件监听就可以解决,如下图所示:

 

        当然,很多时候情况会相对复杂一些,如vue官方就提供了很好的示例

接下来的示例展示了一个由于在一个 Vue 组件中使用 Choices.js 库而没有将其及时清除导致的内存泄漏。等一下我们再交代如何移除这个 Choices.js 的足迹进而避免内存泄漏。

下面的示例中,我们加载了一个带有非常多选项的选择框,然后我们用到了一个显示/隐藏按钮,通过一个 v-if 指令从虚拟 DOM 中添加或移除它。这个示例的问题在于这个 v-if 指令会从 DOM 中移除父级元素,但是我们并没有清除由 Choices.js 新添加的 DOM 片段,从而导致了内存泄漏。

 

解决这个内存泄漏问题

在上述的示例中,我们可以用 hide() 方法在将选择框从 DOM 中移除之前做一些清理工作,来解决内存泄露问题。为了做到这一点,我们会在 Vue 实例的数据对象中保留一个 property,并会使用 Choices API 中的 destroy() 方法将其清除。

通过这个更新之后的 CodePen 示例可以再重新看看内存的使用情况。

 

这样做的价值

        内存管理和性能测试在快速交付的时候是很容易被忽视的,然而,保持小内存开销仍然对整体的用户体验非常重要。

        考虑一下你的用户使用的设备类型,以及他们通常情况下的使用方式。他们使用的是内存很有限的上网本或移动设备吗?你的用户通常会做很多应用内的导航吗?如果其中之一是的话,那么良好的内存管理实践会帮助你避免糟糕的浏览器崩溃的场景。即便都不是,因为一个不小心,你的应用在经过持续的使用之后,仍然有潜在的性能恶化的问题。

        

 

参考资料:

javascrip的内存管理

垃圾回收主要方法

posted @ 2021-06-29 14:54  pixel-matrix  阅读(2744)  评论(0编辑  收藏  举报