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認爲所有對象應該已被卸載我們也要看看其它瀏覽器的內存增長報告。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章