AS3 内存泄漏和解决方法

delete关键字在Flash中是用来删除定义的变量,但是并不将对象从内存中清除掉(这是垃圾收集器的工作)。它只是将一个变量的引用设置成无效,让内存中的这个对象无法再被引用和使用,也无法再使用for in来枚举。 
 
事实上,垃圾处理器(GC)将在特定的时候,自动的从内存中删除那些不再被引用和使用的变量。比如,你创建了两个对象引用A和B,都指向了对对象 ObjectX的引用,如果delete了A,并不会让垃圾收集器把ObjectX从内存中删除,因为B的引用还是指向了这个对象。如果将A和B都 delete,则不再有对ObjectX的引用,ObjectX也将被垃圾收集器回收。例如:

var a:Object = new Object();
var b:Object = a; // b和a引用同一个new Object();
delete a;
trace(b); // 输出[object Object] - 在内存中还是存在
delete b;
// GC将回收object这个特性在Flash8和9(AS123)中几乎都是一样的,但是在Flash8中,GC的一些特性得到改善并能更好的工作。(注意,垃圾收集不是即时的) 

虽然GC在AS3中并没有什么本质上的改变,但是因为使用了新的虚拟机,delete关键字的行为有所改变。现在,delete关键字只能针对类的动态属性和非公有成员有效。而在AS1和2中,delete能被用在所有的东西上。

// ActionScript 2
class DeleteVarClass {
        
    public var myVar:Number;
    
    function DeleteVarClass() {
        myVar = 1;
        trace(myVar); // 1
        delete myVar;
        trace(myVar); // undefined
    }
}

// ActionScript 3
package {
    public class DeleteVarClass {
        
        public var myVar:Number;
            
        public function DeleteVarClass() {
            myVar = 1;
            trace(myVar); // 1
          delete myVar;
            trace(myVar); // 1
        }
    }
}在上面的AS3例子中,因为myVar变量是一个公有成员,所以不能用delete来删除这个变量。

尽管在AS3中不能删除类成员,但是如果你想删除一个对象的所有引用,可以通过将变量设置为null来代替delete。如:

myVar = null;如果一个对象的所有引用都是null,GC将自动的从内存中删除这个对象。

*Dictionary类

AS3中的Dictionary类(flash.utils.Dictionary)是一个新的AS类。Dictionary类和Object唯一的区别在于:Dictionary对象可以使用非字符串作为键值对的键。例如:

var obj:Object = new Object();
obj["name"] = 1; // 键是字符串"name"
obj[1] = 2; // 键是1 (被转换成字符串"1")
obj[new Object()] = 3; // 键是new Object(),被转传成字符串"[object Object]"

for (var prop:String in obj) {
     trace(prop); // 输出:[object Object], 1, name
     trace(obj[prop]); // 输出:3, 2, 1
}

也就是说,无论用什么类型的变量作为键,都将被转换成字符串。同时,如果你使用了不同的对象作为键,都会北转换成字符串"[object Object]"作为键,因此而指向了同一个数据。例如:

ActionScript Code:   
var a:Object = new Object();   
var b:Object = new Object();   

var obj:Object = new Object();   
obj[a] = 1; // obj["[object Object]"] = 1;   
obj[b] = 2; // obj["[object Object]"] = 2;   

for (var prop:String in obj) {   
     trace(prop); // traces: [object Object]   
     trace(obj[prop]); // traces: 2   

}Dictionary类将没有这个限制,你可以将键设置成任何一种数据类型。例如:

import flash.utils.Dictionary;

var a:Object = new Object();
var b:Object = new Object();

var dict:Dictionary = new Dictionary();
dict[a] = 1; // dict[a] = 1;
dict[b] = 2; // dict[b] = 2;

for (var prop:* in dict) {
     trace(prop); // traces: [object Object], [object Object]
     trace(dict[prop]); // traces: 1, 2
}虽然在trace的时候,输出的还是[object Object],但是这个结果是对象的toString的结果。在Dictionary对象中,代表的是不同的对象引用。

注意,这里的prop的类型是*。这是很重要的,因为dict对象中的键可能是任何数据类型的。



一、Flash Player垃圾回收机制:

Flash Player垃圾回收工作是由垃圾回收器(garbage collector)完成的。垃圾回收器是运行在后台的一个进程,它释放那些不再被应用所使用对象所占用的内存。不再被应用所使用的对象是指那些不再会被那些活动着(工作着)的对象所“引用”的对象。在AS中,对于非基本类型(Boolean, String, Number, uint, int)的对象,在对象之间传递的都是对象引用,而不是对象本身。删除一个变量只是删除了对象的引用,而不是删除对象本身。一个对象可以被多处引用,通过这些不同的引用所操作的都是同一个对象。

通过以下两段代码可以了解基本类型和非基本类型对象的差异:
基本类型的值传递:
private function testPrimitiveTypes():void
{
var s1:String="abcd"; //创建了一个新字符串s1,值为"abcd"
var s2:String=s1; //String是基本类型,所以创建了一个新的字符串s2,s2的值拷贝自s1。
s2+="efg"; //改变s2的值s1不会受影响。
trace("s1:",s1); //输出abcd
trace("s2:",s2); //输出abcdefg
var n1:Number=100; //创建一个新的number,值为100。
var n2:Number=n1; //Number是基本类型,所以又创建一个新number n2,n2的值拷贝自n1。
n2=n2+100; //改变n2对n1不会有任何影响。
trace("n1",n1); //输出100
trace("n2",n2); //输出200
}
非基本类型对象的引用传递:
private function testNonPrimitiveType():void
{
// 创建一个新对象, 然后将其引用给变量a:
var a:Object = {foo:"bar"}
//将上面所创建对象的引用拷贝给变量b(通过变量b建立对对象的引用):
var b:Object = a;
//删除变量a中对对象的引用:
delete(a);
// 测试发现对象仍然存在并且被变量b所引用:
trace(b.foo); // 输出"bar", 所以对象仍然存在
}
对于非基本类型对象,AS3采用两种方法来判定一个对象是否还有活动的引用,从而决定是否可以将其垃圾回收。一种方法是引用计数法,一种方法是标记清除法。
Reference Counting
引用计数法是判定对象是否有活动引用的最简单的一种方法,并且从AS1就开始在Flash中使用。当创建一个对对象的引用后,对象的引用计数就加一,当删除一个引用时,对象的引用技术就减一。如果对象的引用计数为0,那么它被标记为可被GC(垃圾回收器)删除。例如:

var a:Object = {foo:"bar"}
// 现在对象的引用计数为1(a)
var b:Object = a;
// 现在对象的引用计数为2(a和b)
delete(a);
// 对象的引用计数又回到了1 (b)
delete(b);
// 对象的引用计数变成0,现在,这个对象可以被GC释放内存。
引用计数法很简单,并且不会增加CPU开销,可惜的是,当出现对象之间循环引用时它就不起作用了。所谓循环引用就是指对象之间直接或者间接地彼此引用,尽管应用已经不再使用这些对象,但是它们的引用计数仍然大于0,因此,这些对象就不会被从内存中移除。请看下面的范例:
创建第一个对象:
var a:Object = {};
// 创建第二个对象来引用第一个对象:
var b:Object = {foo:a};
//使第一个对象也引用第二个对象:
a.foo = b;
// 删除两个活动引用:
delete(a);
delete(b);
上面的例子中两个活动引用都已被删除,因此在应用程序中再也无法访问这两个对象。但是它们的引用计数都是1,因为它们彼此相互引用。这种对象间的相互引用可能会更加复杂(a引用b,b引用c,c又引用a,诸如此类),并且难以在代码中通过删除引用来使得引用技术为变为0。Flash player 6 和7中就因为XML对象中的循环引用而痛苦,每个XML节点既引用了该节点的子节点,又引用了该节点父节点。因此,这些XML对象永远不会释放内存。好在 player 8增加了一种新的GC技术,叫做标记清除。
标记清除(Mark Sweeping)
AS3使用的第二种查找不活动对象的GC策略就是标记清除。 Player从应用的根节点开始(在AS3中通常被称为”根(root)”),遍历所有其上的引用,标记每个它所发现的
对象。然后迭代遍历每个被标记的对象,标记它们的子对象。这个过程第归进行,直到Player遍历了应用的整个对象树并标记了它所发现的每个东西。在这个过程技术的时候,可以安全地认为,内存中那些没有被打标记的对象没有任何活动引用,因此可以被安全地释放内存。可以通过下图可以很直观地了解这种机制(绿色的引用在标记清除过程中被遍历,绿色对象被打上了标记,白色对象将被释放内存)
标记清除机制非常准确,但是这种方法需要遍历整个对象结构,因此会增大CPU占用率。因此,Flash Player9为了减少这种开销只是在需要的时候偶尔执行标记清除活动。
注意:上面所说的引用指的是“强引用(strong reference)”,flash player在标记清除过程中会忽略“弱引用(weakness reference )”,也就是说,弱引用在标记清除过程中不被当做引用,不会阻止垃圾回收。
垃圾回收的时机
Flash Player在运行时请求内存的速度受限于浏览器。因此,Flash Player采用小量请求大块内存,而不是大量请求小块内存的内存请求策略。同样,Flash Player在运行时释放内存速度也相对较慢,所以Flash Player会减少释放内存的次数,只有在必要的时候才释放内存。也就是说,Flash Player的垃圾回收只有在必要的时候才会执行。
当前,Flash Player的垃圾回收发生在Flash Player需要另外请求内存之前。这样,Flash Player可以重新利用垃圾对象所占用的内存资源,并且可以重新评估需要另外请求的内存数量,也会节省时间。
在程序的实际运行中验证了以上的说法,并不是每次应用申请内存时都会导致垃圾回收的执行,只有当Flash占用的内存紧张到一定程度时才会执行真正的垃圾回收,如果应用中内存开销增长是匀速的,那么计算机物理内存越大,则垃圾回收触发周期越长。在我的测试环境中,计算机有2G的物理内存,直到打开FLSH 应用的浏览器占用700M物理内存之后才会导致Flash Player回收垃圾内存。


二、开发中导致内存泄露的常见情况

通过上面的讨论我们可以知道,只要对象被其他活动对象(仍在运行的)所引用,那么这个对象就不会被垃圾回收,从而可能造成内存泄露。
在我们的开发中,如下的一些情形会导致内存泄露:

(一)被全局对象所引用的对象在它们不再使用时,开发者忘记从全局对象上清除对它们的引用就会产生内存泄露。常见的全局对象有stage,主 Application,类的静态成员以及采用singleton模式创建的实例等。如果使用第三方框架,比如:PureMvc,Cairongorm 等,要注意这些框架的实现原理,尤其要注意框架里面采用singleton模式创建的controler和Model。
(二) 无限次触发的Timer会导致内存泄漏。无论无限次触发的 Timer 是否为全局对象,无限次触发的Timer本身以及注册在Timer中的监听器对象都不会被垃圾回收。

(三)通过隐式方式建立的对象之间的引用关系更容易被程序员所忽略,从而导致内存泄露。最常见的以隐式方式建立对象之间的引用就是“绑定”和“为对象添加事件监听器”。通过测试我们发现“绑定”不会造成内存泄露,对象可以放心地绑定全局对象。而调用addEventListener()方法“为对象添加事件监听器”则可能产生内存泄露,大多数内存泄露都因此而来:下面代码:
a.addEventListener(Event.EVENT_TYPE,b.listenerFunction)
使得a对象引用了b对象,如果a对象是一个全局对象(全局对象在应用运行期间始终存在),则b对象永远不会被垃圾回收,可能会造成内存泄露。比如下面的代码就有造成内存泄露的可能:
this.stage.addEventListener(Event.RESIZE,onResize);
上面代码中的stage是UIComponent的stage属性,表示当前Flex应用运行的“舞台”。
不过,通过以下三种方式使用addEventListener方法不会造成内存泄露:
1.
用弱引用方式注册监听器。就是调用时将addEventListener的第五个参数置为true,例如:someObject.addEventListener(MouseClick.CLICK, otherObject.handlerFunction, false, 0, true);
2.
自引用的方式。即:为对象添加的监听处理函数是对象本身的方法。例如:
this.addEventListener(MouseClick.CLICK, this. handlerFunction);
3子对象引用。即:为子对象添加的监听处理函数是父上对象的方法。例如:
private var childObject:UIComponent = new UIComponent; addChild(childObject); childObject.addEventListener(MouseEvent.CLICK, this.clickHandler);


三、内存释放优化原则

1. 被删除对象在外部的所有引用一定要被删除干净才能被系统当成垃圾回收处理掉;

2. 父对象内部的子对象被外部其他对象引用了,会导致此子对象不会被删除,子对象不会被删除又会导致了父对象不会被删除;

3. 如果一个对象中引用了外部对象,当自己被删除或者不需要使用此引用对象时,一定要记得把此对象的引用设置为 null;

4. 本对象删除不了的原因不一定是自己被引用了,也有可能是自己的孩子被外部引用了,孩子删不掉导致父亲也删不掉;

5. 除了引用需要删除外,系统组件或者全局工具、管理类如果提供了卸载方法的就一定要调用删除内部对象,否则有可能会造成内存泄露和性能损失;

6. 父对象立刻被删除了不代表子对象就会被删除或立刻被删除,可能会在后期被系统自动删除或第二次移除操作时被删除;

7. 如果父对象 remove 了子对象后没有清除对子对象的引用,子对象一样是不能被删除的,父对象也不能被删除;

8. 注册的事件如果没有被移除不影响自定义的强行回收机制,但有可能会影响正常的回收机制,所以最好是做到注册的事件监听器都要记得移除干净。

9. 父对象被删除了不代表其余子对象都删除了,找到一种状态的泄露代码不等于其他状态就没有泄露了,要各模块各状态逐个进行测试分析,直到测试任何状态下都能删除整个对象为止。


四、内存泄露举例

1. 引用泄露:对子对象的引用,外部对本对象或子对象的引用都需要置 null ;

2. 系统类泄露:使用了系统类而忘记做删除操作了,如 BindingUtils.bindSetter() , ChangeWatcher.watch() 函数 时候完毕后需要调用 ChangeWatcher.unwatch() 函数来清除引用,否则使用此函数的对象将不会被删除;

类似的还有 MUSIC , VIDEO , IMAGE , TIMER , EVENT , BINDING 等。

3. 效果 泄露:当对组件应用效果 Effect 的时候,当本对象本删除时需要把本对象和子对象上的 Effect 动画停止掉,然后把 Effect 的 target 对象置 null; 如果不停止掉动画直接把 Effect 置 null 将不能正常移除对象。

4. SWF 泄露:要完全删除一个 SWF 要调用它的 unload() 方法并且把对象置 null;

5. 图片泄露:当 Image 对象使用完毕后要把 source 置 null;( 为测试 ) ;

6. 声音、视频 泄露 : 当不需要一个音乐或视频是需要停止音乐,删除对象,引用置 null;


五、内存泄露解决方法

1. 在组件的 REMOVED_FROM_STAGE 事件回掉中做垃圾处理操作(移除所有对外引用(不管是 VO 还是组件的都需要删除),删除监听器,调用系统类的清除方法)

先 remove 再置 null, 确保被 remove 或者 removeAll 后的对象在外部的引用全部释放干净 ;

2. 利用 Flex 的性能优化工具 Profile 来对项目进程进行监控,可知道历史创建过哪些对象,目前有哪些对象没有被删除,创建的数量,占用的内


关于查找内存泄漏安全加载/卸载多个SWF模块和子应用是涉及内存泄漏最常见场景。每天,我们了解到播放器更多的如何进行内存管理及其特性,因此该总结了。
  当调试怀疑在加载/卸载SWF时发生了内存泄漏,我一般会执行以下操作:

1)建立应用程序或测试来多次加载和卸载SWF(至少3次),迫使垃圾收集器在每次加载或卸载后工作,接着使用性能分析工具来查看内存中有多少个模块的xxx_FlexModuleFactory或子应用程序的SystemManager的副本。如果超过1个,保持加载和卸载查看该数值是否继续增长。任何模块或SWF引入不同风格的新组件将需要利用StyleManager注册,StyleManager始终首先被加载。你可以在主程序中或通过CSS模块预加载样式来防止其发生。如果加载第二个副本可能在此处停留,因为播放器或FocusManager可能一直挂起,如果你看到超过2,绝对是内存泄漏了,您应该使用Profiler来寻找泄漏。
2)经过多次装卸后,采用内存快照,然后再多次加载和卸载再采用快照。清除所有的过滤器,删除百分比,用类名排序并手动比较每个类的实例数量。它们可能是完全匹配地,除了少数字符,弱引用。一切都值得怀疑和调查。
3)我想清理SWF得到所有引用,接下来在调试器中多次加载和卸载并查看console面板信息。在调试面板中查找以 [UnloadSWF]标记开始的行:  告诉我播放器认为SWF卸载后一切都清理了,请注意,清理可能不是马上就执行的,即使有时播放器请求GC(垃圾回收)后若SWF还有内部引用会得到“later”稍后清理。如果不明白,回到第2步比较内存快照查找可能泄漏的对象。
4)现在我确信即使播放器在卸载SWF后认为一切OK了,但System.totalMemory却仍在增加,最后的测试将其导出为 release版并运行在release播放器中。调试版播放器会将调试信息编译进SWF中,这样会歪斜System.totalMemory的值(值会不正确)。目前测试,一旦通过第3步,release播放器的System.totalMemory报告是可以接受的,一个小得多的并可接受的最大内存值上限。
通过上述操作后,当使用操作系统工具检查播放器进程,你可能会发现播放器内存属性仍然在增长,这个问题是播放器团队留下的研究空白区域。对于Internet Explorer,人们常发现最小化IE会导致内存应用减小,这竟味着IE的内存管理器在处理着什么,而不是播放器或你的应用程序减小了内存。我们不知道有什么编程方式可以强迫IE缩减内存。即使Flash认为所有对象应该已被卸载我们也要看看其它浏览器的内存增长报告。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章