Flex 應用內存泄露的分析與診斷

FlashPlayer 虛擬機的垃圾回收機制

垃圾收集器採用計數法或標記法來查找需要清除的對象。計數法由於無法檢測循環引用的對象,現在已經很少採用了。重點談一下標記法。Flex 應用的對象在內存中被映射成樹形結構。這很好理解,每個 Flex 應用總有一個 Application 的入口被稱爲根節點(Root),垃圾收集器從根節點開始遍歷每個對象,對可達對象標記爲“有效”(有一種例外就是弱引用,後面的章節詳談)。而在這棵樹之外的孤島對象或者由於循環引用形成的孤島對象集合被標記爲“無效”,垃圾收集器會在合適的時間銷燬這些無效對象,完成一次垃圾收集。而垃圾收集器是運行在虛擬機中的一個低優先級的守護進程,爲了不影響性能,它只在必要的時候才運行。例如在向操作系統申請新內存空間的時候,發生異常的時候等等,因此內存並不是實時回收的。

Flex 內存泄露的原因

有了垃圾收集器,爲什麼 Flex 還會產生內存泄露呢?從垃圾收集器的角度看,對象分爲“有效”和“無效”兩類;而從 Flex 應用程序的角度看,對象又被分爲“有用”和“無用”兩類。

舉個例子,當程序出現邏輯錯誤需要提示用戶時,Flex 程序構造一個提示框,這時,提示框是一個“有用”的對象,當用戶點擊關閉按鈕關掉提示框後,提示框就變成“無用”的對象了,應用程序再也不會用到它(下次出現相同邏輯錯誤時,程序又會構造一個全新的提示框)。應用程序認爲這個提示框應該被回收掉,但是因爲某種原因,存在一個從“有效”對象到這個提示框的引用,垃圾收集器顯然認爲提示框也是“有效”的。這個“有效”“無用”的提示框便造成了 Flex 的內存泄露。

開發過程中造成內存泄露的兩種情況

瞭解了 Flex 內存泄露的原因,從程序員的角度來講,對於對象引用的混亂管理是造成 Flex 內存泄露的人爲因素。Flex 開發中對於對象的引用分爲兩種:顯示引用和隱式引用,我們分別就這兩種情況討論一下它們是如何造成內存泄露的。

顯示引用

  1. 表達式 b=a,創建一個從 b 指向 a 的引用,當 a 變成無用對象時,由於還存在 b 對它的引用,所以 a 的內存不能被回收。在開發過程中,全局變量、靜態變量、特別是採用單例模式創建的對象,對其他對象的引用,如果不及時釋放都極易造成內存泄露。例如: 

    清單 1. 表達式顯示引用
    				
     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 所佔的內存不會被回收,造成內存泄露。

  2. 以對象爲參數的方法,在方法體內部創建了指向該對象的引用,沒有及時釋放而導致內存泄露。將上面的代碼變化一下: 

    清單 2. 以對象爲參數的方法
    				
     var chart : AreaChart = new AreaChart(); 
     leak(chart); 
     chart = null; 
     ......
     public static var staticVar : Object = new Object(); 
     public function leak(chart : AreaChart):void{ 
     staticVar = chart; 
     } 
    

    原因和上例相同,只是發生的位置更加隱蔽。

  3. 對象自身的無參數方法調用,在方法體中創建對“this”關鍵字的引用,沒有及時釋放而導致內存泄露。下面這段程序是 UIComponent 的 setFocus() 的源代碼: 

    清單 3.UIComponent.as
    				
     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 方法的引用,如下圖所示,


圖 1. 註冊事件監聽器引用圖
圖 1. 註冊事件監聽器引用圖 

即使把 b 置爲 null,只要 a 對象沒有被回收,b 也不會被回收,從而導致內存泄露。通過弱引用方式可以避免這種內存泄露,在後面的章節會有詳細描述。

Flex Builder Profiler 工具簡介

Adobe 公司在 Flex Builder 3 中提供了一個 Profiler 工具,用於 Flex 內存診斷和性能調優。本文重點介紹 Profiler 用於內存診斷方面的功能。

Profiler 的啓動

在 Flex 開發視圖,選擇主入口文件並點擊鼠標右鍵 ->Profile As->Flex Application 啓動 Profiler 工具。如下圖所示:


圖 2. 本地啓動 Profiler
圖 2. 本地啓動 Profiler 

Profiler 參數配置

啓動後,系統首先彈出對話框讓用戶配置 Profiler 的參數,如下圖所示:


圖 3. Profiler 參數配置
圖 3. Profiler 參數配置 
  • 選項“Enable performance profiling”是用做性能調優的,主要用來找到響應時間的瓶頸。在做內存調試時,將這一項勾掉。
  • 選項“Generate object allocation stack traces”選項可以跟蹤對象創建的整個過程,這個功能非常消耗系統資源,在調試的初期,目的是找到內存泄露的對象,而不關心它的創建過程,因此先不要選擇該項。

配置完畢,點擊 Resume 按鈕繼續執行。

幾秒鐘之後,Profiler 工具開始運行,如下圖所示:


圖 4. Profiler 運行主界面
圖 4. Profiler 運行主界面 

Profiler 常用功能

“Profile”窗口顯示正在運行的 SWF 應用,選中後,窗口上的一系列的按鈕就變成可用狀態,如下圖所示:


圖 5. Profiler 窗口
圖 5. Profiler 窗口 

這裏簡要介紹一下和內存調試相關的按鈕的功能:

圖 5 中 1“Run Garbage Collector”,點擊該按鈕,會強制執行一次內存回收。

圖 5 中 2“Take Memory Sanpshot”,在運行的任何時刻,點擊該按鈕,系統首先會自動執行一次強制內存回收,然後捕獲一幀內存快照,作爲當前正在運行的應用的子對象,如下圖所示:


圖 6. 捕獲內存快照
圖 6. 捕獲內存快照 

雙擊這幀內存快照,會在下部窗口打開一個頁籤,顯示該內存快照中的對象,包括對象所在的包,實例數目和百分比,所佔內存和百分比,如下圖所示:


圖 7. 內存快照中的對象
圖 7. 內存快照中的對象 

圖 5 中 3“Find Loitering Objects”,找出“遊蕩對象”。該按鈕需要同時選中兩幀內存快照(按住 Ctrl 鍵點選內存快照)纔有用,如下圖所示:


圖 8. 選中兩幀內存快照
圖 8. 選中兩幀內存快照 

它的作用是通過對比兩幀內存快照,找出在後一幀內存快照中存在而在前一幀內存快照中 不存在的對象。在某些特定的場景中,該功能能夠迅速找到內存泄露的對象。

利用 Profiler 工具診斷內存泄露

內存快照對比法診斷內存泄露

Flex 應用內存泄露的最直觀表象是當用戶進行某些相同操作時,內存和對象實例會持續增加,即使進行了垃圾回收,內存也不會回到原始的水平。

在上一章節中,我們瞭解了 Profiler 的主要功能,它可以在任何時刻捕獲當時的內存快照。那麼可以想象,如果每當用戶進行一次相同的操作時,我們就捕獲一幀內存快照,通過對比幾幀快照,找出持續增加的對象實例,就可以發現是哪些對象導致了內存泄露,從而發現程序中的漏洞,最終解決問題。

我們通過一個例子來實踐一下這個假設。本文提供了一個模擬生成驗證碼的 Flex 程序 DetectMemoryLeak.mxml,如下圖所示:


圖 9. 模擬生成驗證碼
圖 9. 模擬生成驗證碼 

每當用戶點擊“Change”按鈕時,就會隨機產生一個 4 位數字替換原來的數字。該程序在用戶每次點擊按鈕後都會導致內存的增加,我們懷疑這個程序存在內存泄露。啓動 Profiler 工具,用戶每點擊一次“Change”按鈕,我們就捕獲一幀內存快照,連續做 3 次,我們對比一下這 3 幀快照,如下圖所示:


圖 10. 3 幀內存快照對比
圖 10. 3 幀內存快照對比 

通過對比,我們發現 NumberChangeLabel 的實例數目隨着用戶的每次點擊都會增加,並且不會被回收,因此斷定是 NumberChangeLabel 導致了內存泄露。雙擊最後一幀內存快照中的 NumberChangeLabel 對象,會打開“Object References”頁籤,顯示這 4 個實例被哪些對象引用,如下圖所示:


圖 11. 對象引用圖
圖 11. 對象引用圖 

表中的 4 條數據代表這幀內存快照中有 4 個 NumberChangeLabel 實例,每個實例後面括號中的數字,比如(7),代表了有多少個引用指向這個實例。點擊“+”號,逐項展開,如下圖所示:


圖 12. 分析對象引用
圖 12. 分析對象引用 

我們發現絕大部分是對象自身或者對象的子元素對該對象的引用,這種引用不會產生內存泄露。而有一項 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”, 如下圖所示:


圖 13. 初始狀態
圖 13. 初始狀態 

點擊“Popup a dialogue”彈出對話框,如下圖:


圖 14. 中間狀態
圖 14. 中間狀態 

點擊“OK”按鈕關閉對話框之後,到達終止狀態。在初始狀態和終止狀態各捕獲一幀內存快照,選中兩幀快照點擊“Finding Loitoring Objects”按鈕,打開“Loitoring Objects”頁籤,如下圖:


圖 15. 遊蕩對象
圖 15. 遊蕩對象 

產生了很多遊蕩對象,別擔心,大部分對象都是由一個或幾個高層對象產生的,找出產生內存泄露的高層對象就能解決問題。我們發現遊蕩對象“LeakDialogue”是程序中的一個用戶自定義對話框,它可能是產生了內存泄露的根源。雙擊這個條目打開“Object Reference”頁籤,如下圖:


圖 16. 遊蕩對象 LeakDialogue 的引用
圖 16. 遊蕩對象 LeakDialogue 的引用 

點擊“+”號逐項展開,通過分析發現,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 會爲事件和事件處理創建內部對象,這些對象不會引發內存泄露,因此只需要關注有沒有高層控件成爲遊蕩對象就可以了。

開發中避免內存泄露的幾點建議

我們暫且把需要回收的對象稱爲臨時對象,那麼從根本上講,對於臨時對象引用的管理不當是引發內存泄露的根本原因,以下是幾點關於如何避免內存泄露的建議。

  1. 對於顯示引用,要儘量減少對臨時對象的引用,尤其是全局變量,靜態變量,使用單例模式創建的變量對臨時變量的引用。這些變量包含 stage,systemManager,application, MVC 框架中 Model 和 Controller,還有以 Manager 命名的對象等等。另外,臨時變量本身要儘量做到高內聚性,對象內部儘量減少對外部對象尤其是全局對象的依賴。
  2. 對於隱式引用,使用弱引用方式註冊事件監聽器,將最後一個參數 useWeakReference 設置爲 true:a.addEventListener("Leak", b.leakHandler, false, 0, true); 這樣做的結果是垃圾回收器在做標記時,會忽略 a 對於 b 的引用,如果 b 沒有被其他對象引用,垃圾回收器就把它標記爲“無效”進而回收,從而避免內存泄露,如下圖所示:

圖 17. 弱引用示意圖
圖 17. 弱引用示意圖 

另外,自引用方式 a.addEventListener("Leak", this.leakHandler); 子對象對父對象的引用方式 children.addEventListener("Leak", parent.leakHandler); 都不會產生內存泄露。

  1. 對於自定義組件要儘量早地做內存泄露的測試。如果等到集成之後再做整體的測試,系統的複雜度變高,組件的層次和關聯過於複雜,要定位到問題的根源,是一件非常費時費力的事。
  2. Flex 還是一門比較新的技術,Flex 框架本身也存在一些內存泄露的問題。好在 Adobe 已經將它的 Bug 系統公開,開發人員可以到 http://bugs.adobe.com/去查找已經存在的內存泄露問題和一些官方的解決方案,相信對解決內存泄露問題會有所幫助。

結束語

本文從 Flashplayer 的垃圾回收機制談起,分析了 Flex 產生內存泄露的原因,以及開發中常見的內存泄露的場景,介紹瞭如何利用 Flex Builder 自帶的 Profiler 工具分析和診斷內存泄露,最後根據實際經驗給出了一些避免內存泄露的建議。希望通過本文能給開發人員一些啓示,開發出更健壯的 Flex 應用。

發佈了115 篇原創文章 · 獲贊 5 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章