Flash MX 中的範圍鏈 (scope chain)與記憶體浪費
作者:FindSome 日期:2007-10-02

Flash MX 中的範圍鏈 (scope chain)與記憶體浪費 Timothée Groleau著 李易修 譯 原文刊載於http://timotheegroleau.com/Flash/articles/scope_chain.htm |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
簡介 Flash MX是非常強大的工具,伴隨而來的是開發者必須注意他們做了什麼。你寫的程式碼可能有無法預期的結果或運作方式,但是你可能不知道為什麼會有這樣的情況發生。這些情況有一大部分是發生於你的程式碼的背面。 所以今天,我將介紹範圍鏈(scope chain),並呈現在Flash MX中範圍鏈如何導致記憶體的浪費。雖然這篇文章前半段非常基礎,但是讀者最好能夠對actionscript有比較深入的了解。 這邊提到的部分程式碼或許能夠應用到JavaScript,但是我並未測試過,不能擔保一定能夠運作。 誌謝 這篇文章並沒有新的創見,只能算是Tatsuo Kato, Casper Schuirink, Peter Hall, Ralf Bokelberg, Gregory Burtch等人在FlashCoders' List討論內容的摘要。 具我所知,巢狀函式 (nested function)的範圍鏈是 Naohiko Ueno在日文電子論壇上先提出的。你可以到這裡去看他所寫的文章。(注意:你必須加入這個論壇,才能閱讀這些文章)這個資訊是Tatsuo Kato在FlashCoders' list上提供的。 這篇文章裡面的許多範例取自Tatsuo Kato先前的文章和巧妙的概念。 回顧 關於記憶體管理 你或許已經注意到,在actionscript中你從來不需要分配 (allocate)或釋放記憶體。因為actionscript是高階程式語言,能夠自動幫你處理記憶體的問題。 在記憶體管理中的垃圾收集器 (garbage collector)一個非常熱門的議題。當你建立了變數與物件,Flash自動配置記憶體給它們。垃圾收集器是一個程式,它會持續追蹤物件是否持續被你的程式使用。如果它偵測到物件不在被使用,它會刪除這個物件並釋放記憶體,釋放先前使用的資源。 垃圾收集器是一個完整的議題,已經超出這篇文章的範圍。它使用類似參照次數 (reference coun)t與標記清掃 (mark and sweep)等技術來追蹤物件是否續存。這篇文章當中,我們將會提到主程式裡至少有一個參照或有任何一個物件在使用時,物件就會存在記憶體當中。
關於函式 「(var)用來宣告區域變數 ( local variables)。如果你在函式裡宣告區域變數,變數是定義給函式使用,當呼叫函式結束後變數即失效。」 這個敘述是摘自 actionscript dictionnary內對var的說明。就像上面所說的:var被用來宣告函式內的區域變數,當函數的執行結束後,區域變數將會被刪除。這個時候垃圾收集器會移除區域變數,因為它們在主程式內已經沒有參照。 下面是一個簡單的例子:
我確定你已經很熟悉上面這種情況了。 關於函式,這裡有一個有趣的例子。你可以從函式外部呼叫函式內部的變數,像下面這樣:
你說會說:「這有什麼大不了的?」但實際上發生了什麼事?在函式外面時,Flash如何存取變數"a"呢?答案是Flash依尋著依附在函式上的範圍鏈來存取變數。
什麼是範圍鏈 (scope chain)? 範圍鏈只在actionscript字典中說明"with"指令的部分出現過一次。我建議每個人都讀一讀那一頁,因為可以從那邊取得許多寶貴的資訊。對於本文來說,那一頁講的太快了,所以讓我們慢下來,讓我好好的做個說明。 什麼是範圍 (scope)?對我來說,範圍是Flash尋找變數時的物件。簡單的說,範圍鏈是當Flash尋找變數時,依序檢查的一組物件。實際執行時,Flash把範圍鏈視為堆疊 (stack),從上開始向下搜尋。當上方範圍鏈上方的物件內找不到欲尋找的變數時,Flash會移動到下個物件,重複同樣的動作,直到找到變數,或整個範圍鏈都已經被檢查過。 當未明確指出範圍時,範圍鏈是唯一取得屬性的一種方式。舉例來說,當執行"trace(a);"時,因為並未明確指定"a"的位置,Flash在範圍鏈內尋找"a"。當我們執行"trace(anyObject.a)"時,就是指定了尋找"a"的詳細位置。"anyObject"參照具體指定了範圍,因此Flash將在物件"anyObject"內部尋找"a",而不到範圍鏈內尋找。 在Flash當中,actionscript只能寫在時間軸上,而時間軸是在影片片段 (movie clip)內(_root或其他的影片片段內)。從放置actionscript程式碼的位置開始,範圍鏈至少包含兩個元素:目前的物件(包含這個程式碼的物件)和_global物件。下面是一段測試的程式碼:
我們逐行解釋,這一行發生了什麼事:第1行在_global 物件中建立了變數"a"。第2行在目前的位置建立了變數"a"。第3行,為了追蹤a,Flash在目前的位置尋找"a",找到之後就輸出結果(5)。第4行刪除目前位置的"a"。在第5行,為了追蹤"a",Flash在目前的位置尋找"a",結果找不到"a",於是就開始到範圍鏈內的第二個物件尋找"a",找到之後輸出結果(4)。 範圍鏈出現在actionscript字典裡面with那一頁的原因,在於with能夠在範圍鏈的最上方新增一個物件。
就如同你所看到的同樣的"trace(a)"敘述,被執行了三次,每次的輸出都不同。因為我們刪除了變數"a",因此Flash不斷向下搜尋範圍鏈內的與參照相同的變數。
函式與activation物件 函式的建立 當一個函式被建立、被Flash使用時,它的範圍鏈就依附在函式的範圍鏈。 依附在函式上時,範圍鏈就無法改變,無法從中新增或刪除新的物件。請看下面的例子:
雖然"meth"為物件"obj"的一個方法,但"obj"不在"meth"的範圍鏈中。也就是說"test"的範圍鏈無法被"obj"影響。唯一能夠影響函式內部的,就只有"this"參照(前面的例子並未使用)。關於"this"參照的說明,容後再述。 你或許說前面的測試並不公平,因為這個函式最初以test為名而建立。唯一要注意的是"function() {...}"這段程式碼在整個程式中出現的位置。當我們不使用暫時的變數時,結果依然相同:
執行函式 每當函式被執行,一個新的物件被建立。這個物件存有所有由關鍵字var所建立的的區域變數,包含函式的參數 (paramenter)、參數陣列 (arguments array)。這個物件稱作activation物件 (activation object)。activation物件只在actionscript字典內出現過一次,也是在with指令那一頁。 當函式執行時,函式建立的activation物件被放函式的範圍鏈的最上方。因此,當函式執行時,函式的範圍鏈為: activation物件->函式的範圍鏈 在上面的程式碼中,如果函式被建立在_root的第1個影格,那麼當函式被執行時,範圍鏈為: activation物件->_root->_global 以記憶體管理的角度來說,activation物件的概念闡明了當函式執行時發生了什麼事:
巢狀函式與記憶體浪費 介紹 我們終於可以開始進入正題。使用Flash MX,新的事件模型 (event model)能夠方便的動態 (on the fly)指定函式到任何事件處理程式 (event handler)(或任何處理這件事的變數)。也就是說,函式能夠將程式碼包裝並且將其他函式指定到事件處理程式裡。 舉例來說:
"resetMC"函式把影片片段當成參數,將位置重新設定為(0,0),並且把函式附加到onEnterFrame處理程式,因此影片能夠開始像右下角移動。這樣舉例應該夠清楚了。然而,這個部分值得深入探討。 activation物件的持續存在 在繼續說明之前,我們先介紹幾個簡單的術語。不同的是,這個部分比較複雜。簡單來說,當一個函式被建立在另一個函式內時,我們稱裡面這個函式為內部函式 (inner function),外面的這個物件稱外部函式 (outer function)。 如果你了解我們先前所說的,你應該已經知道發生了什麼事。在上面的程式當中,每次"resetMC"被呼叫,activation物件被建立,並加到函式的範圍鏈。在第3行,內部函式被建立,被指定到傳入的影片片段的onEnterFrame處理程式中。 當內部函式被建立,目前的範圍鏈將會被附加到該函式做為它的範圍鏈。也就是說,外部函式的activation物件是目前的範圍鏈的一部分,代表一個參照已經被加到內部函式的範圍鏈上。 一般情況下,如果上面的程式被寫在_root的第1個 影格,附加到內部函式的範圍鏈應該是這樣的: 內部函式的activation物件 ->_root -> _global 當內部函式執行時,範圍鏈則變成: 內部函式的activation物件 -> 外部函式的activation物件 -> _root -> _global 你或許會問:「這有什麼大不了?」是的,因為外部函式的activation物件現在是內部函式的範圍鏈的一個參照,而內部函式現在是持續存在的,是影片片段的一個方法,所以activation物件也是持續存在的。的確,對垃圾收集器來說,activation物件有一個持續存在的參照,因此無法移除。因為上面的原因,activation物件內區域變數所佔用的記憶體無法被釋放。 複製函式 有許多方法可以實做巢狀函式。首先,就像我們前面注意到的,當函式每次被執行就會建立一個新的activation物件。因此,依然根據前面的程式碼,如果我們使用"resetMC"函式做為100個影片片段的onEnterFrame處理程式,那麼我們將會有100個不同的activation物件持續存在記憶體當中。 第二,因為內部函式被建立,並被外部函式指定到每個影片片段的onEnterFrame處理程式,所以每個影片片段都被指定了不同的內部函式,每個處理程式都會佔用記憶體。很明顯的每個被onEnterFrame處理程式附加的影片片段,不能共用相同的內部函式並不是很有效率的處理方式。此外,物件導向程式建議在原型建立類別的方法,取代在建構式內建立方法。如果在建構式內建立方法,那麼每次多一個類別實體,就會複製一次相同的函式。 記憶體浪費,不是記憶體漏失 (memory leak) 分辨這兩者的差異是很重要的。記憶體漏失是使用的資源不斷增加,可能造成系統當機的一種情況。 以Flash中的巢狀函式來說,並沒有記憶體漏失的問題,而是記憶體浪費。內部函式一直保有一個對外部函式的activation物件的參照,但是這是一個一對一的的關係。當內部函式被從記憶體中移除時(舉例來說,被附加內部函式的物件被移除),對activation的唯一的參照也被刪除,接下來垃圾收集器將會(應該)把內部函式和外部函式的activation物件同時刪除。所以儲存外部函式的activation物件所利用的資源就會被浪費,但不會一直增加下去,造成失控。 我們能夠印證嗎? activation物件的持續存在 是的!看看下面的程式碼:
在"test"函式中,"a"是一個區域變數,所以我們可以預期當我們呼叫"test"函式之後,"a"會消滅並從記憶體中釋放。然而,當我們執行上面的程式,呼叫物件"o"上的"meth"會輸出"5",這代表能夠在"meth"的範圍鏈當中找到"a",證實了"a"並未從記憶體中釋放。 你或許會以為找得到"a",因為在內部函式內有一個寫死的參照 (hardcoded reference)。Flash應該會做某件事情,確保特定的參照存在。如果是這樣確實不錯,但是實際上並非如此。外部函式的activation物件被存在內部函式的範圍鏈之內,而所有的區域變數都被存取。 使用eval,我們就可以動態的在範圍鏈內搜尋變數。讓我們看看下面這一段程式:
上面的程式中,"retriece"函式內的所有變數都沒有被寫死的參照,尚未透過變數名稱或使用eval前,我們就能夠取得所有那些我們以為已經被刪除的區域變數。雖然這些變數並未被主程式使用,但是它們仍然存在記憶體當中,造成資源的浪費。 關於複製與記憶體浪費 是的,再一次!讓我們看看下面的程式碼:
第19行顯示了雖然"o1"和"o2"的方法有這同樣的名稱,但是它們並不是參照到記憶體中的同一個函式。 第20行顯示雖然"o1"和"o2"物件藉由"theFunc"回傳的"txt"的屬性有相同的值,但是這些物件其實並不相同,代表"hello there"字串在記憶體中儲存了兩次,一個物件一次。 事實上,每次"addFunc"被呼叫,增加"theFunc"到某個物件上時,"Hello there"字串就在記憶體內被複製了。 我們可以怎麼做? 除非你對上面提到的巢狀函式特別感興趣,否則最好的方式就是在同一個階層建立所有的函式,且並且只在函式內部使用函式參照,以取代巢狀迴圈。 舉例來說,如果我們重寫前面列出的程式,像下面這樣:
就像你在前面所看到的,第17、18行輸出結果為undefined。這是因為,當"o1"和"o2"的"theFunc"被呼叫,Flash不能在範圍鏈內找到"aVariable",代表"addFunc"的activation物件沒有被加到這個方法的範圍鏈之內,且區域變數"aVariable"也如預期的被刪除了。 再者,第20行輸出的是"true",代表"o1"與"o2"的函式物件是相同的,只存了一份在記憶體當中。 記憶體空間為重要考量時,這個方法能後讓你的程式碼執行的更快速。基本上,這個方法不會把時間浪費在再記憶體中建立新的函式:記憶體配置、資料傳輸等等的時間。因此,當你使用巢狀函式時,每此你呼叫外部函式,你會使用較少的處理器循環來建立內部函式。先建立函式並且只在外部函式中設定簡單的指定,能夠讓執行效能大大的提升。 結論 結論包含範圍鏈與記憶體浪費。簡單來說,下列幾點是我們討論過的:
事實上,這件事十分單純。這個問題並不是很明顯。很多時候,你常常不知不覺中就浪費了系統資源。 在你開始改寫你所有使用巢狀函式的程式碼之前,我想澄清一點,在大部分的情況下,使用巢狀迴圈沒有什麼大不了的。在小案子裡,如果你的程式並不會佔用所有的系統資源,而且執行效能良好,就不需要再去改寫它了。當你的程式比較複雜時,我還是建議你去了解一下Flash如何執行你的程式碼。 我想當你處理大量的文字資料與巢狀函式時,會造成大量記憶體的浪費。字串能夠很快的達到好幾打甚至幾百個字元,所以如果你在區域變數裡面儲存了很長的字串,並且持續存在時,你就可能遭遇到問題了,如果只有簡單的整數或參照,那麼情況不會太糟。 讀了本文之後,對於檔案的整理、檔案大小、記憶體的使用與效率,別太掛心。繼續寫你的程式,只要記得有這回事就好:)。 謝謝你讀到這邊。我希望這篇文章對你有幫助。如果你發現任何錯誤(程式碼、事實、術語、文法、拼字等等)或是你有更適合的例子,或只是想與我分享一些新東西,請寫信給我。得到迴響的話,我會很開心的。 在離開之前,我想這個議題會引發出一些其他的問題,所以我在文末加了附錄這個章節。 最後一句話:一個特色總是建立在大量的練習之上。巢狀函式可能會造成記憶體的浪費,對一些應用來說,這可能會派上用場。舉例來說,private和static屬性能夠利用範圍鏈來實做;關於這個議題,可以參考另一篇文章。 參考資料 本文中大部分的概念來自欲FlashCoder's list中的討論串。最有趣的應該就是下面這篇: http://chattyfig.figleaf.com/ezmlm/ezmlm-cgi?1:sss:56601:200212:blejmgjoemfcdojimbmn#b
附錄 - 問題與解答 範圍鏈能有多深? 恩,我不知道 :)。我寫了五層的巢狀函式,它們的activation物件都存在。
既然我們能夠取的所有變數,那麼就表示每個函式的activation物件保留了最裡面的內部函式。 所以我也不知道範圍鏈能有多深,但是這數字肯定不小。但是在任何案子裡面,如果你已經用了五層的巢狀函式,我想你最好重新調整一下你的寫作策略! 範圍鏈與原型鏈 (prototype chain) 我們先前提到過當Flash搜尋變數時,會從範圍鏈最上端開始向下搜尋。這樣的機制稱為繼承鏈 (inheritance chain),而原型鏈也是另一種繼承鏈。本文雖然沒有討論OOP,但是如果想要了解Flash內如何實作OOP的話,可以參考Robin Debreuil的oline book。 概括的說,Flash內的每個物件都是類別的實體,特性是由其他類別繼承而來的。每個類別可能有它自己的屬性和方法,當你呼叫物件內的方法時,如果物件本身沒有提供,Flash就會向上尋找繼承鏈,直到找到方法或是到達最上層。 雖然範圍鏈是屬於Flash內部的操作,但是原型鏈卻能夠直接給開發者利用,經由 "__proto__"參照連結各物件之間的已存在原型鏈。 下面是實作的方法:
當Flash尋找變數"a"時,可以發現6未被指定為"addFunc"函式內區域變數"a"的值。那麼當"meth"被執行時,Flash做了什麼? 首先Flash在"meth"的activation物件內尋找"a",但是沒有找到。於是Flash尋找"meth"的activation物件是否有 "__proto__"參照。結果找不到 "__proto__"參照,因此Flash移動到範圍鏈的下一個物件,也就是"addFunc"的activation物件。在這邊還是沒找到"a"。於是,Flash尋找''__proto__"參照,找到了之後,在其中搜尋"a"。最後Flash在物件內的"__proto__"找到了"a",讀出它的值。 當沒有明確的範圍時,變數如何被設定? 我不知道是否應該先說明這個問題。這個問題並不複雜,但也不十分明顯。當你設定一個變數的值時,如"myVar=5;",Flash會到範圍鏈內的每個物件尋找是否有"myVar",除了_global。那個含有"myVar"物件(簡稱"o")的先被找到,則"o"物件內的"myVar"變數的值設定成5。 如果找不到"myVar"的參照,那麼Flash會建立一個新的參照"myVar"在範圍鏈最底層的物件裡。此物件只高於_global。 很顯然的,當我們設定變數的值,在範圍鏈內的物件的原型鏈未被檢查。所以儘管"myVar"參照存在於"o"的範圍鏈內,"o"內的"myVar"不會被設定新的值,除非範圍鏈內最後一個物件高於_global。 因此,要在_global物件內建立或設定屬性時,_global不能省略。 下面的程式碼說明了這種情況:
With與範圍鏈 "with"讓我又愛又恨。因為它與範圍鏈互相影響,與巢狀函式有許多類似之處。舉例來說,我們在前面討論過的,我們可以使用"with"和巢狀函式在不詳細指定範圍的情況下設定變數。然而,它們是有一些差異的。最大的差異是連結到函式的建立。我在稍早提到過當函式建立時,目前的範圍鏈會依附到函式上。就像我們所看到的,這是因為activation物件會持續存在。如果在函式建立時,目前的範圍鏈被附加到函式上,那麼代表我們能夠用"with"把物件加到函式的範圍鏈中。但是實際上卻無法運作。看看下面的程式碼:
在上面的程式碼中,我們使用"with"把物件放到範圍鏈的最上層。第4行顯示我們能夠取得"a"的值。根據目前的範圍鏈,我們建立一個新的函式(第5行)並將它指定為o2的一個新的方法。稍後,我們呼叫"o2.aMethod();",傳回undefined。表示範圍鏈內找不到"a",代表o1物件並未被加到函式的範圍鏈內。 當使用"with"將物件加到範圍鏈內時,函式建立時並不會將目前位置加入範圍鏈。 關於"this"參照呢? 至少在我的測試中,不管深度如何,"this"並不會被範圍鏈影響。"this"總是會參考呼叫函式的物件。 不同的是,函式的範圍鏈不會變化,代表函式中的"this"會隨著哪個呼叫此函式的物件而改變。我們可以回頭看看前面的例子:
"this"的意思是"呼叫函式的物件的參照"。我們稍早提到過,區域變數只是activation物件的屬性。如果使用var,我們附加函式到activation物件上,並在activation物件內執行這個函式,"this"參照會參考activation物件本身!依據這個情況,我們可能可以取得特定activation物件的參照,並將它存在其他地方(如果你可以找到這個寫法的用處)或在activation物件內使用陣列操作以取得或動態設定屬性(如果你可以找到這個寫法的用處)。
陣列操作與eval actionscript裡面的eval是不被建議使用的嗎?eval和陣列操作有什麼不同?Ralf Bokelberg在這篇文章中提供了問題的解答,引述如下: <quote> 你如何存取目標物件 是的,eval和陣列操作的最大差異在於eval能夠解決路徑的問題,而陣列操作只能取得物件的屬性。 根據我們之前討論過的,陣列操作與eval的另一個差異是它們的範圍鏈與原型鏈間的關係。使用eval或陣列操作取得單一變數(不是路徑)時,我們可以說陣列操作藉由搜尋物件的原型鏈取得變數;eval則是藉由搜尋目前的範圍鏈。再者,我們先前提到過,搜尋範圍鏈時也包含搜尋範圍鏈內每個物件的原型鏈。
你完全了解了嗎? 好啦,進入尾聲啦。根據我們先前討論過的,不使用Flash MX,你能夠知道下面這段程式碼輸出的結果嗎?
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
![]() |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|