1. 程式人生 > >HT圖形組件設計之道(三)

HT圖形組件設計之道(三)

忘記 ive 設計架構 垃圾回收 喜歡 進行 src 支持 優秀

上篇我們通過定制了CPU和內存展示界面,體驗了HT for Web通過定義矢量實現圖形繪制與業務數據的代碼解耦及綁定聯動,這類案例興許文章還會繼續以便大家掌握很多其它的矢量應用場景,本篇我們先切換個話題,談談模型-視圖-事件之間的關系。

圖形組件設計架構上主要就是在規劃Data模型。View視圖和Event事件之間的關系。這些年業界逐漸將各種GUI設計模式提煉成理論歸類。MVC、MVP和MVVM的主要大類常被統稱為MV*。有非常多文章進行各種設計模式的定義和比較,本篇不打算深入展開理論的討論,不同圖形組件設計架構都會有非常多差異,持續發展的組件事實上每時每刻都在進行著各種設計上的改進。相信有非常多不錯的組件已經創新出了很多其它新的更有用的設計模型,僅僅只是還未被提煉到理論高度進行歸類讓世人知曉,因此過細去定義什麽是P,什麽是VM,哪個功能應該寫在哪個部分才算合理我認為是沒太大意義的。僅僅要不斷改進產品,團隊能更好維護擴展,用戶易學易用就夠了。理論高度留給Martin Fowler這類神級大師去定義。

提到Martin Fowler由於他的《GUI Architectures》和《Presentation Model》是我較早見到將MVC和MVP理清的文章,從實現角度事實上幾十年前蘋果用於開發Mac OS X的Cocoa Bindings技術已採用了類似的設計,而且Objective-C語言的Key-Value Coding和Key-Value Observing機制。加上XCode工具的可視化支持,能夠說多年來早已讓眾多開發人員不知不覺在享受這些設計模型能帶來的開發力。

Java的Swing界面一直飽受詬病,但事實上非常早就有JGoodies這樣優秀項目,Swing本就不算大眾。了解JGoodies更是小眾,而更少人了解JGoodies Binding這多年前就實現得很不錯的MVP架構封裝,有興趣的讀者可看看JGoodies這篇06年的PPT《Desktop Patterns and Data Binding》。

Adobe的Flex和微軟的Silverlight/WPF本被業界寄予厚望,沒想這哥倆如匆匆過客被老東家拋棄了。但他們還是推動了MVP和MVVM設計模式的普及,現在HTML5領域的KnockoutJS、Backbone.js、AngularJS、PureMVC、Ember.js等眾多MV*框架假設雨後春筍般崛起。甚至須要有人專門維護個TodoMVC的站點來:Helping you select an MV* framework!

HT本身也是一套MV*的框架,但我們培訓客戶時非常少過細討論設計模式,在我看來好的組件封裝應該不必讓用戶糾結於你的設計模式。用戶幾個月不用你的框架後。依舊能高速上手不必有一個重寫學習的過程。這是我們最求的理想框架,從這個角度說眼下非常少有圖形框架能讓我們愜意,相信非常多人有類似痛苦的經歷,一段時間不用某套框架後。要用時全然忘記怎樣入手,Swing老手不看老代碼不知怎樣對JTree和JTable加入數據,Flex老手一下子想不起來invalidateProperties,invalidateSize和 invalidateDisplayList這幾個自己定義組件必掌握函數的細節。SL/WPF老手想不起來定義一個DependencyProperty屬性除了AffectsRenderer和AffectsMeasure還有多少要考慮的因素,上段提到的一堆新興的HTML5界MV*框架。相信更少有人敢說熟練精通,你可能在某個項目中用了好幾個月甚至一兩年,但一段時間不用你非常easy忘記。因此對喊出精通缺乏勇氣了。我認為這不是大家不聰明不勤奮。而是眼下的這些框架真還沒做到足夠好,我們一直努力讓HT朝我們認為愜意的方向發展,以後文章我再展開討論HT怎樣設計讓用戶不健忘的API接口。

回到今天模型-視圖-事件的話題。Data和View分離後必定須要有Event事件的監聽和派發機制來建立起數據綁定。我控制欲比較強不是非常喜歡AngularJS那種dirty checking的機制,有事件變化我希望立即被通知到,做我該做的處理,至於有人操心性能問題那是多慮了。圖形組件發展這麽多年已積累無數成熟技巧來規避事件的性能問題。

性能問題倒不用操心,畢竟這方面任務大部分情況都是交由框架實現者去考慮,但不須要用戶深入了解框架的實現細節,並不意味著用戶能夠全然不關系基本架構脈絡,框架應用者還是有必要了解模型-視圖-事件之間的引用關聯關系,否則easy出現內存泄露的問題,曾經經歷過一個客戶團隊設計的client框架。可管理全部界面的窗體,結果出現總是OOM的內存溢出,幫他們檢查後發現,他們有個全局的WindowManager對象,在每一個窗體創建時都會加入對窗體的引用,這樣固然貌似非常強大,全局都能夠控制全部界面窗體。但由於絕大多數開發者,不會在窗體關閉要銷毀時主動去刪除全局WindowManager對象的引用。進而導致了全部窗體都能被全局對象引用到而無法垃圾回收。因此框架的使用者還是有必要多框架的機制有所了解才幹避免這類的內存泄露問題。

非常多情況下內存泄露不是長期的執行也非常難發覺。但對於HT的Graph3dView這樣的基於WebGL的3D組件問題尤為明顯。由於大部分瀏覽器對單個頁面能執行的WebGL上下文是有限制的,比如PC上的chrome或firefox也就執行十五六個。手機平板等移動終端會更受限,因此假設出現內存泄露老的上下文沒關閉,超越上限時就會出現類型”Too many active WebGL contexts. Oldest context will be lost.”的異常。

下面我對《HT入門手冊》的第一個樣例做個擴展。對工具條添加了例如以下代碼邏輯的三個button,第一個button一下子創建了20個新的Tab頁,每一個Tab頁包括一個Graph3dView組件,另外兩個button實現刪除部分頁簽的功能。

{
	label: ‘Create 20‘,
	action: function(){                             
		for(var i=0;i<20;i++){  
			var tab = new ht.Tab();
			tab.setName(‘tab-‘+i);
			tab.setClosable(true);                                
			tabView.getTabModel().add(tab);    
			var g3d = new ht.graph3d.Graph3dView(dataModel);
			g3d.name = ‘g3d-‘ + i;
			window[‘g3d-‘ + i] = g3d;
			tab.setView(g3d);
		}                            
	}
},  
{
	label: ‘Destroy 5‘,
	action: function(){           
		var emptyModel = new ht.DataModel();
		tabView.remove(‘tab-5‘);
		window[‘g3d-5‘].setDataModel(emptyModel);
		delete window[‘g3d-5‘];  
		this.disabled = true;
	}
},
{
	label: ‘Destroy 6-10‘,
	action: function(){    
		for(var i=6; i<=10; i++){
			tabView.remove(‘tab-‘ + i);
			var emptyModel = new ht.DataModel();
			window[‘g3d-‘ + i].setDataModel(emptyModel);
			delete window[‘g3d-‘ + i];                                
		} 
		this.disabled = true;
	}
}

點擊創建20個頁簽的button分別打開頁簽之後系統的內存對象引用關系例如以下圖所看到的:

技術分享

由於dataModel作為全局對象被window應用著,並且其它新創建的頁簽中的Graph3dView都綁定了該數據模型。框架使用者應該了解,各種組件都對dataModel數據模型加入了事件監聽,事實上數據模型並不知道各種View的存在,數據模型僅遵循有數據變化後將事件正確的派發給全部消費者,而這20個Graph3dView就是當中的消費者。而Graph3dView中每一個有都有一個WebGL的context上下文。因而形成了一條從全局window到dataModel數據模型,再到Graph3dView組件。最後到WebGL上下文的引用關系網,這樣自然假設我們不主動斷開這個關系,哪怕Tab頁簽被關閉銷毀,Graph3dView依舊還會存在系統內存的問題(這個樣例我們為了測試方便事實上還在window上直接引用了Tab和Graph3dView對象)。

技術分享

因此由以上視頻你會發如今chrome下當點擊到第16個包括Graph3dView的頁簽後就出現了”Too many active WebGL contexts. Oldest context will be lost.”的異常。在WebGL中可通過對Canvas加入webglcontextlost的事件監聽可推斷自己的上下文被銷毀了,並可通過加入webglcontextrestored的事件監聽在瀏覽器資源足夠時又一次進行恢復。

在我們這個案例中要讓系統資源恢復,我們必須讓過多的Tab頁簽中的Graph3dView被徹底回收,因此工具條上的另外兩個button從代碼邏輯可知,我們將Graph3dView設置了一個新的空得DataModel數據模型。使其斷開了和全局window.dataModel的引用,當然Tab頁簽也得刪除。從以上視頻中也能夠看得出當我們銷毀了部分Tab頁簽後就能得到webglcontextrestored的事件恢復,因此第一個”HT for 3D Web”的頁簽經歷了webglcontextlost和webglcontextrestored的過程。

啟動初始化時僅僅有”HT for 3D Web”的第一個頁簽,因此通過Chrome的Debug Profiles可查看到ht.graph3d.Graph3dView的Objects Count項僅僅有1,通過Profiles的Retainers我們還能夠清楚的掌握眼下達到那些對象引用了Graph3dView對象:

技術分享

當點擊構建20個頁簽button後,Profiles能看到Objects Count為21:

技術分享

當我們點擊兩個刪除button銷毀6個Tab頁簽後發現。Objects Count下降到了15:

技術分享

最後能夠發現第一個HT for 3D Web的頁簽浴火重生了

技術分享

這個案例僅僅是為了測試方便因此將dataModel對象作為全局變量,所以引發了一些列內存泄露的資源不足問題,一般項目應用中不用的組件不須要考慮這麽復雜,比如還須要斷開dataModel引用這些步驟,常規應用場景中比如一個對話框打開後。一般數據模型和視圖組件都在這個對話框範圍內相互引用,僅僅要確保不出現上文提到的有全局引用能影響這個對話框內的某個對象。那麽你在使用完該對話框後不須要做不論什麽處理。那一堆的對象哪怕他們之間引用再復雜甚至互對應用,反正沒有全局對象可以再引用到他們,他們統統都會被銷毀。

總結下本篇的兩個觀點:

1、再好的封裝設計也須要使用者掌握主要的架構脈絡。就像再好的車你也得學會開學會主要的保養,什麽都不學的話,再好的框架也會像好車一樣被你開壞

2、不要懼怕MV*的事件和引用關系。理清事件機制和對象引用關系後。你能夠精確掌控不論什麽時刻的不論什麽內部細節。這點主要針對設計框架者而言,使用者應該大膽的擁抱MV*的框架,性能和各種潛在的內存問題放心的交給框架去解決


HT圖形組件設計之道(三)