引言

在开发综合治理平台态势概览的大屏页面的过程,遇到了页面崩溃的问题,本帖子记录了崩溃的原因分析和解决方案。

问题

打开综合治理平台,进入态势概览页面,停留在此页面一段时间,会出现如下图所示的页面崩溃的情况。

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出

原因分析

注:以下操作环境建议在浏览器隐身模式下进行,防止其他因素干扰

使用工具

根据页面崩溃的提示,可以初步判定是页面内存溢出导致的崩溃,为了验证内存是否溢出,可以使用 Chrome 浏览器自带的工具分析验证。这里介绍三种工具的使用,可以结合实际需求来使用。

  1. 任务管理器 1) 打开方式:
  2. ios 开发者账户崩溃日志 开发者选项崩溃异常_Memory_02

  3. 2) 界面:
  4. ios 开发者账户崩溃日志 开发者选项崩溃异常_ios 开发者账户崩溃日志_03

  5. 3)使用方式: 任务管理器的使用方法最为简单,打开需要分析的页面,直接观察内存占用空间与 JavaScript 使用的内存即可。如果这两个数据持续上升,说明内存正在泄漏。
  6. 开发者工具 Performance 面板 1) 打开方式: 按 F12 打开开发者工具,选择 Performance 栏 2) 界面:
  7. ios 开发者账户崩溃日志 开发者选项崩溃异常_开发者工具_04

  8. 3) 使用方式: 打开需要分析的页面,等页面稳定下来后,点击 Performance 左上角的录制按钮开始录制,它会保存下页面的快照、JS Heap、Document、Nodes、Listener、GPU Memory等信息。录制一段时间(最好在一分钟以上)后,停止录制,等待工具生成报告。
  9. 开发者工具 Memory 面板 1) 打开方式: 按 F12 打开开发者工具,选择 Memory 栏 2) 界面:
  10. ios 开发者账户崩溃日志 开发者选项崩溃异常_Memory_05

  11. 3) 使用方式: 这个工具的使用方式最为复杂,这里只简单介绍下 Heap snapshot 的使用,感兴趣的可以自行搜索其他使用方式。 首先,打开需要分析内存的页面,点击工具左上方的录制按钮,生成分析报告。 其次,进行一些可能导致内存上升的操作后,再次点击录制按钮,生成分析报告。 最后,我们有了两份分析报告,通过菜单栏的下拉框选择 Comparison 过滤分析结果。观察 #New、#Deleted、#Delta 这三列,分别代表新增对象数、删除对象数、新增数与删除数的差值,找到那些只有新增,没有删除的对象,看看是被谁引用了,据此来找到可能导致内存溢出的代码。
  12. ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_06

确定内存溢出原因

打开态势概览页面后,通过任务管理器观察页面使用的内存,发现内存是持续上升的,这时再通过 Performance 工具进一步分析。 以下是 Performance 的分析结果:

ios 开发者账户崩溃日志 开发者选项崩溃异常_ios 开发者账户崩溃日志_07

通过分析结果可以看到,内存资源在持续上升,再进一步观察结果,可以发现内存上升是存在一个阶梯式的上升周期的,为什么会产生这种现象呢?放大内存上升的部分进一步分析看看:

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_08

上图显示了内存上升部分细节,把鼠标移动到页面快照上,可以清楚地看到,当上一个大屏页面轮播到下一个页面时,内存就会上升并且不会再下降到之前的水平。现在已经有理由怀疑是页面轮播引起的内存溢出,所以,通过暂停页面轮播,再进行一次 Performance 分析,看看分析结果:

ios 开发者账户崩溃日志 开发者选项崩溃异常_ios 开发者账户崩溃日志_09

ios 开发者账户崩溃日志 开发者选项崩溃异常_Memory_10

通过上图可以看到,暂停页面轮播后,并没有明显的上升趋势,说明浏览器可以正常回收内存,并没有溢出。至此,已经可以确定当页面轮播时内存会溢出。

分析内存溢出对象

经过第二步的分析,已经知道了大屏页面切换会导致内存升高,利用这个证据,用 Memory 工具去进一步分析,找到那些被引用本该被释放,但实际没有的释放的对象。 首先,打开态势概览页面,先暂停页面轮播切换,停留在总体态势页面,待页面加载完成,然后打开 Memory 工具,点击录制按钮分析总体态势页面的内存。分析完成后,手动切换到风险态势页面或者其他大屏页面,再切换回总体态势页面,然后在 Memory 工具中再次点击录制按钮分析页面切换之后的内存。完成以上操作之后,就得到了两份分析报告,分别是内存上升前和上升后的,在 Memory 工具的菜单栏下拉框中选择 Comparison,看看到底是哪些家伙占用了内存!

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_11

来分析上面的结果图,首先,页面上有各种类型的对象,点击对象可以看到对象具体的引用信息,我们的任务是通过对象引用信息找到一些蛛丝马迹。我们可以搜索 detached 开头归类的对象,也就是那些在内存中但是没有在页面进行渲染的元素。选择一个,可以看到它的详细引用信息:

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_12

很明显,ehcarts 引用了这个对象,而这个对象连同它的引用,理应是在页面切换之后被销毁了的,既然他没有销毁,说明我们的代码是有问题的。接下来要做的是,找出 ehcarts 引用的对象没有被销毁的原因,修改相关代码,再验证。

解决方案

使用正确的 echarts 实例销毁方法

根据上面的原因分析,我们知道是 echarts 引用的对象没有正常被销毁,那么很简单,我们只要尝试正确销毁 echarts 实例就好了。进入到我们的 ehcarts 组件代码,定位到 beforDestory 钩子,可以看到,已经有一段代码对 echarts 实例进行释放了。

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_13

进入 echarts 官网查询销毁实例的相关 api,发现.clear()方法只是清空了实例,并没有销毁,而.dispose()方法才会销毁实例。

ios 开发者账户崩溃日志 开发者选项崩溃异常_内存溢出_14

答案到这里已经呼之欲出了,我们项目中之前一直用的是.clear()方法清空 echarts 实例对象,而不是用.dispose()销毁,所以 echarts 实例并没有被正常销毁,当我们频繁地切换页面的时候,echarts 实例就会不断的累加,占用的内存也会随之增加。所以,这里建议,以后我们封装 echarts 组件的时候,统一使用.dispose()方法销毁组件。

ios 开发者账户崩溃日志 开发者选项崩溃异常_ios 开发者账户崩溃日志_15

页面隐藏时停止定时器任务

你以为到这里就结束了吗?事情没有那么简单!在搜索内存溢出解决方案的时候,在网上看到了一篇文章:

ios 开发者账户崩溃日志 开发者选项崩溃异常_Memory_16

再次通过 Performance 工具分析验证,结果如下:

ios 开发者账户崩溃日志 开发者选项崩溃异常_Memory_17

果然,内存又在持续增加,那么就使用网上分享的方法解决这个问题,添加一个标签切换的监听函数,在离开页面时,把页面的定时器清除,回来页面时重新开启定时器,这样就可以了。

结果

  1. 保持在态势概览页面,并开启轮播页面,使用 Permermance 工具进行内存分析 结果:内存保持在平稳状态,没有上升,页面没有崩溃
  2. ios 开发者账户崩溃日志 开发者选项崩溃异常_开发者工具_18

  3. 进入态势概览页面,开启轮播页面后,切换到其他标签页或最小化浏览器 结果:内存保持在平稳状态,没有上升,页面没有崩溃
  4. ios 开发者账户崩溃日志 开发者选项崩溃异常_ios 开发者账户崩溃日志_19

  5. 保持在态势概览页面,并开启轮播页面,不做其他操作,挂机一天一夜。 结果:内存保持在 2G 左右,页面没有崩溃

总结

通过此次的内存溢出分析,我们认识了一些内存分析工具及内存分析方法,也发现了代码中不足的地方,最后通过正确的方法解决了内存溢出的问题。希望这篇文章可以对大家日后的工作有所帮助。当然,这只是很小一部分,也可能有不正确的地方,欢迎大家提出疑问一起探讨。