在本文中,將討論如下內容:
1、系統資源,為後面討論圖片資源做一鋪墊
2、SWT中的圖片資源管理
3、Display hook銷毀機制,JFace中圖片資源管理的重要基礎
4、JFace中的ImageDescriptor
5、JFace中的圖片資源管理(ImageRegistry)
6、JFace中圖片資源管理ImageRegistry所適用的場景和使用規則
7、Eclipse中插件share images機制
8、在Eclipse插件開發或者開發RCP程序時,使用圖片資源需要的注意事項
【系統資源】
眾所周知,Java開發人員在使用SWT/JFACE的時候,並不能借助於Java內置的垃圾回收機制來徹底完成 系統資源的清理(Java虛擬機只能幫助我們釋放虛擬機內存中的系統資源句柄引用對象)。在SWT中系統 資源對象的定級類型是org.eclipse.swt.graphics.Resource,在類型明確說明了“Resources created by the application must be disposed”,這也讓我們想起了關於Image使用的一句名言“誰創建,誰負 責”,當然,這個原則也同樣適用於其他類型的系統資源。
我們之所以如此關注系統資源的使用,尤其是臭名昭著的圖片資源,主要是因為我們怕了系統資源洩 漏引起的系統crash的問題。例如org.eclipse.swt.SWTError: No more handles異常有可能在我們試圖創 建圖片資源的時候發生,這說明當前系統句柄已經不足,造成這個問題的罪魁禍首當然是我們寫代碼的人 。
【SWT中的圖片資源管理】
我們直接看一下SWT中圖片資源類型的定義(org.eclipse.swt.graphics.Image),在類型說明中明確 指出了:“Application code must explicitly invoke the Image.dispose() method to release the operating system resources managed by each instance when those instances are no longer required”。我們再看一下另外一個我們熟悉的類型org.eclipse.swt.graphics.ImageData,我們可以將 其看作是Image對應的元數據模型對象,描述了具體創建Image需要的信息。
通過上面的說明,我們發現SWT唯一告訴我們的是:自己創建的圖片資源,自己負責去銷毀,通過調用 Image.dispose()。那我們在使用SWT的時候,應該如何釋放圖片資源呢?
我們知道SWT的widget在銷毀的時候,也會銷毀子widget,所以,覆寫你自己的Component對應的 dispose方法,將你使用的系統資源銷毀。目前,也只能這樣了~_~。如果覺得不滿意,接著看下面的 Display hook銷毀機制。
【Display hook銷毀機制】
在Display device中,我們看了如下一個hook接口:
/**
*Causesthe<code>run()</code>methodoftherunnableto
*beinvokedbytheuser-interfacethreadjustbeforethe
*receiverisdisposed.
*/
public void disposeExec (Runnable runnable) {
//注冊用戶自定義runnable,在display release的時候回調此runnable
將runnable注冊到disposeList
}
disposeList中的線程會在display release的時候被調用,如下:
/**
*Releasesanyinternalresourcesbacktotheoperating
*systemandclearsallfieldsexceptthedevicehandle.
*/
protected void release () {
……
//會執行用戶注冊的銷毀線程
if (disposeList != null) {
for (int i=0; i<disposeList.length; i++) {
if (disposeList [i] != null) disposeList [i].run ();
}
}
……
}
看來,SWT並沒有把事情做絕了,還是給開發者留下一條後路的。Display允許開發者注冊一個自定義 線程hook到Display的release過程,開發者可以用如下方式來確保開發者使用的系統資源在Display release的時候被銷毀:
display.disposeExec(new Runnable() {
public void run() {
//銷毀系統資源的邏輯代碼
image.dispose();
…….
}
});
以上方式其實也是JFace中圖片資源管理(ImageRegistry、ResourceManager)能夠確保Display release的時候能夠徹底釋放被ImageRegistry托管的圖片資源。
到這裡回顧一下,SWT中資源釋放的途徑吧:
1、覆寫相應Component對應的dispose方法。這有別於Display的hook機制,因為其能夠在Display運行 期間(未被release之前)就釋放掉系統資源,最好的方式。
2、利用Display的hook機制,確保在Display被release的時候能夠銷毀資源。注意,請不要過多依賴 此方式,因為很容易造成在Display被release之前,已經發生了系統crash的問題。
【JFace中圖片資源管理--ImageDescriptor】
前面我們已經見過SWT中的Image和ImageData類型了,在繼續下面的內容之前,我們先看一下在JFace 中我們最常用來創建圖片資源的一個工廠類:ImageDescriptor。在ImageDescriptor的類型說明中告訴我 們,有兩種使用ImageDescriptor創建圖片的方式,分別通過createImage和createResource接口, “There are two ways to get an Image from an ImageDescriptor. The method createImage will always return a new Image which must be disposed by the caller. Alternatively, createResource() returns a shared Image. When the caller is done with an image obtained from createResource, they must call destroyResource() rather than disposing the Image directly.” 。分析如下:
首先看一下createResource方式,ImageDescriptor是一種DeviceResourceDescriptor,後者的對外操 作如下:
/**
*Createstheresourcedescribedbythisdescriptor
*/
public abstract Object createResource(Device device) throws DeviceResourceException;
/**
*Undoeseverythingthatwasdonebyapreviouscalltocreate(...)
*/
public abstract void destroyResource(Object previouslyCreatedObject);
這也就是說,ImageDescriptor提供了createResource / destroyResource接口來負責創建和銷毀 Image資源。請注意這邊的一點,在孤立使用ImageDescriptor(沒有配合ResourceRegistry使用,例如 ImageRegistry)的時候,用戶還是要負責通過調用destroyResource來釋放創建的資源。
其次來看一下createImage的方式:
/**
*The returnedimagemustbeexplicitlydisposedusingtheimage'sdispose call.Theimagewillnotbeautomaticallygarbagecollected. */
public Image createImage(boolean returnMissingImageOnError, Device device) {}
這也就是說,ImageDescriptor提供的createImage的方式,也需要用戶來顯示的銷毀資源。那 createImage和createResource兩種方式之間的差別是什麼呢?稍微分析一下ImageDescriptor的這兩種創 建方式的實現,我們就可以看出來差別:
1、createImage每次都創建一個全新的圖片資源(圖片資源的創建是很耗時的~_~)
2、createResource的方式采用了緩存的方式復用已經創建過的資源,並不是每次都創建一個全新的資 源。這一點雖然帶來了性能的提高,但是並沒有解決圖片資源釋放的問題,倒是給開發者留下了一種假象 ,造成了隨便使用ImageDescriptor的問題,反而造成了大量的圖片資源(當然,更多的是由於調用 createImage的方式造成的,因為每次都創建一個全新的圖片資源)沒有釋放。
到現在為止,我們看到JFace已經對SWT中的圖片資源的管理做了一個小的補充:提供了 ImageDescriptor.createResource的方式,可以利用緩存效果,能夠減少不必要的圖片系統資源創建,而 且效率有所提高。關於如何釋放,可以參考SWT中覆寫Component.dispose的方式,例如在label provider 使用的圖片資源,可以覆寫對於provider的dispose方法,JFace框架會自動調用。
【JFace中圖片 資源管理--ImageRegistry & ResourceManager】
下面,我們接著看一下JFace中的 ImageRegistry的實現原理。
首先我們看一下JFace中的資源管理門面類(façade class) JFaceResources,我們由它來獲取我們的JFace ImageRegistry:
public static ImageRegistry getImageRegistry() {
if (imageRegistry == null) {
imageRegistry = new ImageRegistry(getResources(Display.getCurrent()));
}
return imageRegistry;
}
public static ResourceManager getResources(final Display toQuery) {
ResourceManager reg = (ResourceManager)registries.get(toQuery);
if (reg == null) {
final DeviceResourceManager mgr = new DeviceResourceManager (toQuery);
//向Display hook了銷毀線程
toQuery.disposeExec(new Runnable() {
public void run() {
mgr.dispose();
registries.remove(toQuery);
}
});
}
return reg;
}
分析了一下ResourceManager(DeviceResourceManager)的實現,我們發現 :DeviceResourceManager就是對DeviceResourceDescriptor(ImageDescriptor)進行了引用計數管理。 通過JFaceResources.getResources利用了前面說的Display的hook銷毀機制(注意,如果不通過 JFaceResources.getResources來獲取ResourceManager,則不會默認享受Display的hook銷毀機制,需要 自己向Display注冊),確保由被托管ImageDescriptor創建的殘留在系統中的圖片資源在Display release的時候會被徹底銷毀。核心方法如下:
create(DeviceResourceDescriptor descriptor)
//如果是首次注冊,創建引用技數,allocate資源並對資源進行緩存
//如果是已經注冊,增加引用技數,直接返回緩存的系統資源
destroy(DeviceResourceDescriptor descriptor) //將
//如果引用技術==1,通過調用deallocate徹底銷毀資源
//如果引用技術>1,削減引用計數(系統資源不會被銷毀)
那就是說,如果一個ImageDescriptor被ResourceManager托管了,那由它創建的資源(注意:通過 ImageDescriptor.createResource的方式)由兩種銷毀的途徑:
1、如果不通過JFaceResources.getResources的方式,單獨使用ResourceManager,則只能利用 ResourceManager的引用計數管理來銷毀資源(引用計數為0時),通過顯示調用 ResourceManager.destroy來削減引用計數。
2、如果通過JFaceResources.getResources來使用ResourceManager,則除了能夠使用到引用計數管理 資源,同時也默認使用了Display的hook銷毀機制,JFace的ImageRegistry也很好的利用了這一點。
現在回頭看一下ImageRegistry提供的核心操作,著重分析一下ImageRegistry在利用了 ResourceManager對ImageDescriptor進行管理的基礎上,做了那些補充:
put(String key, Image image) //注冊image
put(String key, ImageDescriptor descriptor) //注冊descriptor
Image get(String key) //獲取imge
ImageDescriptor getDescriptor(String key) //獲取descriptor
remove(String key) //取消注冊
dipose() //銷毀資源
通過對ImageRegistry簡要的分析之後,我們的結論如下:
1、如果以put(String key, ImageDescriptor descriptor)的方式注冊,ImageRegistry直接講 descriptor委托給ResourceManager委托管理,自己並不承擔管理任務。而且,ImageRegistry對這種方式 注冊的ImageDescriptor所創建的系統圖片資源的銷毀也委托給ResourceManager進行,並不是在以上自己 的dispose方法中進行,而是在ResourceManager.dispose方法中進行。
2、如果以put(String key, Image image)的方式注冊,ImageRegistry做了部分的補充管理,其將 image包裝進自己的OriginalImageDescriptor(ImageRegistry的一個內部類,繼承自ImageDescriptor, 對圖片資源本身增加引用計數)實現中,並對image本身進行了引用計數管理。同時,對這種方式注冊的 圖片資源的銷毀是ImageRegistry自己承擔的,在自身的dispose方法中完成。(注意,在ImageRegistry 的構造方法中,將ImageRegistry.dispose封裝為一個runnable注冊到了ResourceManage的dispose過程中 ,而ResourceManage.dispose已經在JFaceResources.getResources方法中被hook到了Display的資源銷毀 過程中)。
3、通過1和2的結論,JFace ImageRegistry對系統資源的銷毀已經做了兩手准備,
其並不希望用戶自己來銷毀資源(無論是通過Image.dispose還是ImageDescriptor.destoryResource ,或者ImageRegistry.dispose),當然,ImageRegistry允許通過remove接口來取消注冊。
JFaceResources
+提供hook機制
ImageRegistry
+自己管理部分資源
ResourceManager
+管理ImageDescriptor及其創建的資源
【ImageRegistry的適用場景和使用規則】
通過上面的實現原理分析,我們知道ImageRegistry並不歡迎用戶來過多地參與圖片資源的釋放過程, 所以ImageRegistry適用於如下場景:
1、決定共享和高度復用的圖片資源。這種資源一般是被使用的特別頻繁,同時,不急於銷毀,只要在 Display release的時候銷毀掉就可以了,所以既可以利用到圖片資源本身緩存的優勢(減少物理創建的 次數),又可以利用其Display的hook銷毀機制,確保會被銷毀。
2、用戶可以直接使用ImageRegistry(不通過JFaceResources.getImageRegistry的方式使用),復用 部分ImageRegistry的管理功能,開發自己的緩存策略,但是,要確保自己會在合適的地方調用 ImageRegistry.dispose方法來銷毀registry。Eclipse Workbench中的shared images機制就用了這一點 。
ImageRegistry的使用規則如下:
1、誰創建,誰負責。具體圖片資源的創建是由ImageRegistry負責的,用戶既然托管了,就不應該再 干預資源的釋放。而且,注冊進ImageRegistry的資源是共享的,一個用戶釋放了,會影響到其他用戶的 使用。當然,對於比較熟悉JFace ImageRegistry原理的開發者,可以參與到引用計數的管理,通過這種 方式,以安全的、不影響其他用戶使用的方式來間接參與釋放的過程。
2、非共享圖片資源請不要交由ImageRegistry托管。對於一個僅限於局部使用而且使用並不是十分頻 繁的圖片資源,這樣做不會帶來什麼好處,而且,尤其是對於不能參與到引用計數管理的初級用戶,這樣 做反而會使得一個本可以馬上釋放的圖片資源反而會一直占用,直到Display release的時候才銷毀。
3、要投入精力對ImageRegistry的key值進行管理,否則,會引起混亂。因為ImageRegistry本質上可 以看作Eclipse平台中的一個全局對象,對其含有的key列表的管理是再所難免。
【Eclipse中插件share images機制】
在Eclipse,一個插件可以暴露(expose)自己的圖片資源,以便提供給需要的插件使用,我們就稱它 為插件之間的share images機制吧。上面提到過了,這其實是部分復用了JFace ImageRegistry的管理機 制。
如何共享(可以參照Workbench插件的share images實現):
1、按照默認約定,創建一個ISharedImages接口,提供有意義key值
2、實現自己創建的ISharedImages接口,並結合ImageRegistry來管理圖片資源;並提供顯示的dipose 公共接口,負責釋放自己管理的圖片資源
3、在自己的插件中暴露ISharedImages
4、在合適時機,調用ISharedImages.dispose來釋放資源。這個時機一般選擇在Plugin stop的時候比 較合適,當然,也可以選擇在其他時機。
如何使用:
1、獲取目標插件的ISharedImages實現,並通過ISharedImages提供的key值來獲取特定的圖片資源。 以workbench插件share images為例:
PlatformUI.getWorkbench().getSharedImages().getImage(key)
2、暴露圖片資源的插件負責圖片資源的創建和銷毀,其他插件不要參與銷毀過程。換句話說,還是要 遵守誰創建、誰負責的原則。以workbench插件share images為例:
在workbench close的時候,會間接調用ISharedImages.dispose()。
【Eclipse中使用圖片資源的經驗總結】
1、堅持“誰創建,誰負責”的原則。分為如下:
a) 如果是用戶自己創建的,請自己釋放。例如通過覆寫Component對於的dispose方法、通過覆寫 label provider對應的dispose方法等等,這對於一些適用於局部的圖片資源較為適合;當然,也可以變 態利用Display的hook釋放機制(但是,一般對於長期使用的資源才會這樣做!!!)。
b) 如果是通過JFaceResources.getImageRegistry的方式使用ImageRegistry時,請不要釋放資源, 讓ImageRegistry自己解決。一般使用於比較頻繁使用的全局共享圖片資源,例如想保持風格統一的圖片 資源等。
c) 如果是使用了IShareImages的機制,請提供圖片資源的插件自己負責釋放。如何使用這種機制, 最好參照eclipse中已有的實現,保持風格統一,“有樣學樣”吧。
2、正確認識系統資源洩漏引起的crash問題的原因。導致原因有兩種:
a) 首先,是沒有釋放,導致洩漏。請參照上面的“誰創建,誰負責”的原則。
b) 其次,是釋放的過晚,導致積累過多。例如本來應該立即釋放的資源,反而通過ImageRegistry進 行了托管,同時有沒有控制引用計數的管理,導致到了Display release的時候才釋放資源。同樣道理, 本來不需要暴露給其他插件貢獻的圖片資源,反而暴露了,導致釋放過完等。
3、正確認識系統資源的創建和銷毀所帶來的時間消耗,這是從系統性能的角度考慮。例如,可以用 ImageDescriptor.createResource的方式替換原始的new Image的方式,減少創建資源過於頻繁和銷毀資 源過於頻繁所帶來的時間占用。對於需要長期使用的貢獻資源,可以使用ImageRegistry的方式等等。
4、對於特殊的場景,可以在參考以上原理(例如JFace中的圖片管理的實現原理分析)的基礎上自己 實現圖片資源的管理策略。這對團隊開發產品的情況下尤其適用,一方面可以優化管理策略,使之更切近 團隊應用;再者,可以減少JFace ImageRegsitry使用的復雜度,並減少誤用。例如,我們可以把插件間 share images的機制看成是對JFace ImageRegsitry的靈活使用。
5、無論使用那種管理策略(無論是來自eclipse還是其他),使用這前一定要仔細看API說明,並簡要 分析一下實現原理。對於做上規模的插件產品/應用來講,畢竟對圖片這種系統資源的管理太重要了!! !對於做較為簡單的開發,基本上本著“誰創建、誰負責”的原則,用完之後在自己感覺合適的地方銷毀 掉就可以了,完全可以不去碰JFace中的ImageRegistry那套東東,引來不必要的負責度和復用,尤其是對 於新手來說。
PS:文章是昨天趕出來的,沒有細看。有什麼錯誤之處,歡迎大家指出~_~