SWT 和 JFace 提供了幾個不同的類,幫助您在幾個緩存中管理 GDI 資源。 緩存往往比您想的更靈巧。如何以及何時使用緩存并不總是顯而易見的。 設計時應該注意的幾個問題是:
GDI 泄漏總是不可接受的,必須進行處理。
處理完泄漏后,您應該考慮下面兩個問題:
應用程序需要的總共的 GDI 資源數目。
創建這些資源導致的開銷。
總共的 GDI 資源數目
您需要清醒地了解應用程序所需要的總共的 GDI 資源數目, 以及有多少資源是副本。 副本相當重要,因為您只要有可能應該共享 GDI 資源以便降低應用程序使用的資源數目。 很容易創建副本,而且您可以都沒有意識到(我曾修改了 Sleak 工具使之發現副本, 并將此改變以及其他有用的改變添加到 Eclipse 中。)
創建 GDI 資源所需的開銷
一般而言,創建字體和圖像耗費的資源比創建顏色多。 根據應用程序的不同,圖像的創建可能成為某些用戶動作的重大開銷。 如果遇到這種情況,您可以考慮使用一些 SWT/JFace 所提供的緩存。
如果可能,應讓平臺來管理資源。當您 在平臺擴展中指定圖像或圖標屬性時 —— 比如視圖、動作等 —— 平臺負責保證資源被正確地創建和刪除。 好的代碼往往是您無需編寫并維護的代碼。 上述提示的合理推論是可能時使用當前平臺的字體和圖像以提高共享。
只要有可能,應共享資源。為此,行之有效的方法是把資源集中到 一個公共包中。您可以將此視為重構公共資源。
每個 UI 包都有一個與之相關的 ImageRegistry。 該注冊項可用于存儲常用圖像。這里的關鍵是常用。我曾見到過開發人員把所有的資源都 放到了這個注冊項內,這并不合適。該注冊項維護著一個由名稱 > 圖像或名稱 > 圖像描述符構成的映射。 圖像描述符是對圖像的輕量描述;它們并沒有與之相關的任何 GDI。 您可以用圖像描述符提前得到圖像注冊項,那么當 首次需要用到該圖像時,注冊項會為您創建它。
對于較為不常用的圖像,您可以自行創建或刪除之,此外您還可以使用 LocalResourceManager。其構造函數的一種形式采用了小部件。 以此方式創建時,LocalResourceManager 會在銷毀小部件時清空 與之相關的資源。
偵聽器泄漏(Listener leaking)
偵聽器相關的泄漏是 UI 代碼常常出現的問題(請參閱 參考資料)。 偵聽器泄漏往往會浪費內存和時間。當您向一個對象添加偵聽器時, 您是在小部件和偵聽器之間創建了一個直接的、牢固的關聯(參看圖 2)。只要小部件存在, 偵聽器及其引用的一切都會一直駐留在內存中。 當小部件或它的父容器被關閉, SWT 刪除偵聽器,從而打破那個牢固的關聯。 我曾見過的很多代碼都說明了開發人員往往在這個問題上不甚清楚。
只要從中添加偵聽器的對象會及時刪除,您無需刪除 JFace/SWT 偵聽器。 關鍵是理解從中添加偵聽器的對象的生命周期。 不管什么時候要向某個對象中添加偵聽器,您都需要自問一下, 偵聽器被添加到哪個對象,偵聽器的生命期有多長。
舉例而言,假設應用程序創建了一個視圖。該視圖包含一個按鈕。 在您構建該視圖時,您為按鈕添加了一個選擇偵聽器,以便應用程序能夠對按鈕單擊作出響應。 您無需為刪除按鈕的偵聽器而對視圖添加一個刪除偵聽器, 您也無需為按鈕被撤銷時刪除按鈕的偵聽器而對按鈕再添加一個刪除偵聽器。 SWT 會在按鈕被撤銷時執行對按鈕偵聽器的刪除。您不必寫這些冗余的代碼和管理多余的工作。
在 RCP 應用程序中,經常會有某人創建一個視圖并將其自身添加為 workbench 頁偵聽器。 Workbench 頁 往往很長壽,直到該應用程序關閉,workbench 頁才會被關閉(從而清空偵聽器)。 在此情況下,您不應該依賴 workbench 頁清空偵聽器關聯。您應當在視圖被關閉時把該視圖作為偵聽器刪除。
我曾在一個聊天程序中看到過另一個受惑于對象生命周期的例子。 每當打開一個聊天窗口,都會向伙伴列表添加一個偵聽器。 聊天窗口永遠不刪除偵聽器,只要伙伴列表沒有被撤銷,不會有什么問題。 終結果是越來越多的偵聽器被添加到伙伴列表,而且它們永遠不會被刪除。 需要強調的是這不僅是內存泄漏,也是對性能的破壞。 偵聽器泄漏的后果是,每個聊天窗口以及它所有可訪問的對象都駐留在內存。 同時,每次當伙伴列表向列表內的偵聽器發信號,都會浪費時間去通知那些本來已經被關閉的聊天窗口。
還有一種常見的情形,是把偵聽器添加到偏好存儲以便您能夠在偏好改變時更新 UI。 我曾見過有開發人員在創建視圖或創建動作時添加偏好存儲偵聽器。 問題在于如果您不刪除偏好存儲偵聽器,您會導致偵聽器累積,因為 一般而言,偏好存儲只有在應用程序關閉時才會被關閉。
動作是一個特殊的例子。動作并不真的有生命周期。 它們被創建后,即使被撤銷或不再使用,您也并沒有對它有什么控制能力。 這意味著當您創建一個動作時,您一般不應該向其他對象添加偵聽器, 因為您并沒有好的方法以刪除那些偵聽器。
如何發現偵聽器泄漏
為發現偵聽器泄漏,我推薦兩個方法:
審查代碼:我會搜尋應用程序代碼中向對象添加偵聽器的位置,在那些位置上我認為 偵聽器的生存時間超過我的預期。對這些偵聽器列表后,我一般在運行時使用調試器 驗證我的假設。即使每個 addListener 都對應有 removeListener 也并不代表沒有問題, 因為開發人員往往會犯一個錯誤,是把 removeListener 包含到某個方法中, 他們以為該方法會被調用而實際上卻沒有。
使用剖析器或差異分析,按如下步驟:
啟動應用程序。
預熱。
得到一個內存快照。
做 5 次動作(打開聊天窗口、讀取郵件消息等)。
得到一個內存快照。
分析應用程序對象的實例數目。 如果有偵聽器泄漏的話,比如說,您可能會發現有多出的 5 個本不應存在的偵聽器。
結束語
我希望本文能為您提供一些關于如何在不同構建之間度量應用程序中堆使用的想法, 還有幾個手工技術用于發現并處理不可避免的泄漏。 如果您還沒有做好準備,試著跟蹤應用程序在構建時耗費的資源量。 做了這些之后,您可以嘗試進行堆分析。 即便您一開始對于堆轉儲還做不了什么,在構建時收集這些轉儲對于日后使用具有極大價值。 開始收集堆轉儲之后,您能夠對域對象做差異分析。從小做起,當您熟悉這些技術后可以加入更多的分析。