引言
Flex 采用 ActionScript 语言作为脚本语言,编译后的二进制代码在 FlashPlayer 虚拟机 AVM(Actionscript Virtual Machine)中运行。和 Java 语言类似,AVM 中也有一个垃圾收集器(Garbage Collection),对于不用的对象,隔一段时间会进行收集并销毁,释放内存。和 C++ 语言相比,程序员不需要时刻关注内存的分配和释放,大大减轻了负担。但是垃圾收集器不能从根本上解决 Flex 内存泄露的问题,这得从 FlashPlayer 虚拟机的垃圾回收机制谈起。
FlashPlayer 虚拟机的垃圾回收机制
垃圾收集器采用计数法或标记法来查找需要清除的对象。计数法由于无法检测循环引用的对象,现在已经很少采用了。重点谈一下标记法。Flex 应用的对象在内存中被映射成树形结构。这很好理解,每个 Flex 应用总有一个 Application 的入口被称为根节点(Root),垃圾收集器从根节点开始遍历每个对象,对可达对象标记为“有效”(有一种例外就是弱引用,后面的章节详谈)。而在这棵树之外的孤岛对象或者由于循环引用形成的孤岛对象集合被标记为“无效”,垃圾收集器会在合适的时间销毁这些无效对象,完成一次垃圾收集。而垃圾收集器是运行在虚拟机中的一个低优先级的守护进程,为了不影响性能,它只在必要的时候才运行。例如在向操作系统申请新内存空间的时候,发生异常的时候等等,因此内存并不是实时回收的。
Flex 内存泄露的原因
有了垃圾收集器,为什么 Flex 还会产生内存泄露呢?从垃圾收集器的角度看,对象分为“有效”和“无效”两类;而从 Flex 应用程序的角度看,对象又被分为“有用”和“无用”两类。
举个例子,当程序出现逻辑错误需要提示用户时,Flex 程序构造一个提示框,这时,提示框是一个“有用”的对象,当用户点击关闭按钮关掉提示框后,提示框就变成“无用”的对象了,应用程序再也不会用到它(下次出现相同逻辑错误时,程序又会构造一个全新的提示框)。应用程序认为这个提示框应该被回收掉,但是因为某种原因,存在一个从“有效”对象到这个提示框的引用,垃圾收集器显然认为提示框也是“有效”的。这个“有效”“无用”的提示框便造成了 Flex 的内存泄露。
开发过程中造成内存泄露的两种情况
了解了 Flex 内存泄露的原因,从程序员的角度来讲,对于对象引用的混乱管理是造成 Flex 内存泄露的人为因素。Flex 开发中对于对象的引用分为两种:显示引用和隐式引用,我们分别就这两种情况讨论一下它们是如何造成内存泄露的。
显示引用
public static var staticVar : Object = new Object(); public function leak():void{ var chart : AreaChart = new AreaChart(); staticVar = chart; chart = null; } |
在 leak()方法中,创建了一个临时变量 chart,然后将它赋给静态变量 staticVar,虽然最后将 chart 置为 null,但是由于静态变量对它有一个引用,chart 所占的内存不会被回收,造成内存泄露。
var chart : AreaChart = new AreaChart(); leak(chart); chart = null; ...... public static var staticVar : Object = new Object(); public function leak(chart : AreaChart):void{ staticVar = chart; } |
原因和上例相同,只是发生的位置更加隐蔽。
public function setFocus():void { var sm:ISystemManager = systemManager; if (sm && (sm.stage || sm.useSWFBridge())) { if (UIComponentGlobals.callLaterDispatcherCount == 0) { sm.stage.focus = this; UIComponentGlobals.nextFocusObject = null; } ... |
在调用 setFocus() 方法后,通过 sm.stage.focus = this,全局对象 systemManager 产生了对 UIComponent 的引用。如果之后不做处理,就会造成内存泄露。这里只是举个例子,对于 UIComponent 的 setFocus() 方法 Flex 已经做了处理,不会造成内存泄露,大家可以放心使用。但是在日常的编程过程中,一定要注意这种非常隐蔽的情况。
隐式引用
隐式引用最常见的情况就是添加事件监听器。a.addEventListener("Leak", b.leakHandler);a 对象的“listener”属性创建了一个指向 b 对象 leakHandler 方法的引用,如下图所示,
即使把 b 置为 null,只要 a 对象没有被回收,b 也不会被回收,从而导致内存泄露。通过弱引用方式可以避免这种内存泄露,在后面的章节会有详细描述。
Flex Builder Profiler 工具简介
Adobe 公司在 Flex Builder 3 中提供了一个 Profiler 工具,用于 Flex 内存诊断和性能调优。本文重点介绍 Profiler 用于内存诊断方面的功能。
Profiler 的启动
在 Flex 开发视图,选择主入口文件并点击鼠标右键 ->Profile As->Flex Application 启动 Profiler 工具。如下图所示:
Profiler 参数配置
启动后,系统首先弹出对话框让用户配置 Profiler 的参数,如下图所示:
配置完毕,点击 Resume 按钮继续执行。
几秒钟之后,Profiler 工具开始运行,如下图所示:
Profiler 常用功能
“Profile”窗口显示正在运行的 SWF 应用,选中后,窗口上的一系列的按钮就变成可用状态,如下图所示:
这里简要介绍一下和内存调试相关的按钮的功能:
图 5 中 1“Run Garbage Collector”,点击该按钮,会强制执行一次内存回收。
图 5 中 2“Take Memory Sanpshot”,在运行的任何时刻,点击该按钮,系统首先会自动执行一次强制内存回收,然后捕获一帧内存快照,作为当前正在运行的应用的子对象,如下图所示:
双击这帧内存快照,会在下部窗口打开一个页签,显示该内存快照中的对象,包括对象所在的包,实例数目和百分比,所占内存和百分比,如下图所示:
图 5 中 3“Find Loitering Objects”,找出“游荡对象”。该按钮需要同时选中两帧内存快照(按住 Ctrl 键点选内存快照)才有用,如下图所示:
它的作用是通过对比两帧内存快照,找出在后一帧内存快照中存在而在前一帧内存快照中 不存在的对象。在某些特定的场景中,该功能能够迅速找到内存泄露的对象。
利用 Profiler 工具诊断内存泄露
内存快照对比法诊断内存泄露
Flex 应用内存泄露的最直观表象是当用户进行某些相同操作时,内存和对象实例会持续增加,即使进行了垃圾回收,内存也不会回到原始的水平。
在上一章节中,我们了解了 Profiler 的主要功能,它可以在任何时刻捕获当时的内存快照。那么可以想象,如果每当用户进行一次相同的操作时,我们就捕获一帧内存快照,通过对比几帧快照,找出持续增加的对象实例,就可以发现是哪些对象导致了内存泄露,从而发现程序中的漏洞,最终解决问题。
我们通过一个例子来实践一下这个假设。本文提供了一个模拟生成验证码的 Flex 程序 DetectMemoryLeak.mxml,如下图所示:
每当用户点击“Change”按钮时,就会随机产生一个 4 位数字替换原来的数字。该程序在用户每次点击按钮后都会导致内存的增加,我们怀疑这个程序存在内存泄露。启动 Profiler 工具,用户每点击一次“Change”按钮,我们就捕获一帧内存快照,连续做 3 次,我们对比一下这 3 帧快照,如下图所示:
通过对比,我们发现 NumberChangeLabel 的实例数目随着用户的每次点击都会增加,并且不会被回收,因此断定是 NumberChangeLabel 导致了内存泄露。双击最后一帧内存快照中的 NumberChangeLabel 对象,会打开“Object References”页签,显示这 4 个实例被哪些对象引用,如下图所示:
表中的 4 条数据代表这帧内存快照中有 4 个 NumberChangeLabel 实例,每个实例后面括号中的数字,比如(7),代表了有多少个引用指向这个实例。点击“+”号,逐项展开,如下图所示:
我们发现绝大部分是对象自身或者对象的子元素对该对象的引用,这种引用不会产生内存泄露。而有一项 DetectmemoryLeak 的 arr 属性对 NumberChangeLabel 的引用是外部引用,也正是因为这个引用导致 NumberChangeLabel 不能被回收。检查相关的源代码:
清单 4. DetectMemoryLeak.mxml
private static var arr : Array = new Array(); private function changeClickHandler(event:MouseEvent):void{ labelContainer.removeAllChildren(); var label : NumberChangeLabel = new NumberChangeLabel(); labelContainer.addChild(label); arr.push(label); } |
发现 arr 是静态数组,arr 的 push 操作导致了内存泄露,去掉 arr.push(label) 后,问题解决。至此,内存泄露的诊断就完成了。
利用“游荡”对象诊断内存泄露
内存快照对比法虽然直观,但是当应用非常复杂时,可能有成百上千个对象,仅通过肉眼对比,效率和准确率会大大降低。我们想到了 Profiler 工具中的“Find Loitoring Objects”查找游荡对象功能,可不可利用这个功能来找出内存泄露的对象呢?从“游荡”对象的定义来看,它是在后一帧内存中存在而前一帧内存中不存在的对象,那么游荡对象是不是就是导致内存泄露的对象呢?对于上个模拟生成验证码的例子,经过修改,程序不再存在内存泄露,当用户点击“Change”按钮时,产生一副新图片 image1,替换前一帧内存中的旧图片 image0,image0 将被垃圾收集器回收。根据定义“image1”是游荡对象,很明显它不会导致内存泄露。
那么游荡对象有什么用处呢?假设有这样一个场景,用户从初始状态 A 进行了一系列操作到达终止状态 B,A 和 B 的状态完全相同,就好像又回到初始状态一样。举个例子,用户从初始状态开始,点击按钮弹出对话框,再关闭对话框后,到达终止状态,起始状态和终止状态完全相同,那么此时如果存在游荡对象就会导致内存泄露了。
运行本文提供的示例程序 LoiteringMemoryLeak.mxml 来模拟这个场景,初始状态只有一个按钮“Popup a dialogue”, 如下图所示:
点击“Popup a dialogue”弹出对话框,如下图:
点击“OK”按钮关闭对话框之后,到达终止状态。在初始状态和终止状态各捕获一帧内存快照,选中两帧快照点击“Finding Loitoring Objects”按钮,打开“Loitoring Objects”页签,如下图:
产生了很多游荡对象,别担心,大部分对象都是由一个或几个高层对象产生的,找出产生内存泄露的高层对象就能解决问题。我们发现游荡对象“LeakDialogue”是程序中的一个用户自定义对话框,它可能是产生了内存泄露的根源。双击这个条目打开“Object Reference”页签,如下图:
点击“+”号逐项展开,通过分析发现,LeakDialogue 的 leakHandler 方法被全局对象 systemManager 的 listener21 引用导致了内存泄露,检查程序发现问题果然出在 systemManager 的事件监听环节:
清单 5.LoiteringMemoryLeak.mxml
private function changeClickHandler(event:MouseEvent):void{ var ld : LeakDialogue = new LeakDialogue(); PopUpManager.addPopUp(ld, this, true); PopUpManager.centerPopUp(ld); systemManager.addEventListener(KeyboardEvent.KEY_DOWN, ld.leakHandler); } |
去掉最后一句,问题解决。
需要说明一点,读者可能发现即使改正后,还是会有少量的游荡对象,这是因为用户的动作会触发事件,Flex 会为事件和事件处理创建内部对象,这些对象不会引发内存泄露,因此只需要关注有没有高层控件成为游荡对象就可以了。
开发中避免内存泄露的几点建议
我们暂且把需要回收的对象称为临时对象,那么从根本上讲,对于临时对象引用的管理不当是引发内存泄露的根本原因,以下是几点关于如何避免内存泄露的建议。
另外,自引用方式 a.addEventListener("Leak", this.leakHandler); 子对象对父对象的引用方式 children.addEventListener("Leak", parent.leakHandler); 都不会产生内存泄露。