基於HTML5的WebGL應用記憶體洩露分析

基於HTML5的WebGL應用記憶體洩露分析

上篇(http://www.hightopo.com/blog/194.html)我們通過定製了CPU和記憶體展示介面,體驗了HT for Web通過定義向量實現圖形繪製與業務資料的程式碼解耦及繫結聯動,這類案例後續文章還會繼續以便大家掌握更多的向量應用場景,本篇我們先切換個話題,談談模型-檢視-事件之間的關係。

http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html

圖形元件設計架構上主要就是在規劃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框架,相信更少有人敢說熟練精通,你可能在某個專案中用了好幾個月甚至一兩年,但一段時間不用你很容易忘記,因此對喊出精通缺乏勇氣了,我覺得這不是大家不聰明不勤奮,而是目前的這些框架真還沒做到足夠好,我們一直努力讓HT朝我們覺得滿意的方向發展,以後文章我再展開討論HT如何設計讓使用者不健忘的API介面。

回到今天模型-檢視-事件的話題,Data和View分離後必然需要有Event事件的監聽和派發機制來建立起資料繫結,我控制慾比較強不是很喜歡AngularJS那種dirty checking的機制,有事件變化我希望馬上被通知到,做我該做的處理,至於有人擔心效能問題那是多慮了,圖形元件發展這麼多年已積累無數成熟技巧來規避事件的效能問題。

效能問題倒不用擔心,畢竟這方面任務大部分情況都是交由框架實現者去考慮,但不需要使用者深入瞭解框架的實現細節,並不意味著使用者可以完全不關係基本架構脈絡,框架應用者還是有必要了解模型-檢視-事件之間的引用關聯關係,否則容易出現記憶體洩露的問題,以前經歷過一個客戶團隊設計的客戶端框架,可管理所有介面的視窗,結果出現總是OOM的記憶體溢位,幫他們檢查後發現,他們有個全域性的WindowManager物件,在每個視窗建立時都會新增對視窗的引用,這樣固然貌似很強大,全域性都可以控制所有介面視窗,但因為絕大多數開發人員,不會在視窗關閉要銷燬時主動去刪除全域性WindowManager物件的引用,進而導致了所有視窗都能被全域性物件引用到而無法垃圾回收,因此框架的使用者還是有必要多框架的機制有所瞭解才能避免這類的記憶體洩露問題。

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

以下我對《HT入門手冊》的第一個例子做個擴充套件,對工具條增加了如下程式碼邏輯的三個按鈕,第一個按鈕一下子建立了20個新的Tab頁,每個Tab頁包含一個Graph3dView元件,另外兩個按鈕實現刪除部分頁籤的功能。

{
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個頁籤的按鈕分別開啟頁籤之後系統的記憶體物件引用關係如下圖所示:

因為dataModel作為全域性物件被window應用著,而且其他新建立的頁籤中的Graph3dView都繫結了該資料模型,框架使用者應該瞭解,各種元件都對dataModel資料模型新增了事件監聽,其實資料模型並不知道各種View的存在,資料模型僅遵循有資料變化後將事件正確的派發給所有消費者,而這20個Graph3dView就是其中的消費者,而Graph3dView中每個有都有一個WebGL的context上下文,因而形成了一條從全域性window到dataModel資料模型,再到Graph3dView元件,最後到WebGL上下文的引用關係網,這樣自然如果我們不主動斷開這個關係,哪怕Tab頁籤被關閉銷燬,Graph3dView依然還會存在系統記憶體的問題(這個例子我們為了測試方便其實還在window上直接引用了Tab和Graph3dView物件)。

http://v.youku.com/v_show/id_XNzU2MzYzODA4.html

因此由以上視訊你會發現在chrome下當點選到第16個包含Graph3dView的頁籤後就出現了”Too many active WebGL contexts. Oldest context will be lost.”的異常,在WebGL中可通過對Canvas新增webglcontextlost的事件監聽可判斷自己的上下文被銷燬了,並可通過新增webglcontextrestored的事件監聽在瀏覽器資源足夠時重新進行恢復。

在我們這個案例中要讓系統資源恢復,我們必須讓過多的Tab頁籤中的Graph3dView被徹底回收,因此工具條上的另外兩個按鈕從程式碼邏輯可知,我們將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個頁籤按鈕後,Profiles能看到Objects Count為21:

當我們點選兩個刪除按鈕銷燬6個Tab頁籤後發現,Objects Count下降到了15:

最後可以發現第一個HT for 3D Web的頁籤浴火重生了

這個案例只是為了測試方便因此將dataModel物件作為全域性變數,所以引發了一些列記憶體洩露的資源不足問題,一般專案應用中不用的元件不需要考慮這麼複雜,例如還需要斷開dataModel引用這些步驟,常規應用場景中例如一個對話方塊開啟後,一般資料模型和檢視元件都在這個對話方塊範圍內相互引用,只要確保不出現上文提到的有全域性引用能影響這個對話方塊內的某個物件,那麼你在使用完該對話方塊後不需要做任何處理,那一堆的物件哪怕他們之間引用再複雜甚至互相應用,反正沒有全域性物件能夠再引用到他們,他們統統都會被銷燬。

http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html

總結下本篇的兩個觀點:

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

2、不要懼怕MV的事件和引用關係,理清事件機制和物件引用關係後,你可以精確掌控任何時刻的任何內部細節,這點主要針對設計框架者而言,使用者應該大膽的擁抱MV的框架,效能和各種潛在的記憶體問題放心的交給框架去解決