在ActionScript中,我们没有API可以直接删除一个对象,也不能控制Player进行GC。但是GC的行为是可以预估的,作为开发者,我们需要了解的是GC执行的时机是发生在需要向操作系统请求分配内存的时候。
从上面的模拟图我们可以看到:
一次GC过程(GC Pass)分为以下两个步骤:
Reference Counting
统计所有对象的引用计数,如果某个对象没有任何引用,就标记为可回收。
这个操作很好理解,需要强调的是weak reference(弱引用)是不参与计算的。引用计数是一个相对省CPU的操作,能够筛选出大部分可回收资源,但是对一些循环引用的情况就无能为力了。在下图中,标记为绿色的对象每个的引用数都为1,但它们明显是应该被回收的。
所以GC需要进行第二个步骤:
Mark Sweeping
这个步骤是从根对象(Root)开始轮询对象的引用。所谓的根对象包括:
这种方式足够精确,能够成功筛选出上图中绿色标记的对象,而它的代价就是较大的计算开销。
为了帮助GC过程更高效的执行,最好是能在第一步引用计数中就把需要回收的对象都标记出来。具体的做法就是把所有不需要的对象引用全部清空,包括:
难点:事件监听是否会造成对象不能回收?这个问题要具体分析,有些情况可以,有些情况却不可以。归根结底还是引用关系的问题。
来看下面这个例子:
Foo.as:
public class Foo extends Sprite
{
private var bar:Sprite;
public function Foo()
{
//监听bar发出的事件。可以看作是bar引用了foo,因为foo的引用被保存在bar的监听者数组里。
bar.addEventListener(MouseEvent.CLICK, clickHandler);
addChild(bar);
}
private function clickHandler(event:MouseEvent):void
{
...
}
}
Main.as:
// 创建foo实例
var foo:Foo = new Foo();
addChild(foo);
// do something with foo...
// 清除foo的引用
removeChild(foo);
foo = null;
我们看到foo引用了bar,而bar又通过事件监听的联系引用了foo,这就构成了一个循环引用。根据前文对GC步骤的分析,这两个对象都必须到第二步mark and sweep才能被标记出来。
如果我们对事件监听用弱引用的方式:
bar.addEventListener(MouseEvent.CLICK, clickHandler, false, 0, true);
但是作为最佳实践,我们还是提倡要手动移除事件监听。以下代码添加一个destroy()方法(也有习惯命名为dispose()或kill()等)到Foo对象中:
public function destroy():void
{
bar.removeEventListener(MouseEvent.CLICK, clickHandler);
removeChild(bar);
bar = null;
}
foo.destroy();
removeChild(foo);
foo = null;
从上例可以看出在一般情况下,监听子对象的事件不会影响GC。不管是弱引用方式的监听,还是显式移除监听,都只是帮助GC更高效运行的手段,而不会影响GC的结果。但是如果事件监听造成的是对象以外的引用关系,情况就不同了,并且很有可能造成回收失败。一个常见的错误例子是监听Stage对象的 RESIZE事件,如果没有显式移除这个监听或者是没有采用弱引用方式,那么这个对象就不会被GC回收的。所以我建议大家还是要尽可能的显式移除监听,切断引用关系。
我们现在已经用对GC最友好的方式做好了清理准备,但是对象还没有从内存中删除。在等待GC执行的这段时间,对象内的代码还在继续执行。比如加在对象上的ENTER_FRAME事件监听处理还在继续执行,对象内的MovieClip或Sound都还在继续播放。我们一定要避免这种情况的发生,所以在切断引用之前,我们还要在destroy()方法中做些清理工作。我们要做的工作包括:
其实这些都是在开发中管理资源的基本常识,归结为一句话就是:谁创建了对象,谁就要负责清理该对象。
下面就以一些我在实际项目中开发的destroy()方法为例,看代码说话:
public function destroy():void
{
// List是一个继承自Sprite的自定义子类
// 移除list的事件监听
list.removeEventListener.remove(MouseEvent.CLICK, clickHandler);
// 我们创建了list实例,也要负责清理
// 调用list对象自己的destroy()方法
list.destroy();
// 将list从显示列表移除
// 这一步并非必须的步骤,原因是list的父对象会被移除,这样list和stage就没有联系了。
// 但是在我的代码中,list还有可能会被添加到外部的容器中(比如stage),那么这一步就是必须的了。
list.parent.removeChild(list);
list = null;
// --------------------
// loader是一个来自tweenmax类库中的ImageLoader实例
// 如果loader已经创建
if(loader)
{
// 调用loader的dispose()方法,优秀的第三方类库都应该有良好的资源管理机制。
// 参数true表示把加载的内容从显示列表上移除,帮我们节约了代码。
loader.dispose(true);
loader = null;
}
// --------------------
// lightbox实例变量保存了一个外部引用
// 根据谁创建谁清理的原则,我们在这里不需要负责该对象的清理,只要删除引用就可以了。
lightbox = null;
}
public function destroy():void
{
// cells是一个数组,包含了一组子对象
var i : int, n : int;
n = cells.length;
for(i = 0; i < n; i++)
{
// 执行每一个子对象的destroy()方法
cells[i].destroy();
// 删除子对象的引用
cells[i] = null;
}
// 删除数组自身的引用
cells = null;
// 如果不需要得到每一个子对象的引用,我们也可以简单的用以下代码来清理数组:
//cells.length = 0;
//cells = null;
}
var n:int = this.numChildren;
for(var i:int = n-1; i >= 0; i--)
{
if(this.getChildAt(i) is IDestroyable)
{
IDestroyable(this.getChildAt(i)).destroy();
this.removeChildAt(i);
}
}