介绍浏览器端的可靠性测试
在上一编文章中我们介绍了浏览器端可靠性测试的概念、测试方法、以及常用的测试和分析工具。我们知道,浏览器端可靠性测试,就是以浏览器为测试平台,通过模拟用户在真实场景下的页面操作(点击、拖拽),来发现 Web 应用中潜在可靠性问题的测试。测试目的是确保 Web 应用在浏览器上能达到令人满意的用户体验和可靠性。
在浏览器的可靠性测试中,我们的测试重点,简单地说,就是浏览器进程的内存泄露。发现和分析这些内存泄露问题就是我们测试工作的主要目的。一般来说,我们可以通过手工测试、自动化测试的方法来发现浏览器进程的内存泄露。同时,借助一些分析工具,可以进一步帮助我们查看内存泄露的内容并分析原因,从而给开发人员提供有力的支持,使问题得到快速的解决。
浏览器端内存泄漏的原因
很多原因都可能导致浏览器端的内存泄露。按照与浏览器的相关性来分,有浏览器相关的内存泄露和浏览器不相关的内存泄露两种。
浏览器相关的内存泄露
所谓浏览器相关的内存泄露,是指在某种特定的浏览器下才会发生内存泄露,而在其他浏览器下并没有内存泄露。举个例子说,有些内存泄露问题只发生在 Internet Explorer 浏览器,而在 Firefox 上就没有泄漏。或者,一些内存泄露只发生在 Internet Explorer 浏览器 的某个版本上,比如在 6.0 的版本上,而在其他版本 Internet Explorer7.0 和 8.0 上就没有泄漏。
Internet Explorer 浏览器上的内存泄露
Internet Explorer 浏览器上的内存泄漏,是最常见的浏览器相关的内存泄漏问题。
微软 MSDN library 里有一篇讲述 Internet Explorer 浏览器的内存泄露模式的文章,非常详尽地讲解了 Internet Explorer 浏览器上的内存泄露的方式和原因,并加以举例。(http://msdn.microsoft.com/en-us/library/bb250448(VS.85).aspx)文章中将内存泄露的模式划分为四种:循环引用 (Circular References),闭包 (Closures),跨页面泄漏 (Cross-Page Leaks),伪泄漏 (Pseudo-Leaks)。
我们这里特别关注“循环引用”这种内存泄露模式。在 Internet Explorer 浏览器上, 我们遇到最多的内存泄露都是由于循环引用引起的。它的原理是这样的:JavaScript 是一种垃圾收集式语言,创建对象的时候分配给该对象内存,没有引用时收回内存。JavaScript 对象使用 Mark-and-Sweep 垃圾回收机制。然而,Internet Explorer 浏览器中的 COM(Component Object Model)对象使用的是 reference counting 的垃圾回收机制。所以,当两种使用不同垃圾回收机制的对象互相引用的时候,就会出现“循环引用”。一旦出现循环引用,引用计数器的值就只会增加,不会减少,所以就不能再达到零,对象无法满足垃圾回收的条件,也就不能回收引用对象使用的内存,就会造成内存泄露。
其他常见的内存泄露模式还有“闭包”。闭包功能非常强大,它们使内部函数在外部函数返回时也仍然可以保留对此外部函数变量的访问。但是,闭包非常易于隐藏 JavaScript 对象和 COM 对象间的循环引用,造成内存泄露问题。跨页面泄漏和伪泄漏遇到的不多,大家可以参看引用的文章自己进行学习。
好在,Internet Explorer 浏览器的内存泄露问题已经得到越来越多的关注。并且,随着 Internet Explorer 版本的不断更新,微软也在努力解决由浏览器本身的缺陷带来的内存泄露。比如,在推出 Internet Explorer 8.0 的时候,很多已知的内存泄露问题已经得到了解决或改善。从我们的实际测试中也看到,Internet Explorer 8 中的内存泄露问题明显少与 Internet Explorer 7。
尽管如此,仍然有许多用户在用着早先的 Internet Explorer 版本,对于测试人员来说,面对产品在不同 Internet Explorer 版本上的支持仍旧是一个挑战。
浏览器无关的内存泄露
浏览器不相关的内存泄露,就是指那些在几乎各种浏览器下都存在的内存泄露,这些泄露可能在程度上有所不同。比如说,某种页面操作会导致在 Internet Explorer 7.0 上 8M 的内存泄露, 而在 Firefox 3.0 浏览器上就只有 2M 的泄露。
代码逻辑的错误通常是导致这种内存泄露的主要原因。比如,在做某个操作的时候需要创建一些对象。在操作结束后应该及时销毁,再次操作的时候再重新创建。但由于代码逻辑的错误,可能会导致没有销毁或销毁动作失败,导致反复生成相同的对象,占用了过多的内存,造成内存泄露。
发现内存泄漏的方法
如何判定内存泄露呢?
首先,我们来谈谈什么是内存泄露。我们知道,内存增长并不一定是内存泄露。我们经常会发现浏览器的内存在增长,比如第一次打开一个新的窗口,或进行复杂页面展示的时候等等,都会导致浏览器进程内存使用的增加。但是如果我们反复做相同的操作,浏览器进程的内存占用通常会维持在一个稳定的值,并不会不停地增长。并且,在刷新页面之后,这些增长的内存基本都会被释放,回到起初的状态。我们说,这样的内存增长并不是内存泄露。
内存泄露的主要表现就是:在重复某种页面操作的时候,浏览器进程的内存占用持续不断地增加。并且,在刷新页面之后,无法释放已经增长的内存。
怎样发现内存泄露呢?
我们可以借助工具,也可以通过手工测试来发现浏览器端的内存泄露。
手工测试
重复相同的页面操作,监测并记录浏览器进程的内存使用情况。手工测试可以在最短的时间内定位一些明显的内存泄露问题。
举例说明手工测试的步骤:
- 打开浏览器(如 Internet Explorer 7.0)
- 载入测试页面
- 打开任务管理器,找到 Internet Explorer 进程
- 进行页面操作,比如刷新页面
- 从任务管理器中获得浏览器进程的内存使用值
- 重复第 4、5 步的操作,将每次内存值记录下来填在表中。
表 1. 内存使用记录表
从上面的表中,我们很清晰地看到,每次页面操作,会导致约 7.5M 的内存增长,这说明:刷新页面的动作存在严重的内存泄露问题。
需要注意的是,在手工测试得到的表单中,我们一般会忽略第一次记录的值,因为第一次的内存增长很可能是正常的创建了一些必要的页面元素,并不一定是内存泄露。我们常用的方法是:计算每次操作后内存使用的增长,并求平均值,就可以得到每次页面操作导致的内存泄露值。
自动化测试
自动化测试是另外一种有效的发现内存泄露的测试方法。借助自动化工具,我们可以通过脚本对页面进行重复性的操作,并自动获取浏览器进程的内存使用值。在自动化测试中,我们经常会发现一些相对较小的内存泄露,以及一些由于长时间运行导致的对浏览器或服务器端的影响。
举例说明自动化测试的步骤:
- 打开浏览器(如 Internet Explorer 7.0)
- 载入测试页面
- 启动测试工具,测试脚本准备就绪
- 启动系统监测工具,监测浏览器进程的内存使用值
- 通过执行测试脚本对页面进行操作
- 实时监测浏览器进程的内存使用趋势
测试结束的时候,根据内存使用趋势图,我们就可以直观地看出是否存在内存泄露,以及内存泄露的严重性。
图 1. 内存使用趋势图 – 存在内存泄露
图 2. 内存使用趋势图 - 没有内存泄露
如何借助工具分析内存泄漏
一旦发现了内存泄露的问题,就要及时向开发团队报告。
我们描述一个内存泄露的缺陷,仅仅描述现象是不够的,还需要提供各种有用的信息给开发人员。在上一篇文章里我们提到,在报告产品缺陷的时候,需要提供的必要信息有:重现问题的步骤、测试环境(操作系统和浏览器的版本)、是否是浏览器相关的问题、在不同浏览器上每次操作产生的内存泄漏大小等等。除此之外,如果我们能够根据测试结果做一些必要的分析,能使开发人员快速地定位到问题所在,使缺陷尽早修复。
要想分析内存泄露,需要借助一些工具。我们这里介绍最常用的基于 Firefox 和 Internet Explorer 浏览器使用的两款分析工具:Firebug 和 sIEve。
Firebug 是 Mozilla Firefox 浏览器的开源扩展,提供了很多功能。我们最常使用的功能有:查看 DOM 结构、JavaScript 调式、profile 等。
sIEve 是一个专门针对 Internet Explorer 浏览器的内存泄露分析工具。我们用它来发现页面中的孤立节点,查找出页面中的循环引用和内存泄露。
使用 Firebug 分析内存泄露的例子
我们通过一个实例来说明,如何使用 Firebug 工具来分析内存泄露问题。
实例一:
在手工测试中,我们发现每一次点击菜单的操作都会导致浏览器进程的内存增长。使用 Firebug 来检查一下点击菜单前后 DOM 结构的变化。在操作之前,我们先刷新一下页面,使得一些不用的或隐藏的 DOM 结点可以被销毁。
执行点击菜单的操作,我们发现点击后生成了这样两个对象:
com_ibm_mm_builder_iwidget_navigation_NavigationMenu_0 和
com_ibm_mm_builder_iwidget_navigation_NavigationMenu_1
图 3. 第一次点击菜单后 Firebug 的显示(查看大图)
再次点击菜单,观察 DOM 结构的变化。我们发现,对于每次的点击操作,都会产生这样两个对象,一直到 com_ibm_mm_builder_iwidget_navigation_NavigationMenu_n。这些对象只能被创建,不能被销毁。
图 4. 第二次点击菜单后 Firebug 的显示(查看大图)
由此,我们可以猜测,这是一个由于代码逻辑错误引发的内存泄露问题。对象 com_ibm_mm_builder_iwidget_navigation_NavigationMenu_* 在点击菜单的时候被创建,但是并没有在下次点击的时候成功地销毁它们。
上面的信息,是对开发人员有用处的。他们可以有针对性的去查看代码,分析产生内存泄露的原因。在这个例子中,开发人员可以重点查看代码中何时创建 com_ibm_mm_builder_iwidget_navigation_NavigationMenu_*,以及何时对它进行销毁,如何调用等等,来修正代码逻辑,使问题得到解决。
使用 sIEve 分析内存泄露的例子
我们也同样给出一个在 Internet Explorer 浏览器下,使用 sIEve 来分析内存泄露的例子。
实例二:
测试中,我们发现刷新页面的操作会带来内存泄露。通过 sIEve 工具,在刷新动作执行之后,我们可以看到提示有 19 个 DOM 结点泄露,导致了内存增长 3M 多。如下图:
图 5.sIEve 中显示的内存泄露
点击“Show Leaks”查看泄露的 DOM 结点。
图 6. sIEve 中显示的泄露的 DOM 结点
列表里显示了所有泄露的结点,我们可以将一切认为有用的信息提供给开发人员,使他们知道到底是哪些对象导致了内存的泄露。一些信息,比如 ID, Tag,OuterHTML 都可能给开发人员提供帮助。
避免内存泄漏的建议
对于浏览器不相关的内存泄露,是比较容易避免的。只要开发人员足够仔细,避免因为代码设计或逻辑的缺陷而导致的重复创建对象,以及未及时销毁对象等问题,并且做好代码审查工作,这一类问题可以预防和避免的。
对于浏览器相关的内存泄露,就相对复杂些了。由于大多数问题来自 Internet Explorer 浏览器,所以我们这里主要摘录一些如何在 Internet Explorer 浏览器下避免内存泄露的 JavaScript 编写技巧。另外,开发团队要有针对性地制定出编码规范,并要求所有的开发人员认真遵守。
下面列出了一些避免内存泄露的 JavaScript 编写技巧,仅供参考:
- 当不再使用某些对象的时候,要打破 JavaScript 本地对象和 COM 对象之间的连接。
- 谨慎地使用闭包(closure)。闭包在 JavaScript 中被大量使用,但它却是最常见的内存泄漏的源头。
- 避免使用扩展(expando)属性。扩展属性是附加到 DOM 元素上的任意 JavaScript 属性,也是导致循环引用的常见原因。
- 避免由于事件导致的循环引用。 DOM 对象绑定的事件处理函数中不要访问 DOM 对象自己的变量。
- 使用显式类型转换,避免由于自动类型转换造成的内存泄露。
- 一个不建议,但在适当的时候可以考虑的方法,就是通过浏览器调用垃圾回收进程,主动进行垃圾回收。