HTML5觸屏版多線程渲染模板技術分享


 

前言:

了解js編譯原理的屌絲們都知道,js是單線程的,想當年各路神仙為了實現js的多線程,為了解決innerHTML輸出大段HTML卡頁面的頑疾,紛紛設計了諸如假冒的“多線程“實現,我自己也在寫開源框架KitJs時候,寫過類似的組件http://www.cnblogs.com/xueduanyang/archive/2012/05/30/2526422.html ,其原理就是改造代碼中的for為setInterval,改遞歸為尾遞歸等等,為可憐的刷新率60Hz爭取17ms的微弱時間。

當然了,這些都不是真正的多線程。其實W3C很早就有關於純前端真多線程實現的,就是http://www.w3.org/TR/workers/ ,一直以來打着HTML5的旗號,各大瀏覽器廠商都都有對應標准的worker實現,具體支持程度我們可以看http://caniuse.com/#feat=webworkers ,值得一提的是咱們的UC也支持WebWorker,這幾年隨着移動HTML5項目的興起,手機多核的炒作,現在就連華強北這種屌絲手機都開始以雙核為賣點了,而真正從APP或者Web為多線程做優化的項目又有多少開發者呢?

作為一個猥瑣的先驅者,本着讓用戶爽,不爽不舒服斯基的原則,我們率先在觸屏項目實現了多個多線程工作模型,本文介紹的多線程渲染模板就是其中的一個。

之前在春田花花群里面,曾今說過我們有個多線程渲染系統,遭到眾多同學的反擊,今天終於能給我一個機會,為大家展現我們的辛苦付出,歡迎大家看完之后熱烈討論。

架構設計:

上一篇介紹我們的調試系統的時候,我貼過一張咱觸屏的架構圖,

clip_image002

從圖中可以看出,在我們MVVM層次的Modules中,有兩個tpl引擎,一個是基本的tpl引擎(引入的是Mustache),被引用指向集成到我們的Common大模塊里面了,還有一個是獨立的多線程模板引擎(基於HTML5 WebWorker工作模式的雙核渲染引擎Mustache+JTemplate)。

該模塊名字叫做tplRender(有點土,起名一直是個技術活…),其他業務視圖模塊VeiwMddules只需要requires模塊,通過tplRender.tpl方法渲染模板,默認就會采用多線程工作方式。

我們的多線程工作模板的工作方式見下圖

clip_image004

首先,以Page當前頁面為一個Application生命周期,在引入該模板的時候,會自動為當前頁面初始化一個WebWorker對象,再加上本來的當前工作進程LocalMainThread,組成兩個等待作業池

當業務代碼執行到模板渲染時,會向tplRender模塊買一張門票,門票上記錄着將當前頁面需要執行模板的相關配置信息(模板所在Dom Element,模板內容,渲染依賴JSON Data,作業總數)以及此次提交給tplRender自動生成的一個MissionId

clip_image006

tplRender模塊在內部做協調調度,目前的調度算法實現還比較簡單,用的按2取模,均勻分幣多個模板渲染任務到Worker和Local(當前主線程)任務隊列中,分別計算

clip_image008

沒完成一個隊列子任務,會默認觸發一次回調,用於進度判斷,會傳遞總作業任務剩余數,只有當總業務進度為0時候,表示任務全部完成

clip_image010

計算完畢,收回門票,撕毀門票,告訴主線程去innerHTML渲染頁面走人。

clip_image012

這么做的好處是,

首先從原理上看,是將主線程需要同步計算的多個任務打散,分解一半到了異步Worker線程去計算,減輕了主線程的負擔

我們都知道

clip_image014

在瀏覽器中,渲染線程和當前Js主線程是或的關系,但是Worker以及請求和渲染線程是可以並行的,所以使用了異步計算模式既可以計算完一小塊即渲染,提高CPU的利用的率

由於是Worker異步計算,對於主線程的頁面事件響應,Gif動畫,js動畫,完全不存在卡住僵死的問題

在瀏覽器刷新中,有個重要的原則是盡量做局部刷新,不要做整體刷新,刷新范圍越小,速度越快,資源消耗越小,我們剛好切合這個原則

另外,可以最大限度的利用現代手機多核的優勢,不浪費資源

那么實際效果呢?

我們測試過簡單的10X單個1w次整數循環,對比的數據結果是單線程運算比多線程模式平均速度要慢上20%~30%,可以肯定的是,對於復雜的字符計算,正則,多重條件判斷等耗時運算,使用多線程雙工模式是一個不錯的選擇。

但是,這里要也要提的是,在簡單模板運算上,一般集中在300ms左右的計算中,多線程工作效率要比單線程要低5%左右的差距

為什么會有這5%的差距呢,因為從前面的代碼頁可以看到,因為異步的加入,導致我們需要維護一系列臨時狀態去保證正常的執行隊列,所以這部分消耗是正常的,另外,不要只看着這5%,要知道這是在犧牲用戶觸屏響應的前提上做的速度,而多線程方式雖然會慢一點,但是不會犧牲用戶觸屏的交互響應,也就是常說的不會卡死。

性能

之前群里面討論,主要集中在對於Worker工作模式的懷疑,下圖可以看到Worker工作模式的標准流程

clip_image016

Worker的主要消耗在於

1. 與主線程之間的通信,主要是通過PostMessage(發送),onMessage(接受),消息的回調都是異步的,且觸發通信都是在本地進行,消耗在90~100ms之間,此部分消耗是異步的,非阻塞的,不影響當前線程,底層並行實現,且經過多次實驗結果表明,與通信數據長度無明顯關系,就是說穩定維持在這樣的一個開銷,單個的話,最小要等這么長時間的,多次通信回調的話,非線程增長,消耗反而會降低(因為worker一旦加載到本地內存中,其實就是本地線程間通信,其速度應該是非常快的,主要耗費在底層實現的調度上)

2. Worker初始化加載,Worker是通過new Worker(url)的方式加載的,一個頁面一個worker進程,默認url走的是http請求,也就是說我們可以通過瀏覽器的Expires,過期頭走瀏覽器緩存,或者可以想辦法通過MainFest方式,走本地瀏覽器緩存

3. Worker自身的計算性能,這點通過PC和手機做了對比,發現一個很奇怪的現象,就是PC主線程的計算1w次循環的速度,一般要比worker中1w次循環的速度快5%~10%不等,這個地方估計要具體有機會看Webkit的實現源代碼,才能知道原因,不過一般情況,普通的計算,Worker計算速度與主線程基本無差

多線程計算中的共享對象問題:

一般情況下,我們不會遇到這問題,但是一些特殊情況,比如需要在Worker進程中取得主線程的Window下一些變量等等

我們現在是通過一個全局對象$ENV傳遞給Worker和LocalThread,在模板里面,都可以訪問到$ENV這個全局對象,只要在執行tplrender.tpl方法前,給這個$ENV對象賦值,既可以把值帶到模板里面去計算

當然這只是簡單的帶對象進去,還有做過把對象從Worker中帶出來,比如一些特殊需求,需要批量計算步長,瀑布流模式下的索引,埋點上報的ytag自增長等等,在Worker和LocalThread並行同步計算時候,Worker里面對於這個ytag的修改,對於LocalThread也需要有影響,那么就需要解決Worker與主線程的變量同步問題

我們目前采用的方式是單一任務步長變量同步(所謂單一任務,就是一次tplrender.tpl開始的記一次任務,一次任務里面根據需要渲染的元素多有少個,會拆分到不同小子任務,分布到Worker或者LocalThread中去,但是這些子任務一起算一個大的任務),其做法是

clip_image018

clip_image020

我們會在主線程,定義一個全局變量$ENV,在$ENV下掛一個step命名空間

在我們的業務代碼里面,默認給每個頁面起一個步長的變量ytag=1001

clip_image022

在模板代碼里面使用自定義方法去遞增步長

clip_image024

這時候,在當前頁面的生命周期中,在worker進程和主進程,同一個tpl任務中,worker與主線程各自的ytag是各自自增長,當任務完成時候,會更新當前頁面的$ENV.step.ytag到最新最大的值

clip_image026

完成ytag變量的自增長

那如何保證ytag自增長值不會重復呢?我們通過方法判斷當前模板計算是在主線程還是在worker里面走不一樣的增量即可實現

多線程計算中自定義方法共享:

我們做模板的意義,在於在MVVM框架中,將顯示的邏輯從VM中最大限度的剝離出來,直接放於View中,最大限度的解放Modules(其只需要與后端通信,對於取回的數據不需要做過多的處理),因為在業務開發中,View是千變萬化的一層,而后端一般不需要大動,所以我們將容易變化的邏輯,比如數據篩選,數據格式,循環顯示等等邏輯統統放在了View層進行,我們也引入了Mustache+JTemplate雙模板引擎解析的雙工模式,去處理帶有業務邏輯的模板代碼,由於JTemplate的引入,使得我們的模板支持js語法,支持了js語法就意味着我們的一些業務邏輯,就可以抽象成模板方法去復用。

所以這就帶來一個問題,我們是怎么復用我們的自定義模板方法的

首先我們實現了一個自定義模板方法對象,所有的符合我么業務要求的自定義模板內使用的js方法都放入這個對象中

clip_image028

第二,我們改造了JTemplate實現,在原有的基礎上,修復了”帶來的bug,且傳入了tplFn這個對象供模板內js使用

第三,我們在Worker實現了最小版本的Require和define,用於適應我們現在r.js的打包工作,實現worker需要的方法自動打包合並

clip_image030

這樣一來,在Worker和本地線程都有tplFn這個方法,也就都可以使用自定義方法了

為什么要整合支持Js語法的模板引擎:

其實前面已經說了,我們要將顯示的業務邏輯,放到模板里去,簡單的模板功能太少,不能發揮並行計算優勢,另外對於顯示邏輯前置化也是大事所趨,后端只需要關注大數據,對於顯示邏輯安排,全部交給前端來做即可。

所以我們在采用Mustache語法系作為我們的基本模板引擎后,加入最小版本的js語法解析引擎jtemplate,在修復了眾多bug之后,並入了我們的模板解析中。

為什么模板解析要用大括號開頭的{%和{{:

因為{{是Mustache默認的符號,使用jtpl原來的<%,一個是好多HTML編輯器對於<開頭會認為是HTML標記,導致顯示不正確,因為我們目前和重構的合作模式是重構可以直接修改我們的HTML代碼,使用{%,可以最大限度的還原原來重構的頁面,及時用瀏覽器打開我們的模板,也可以直接看到頁面

對於不支持多線程的如何處理:

首先我們代碼里面有容錯,和特診判斷,

clip_image032

其次,我們有是否使用Worker的開關,以及基本的Mustache引擎的tpl可以使用,不一定非是多線程渲染的方式

模板的預編譯:

其實理論上這段和多線程沒啥關系,既然說到了模板,那么就說說預編譯吧,其實預編譯沒啥神奇的地方,無非就是把html模板轉換成js相加的字符串功能,以提高eval這部分的性能,這個是一般模板引擎都會自帶的功能,比如Mustache 或者jtpl的預編譯功能,只是大多數同學現在研究在后端用nodejs跑這個預編譯,出來的直接是js 而不是模板了。

所以說我們也支持這種預編譯形式,由於worker的限制,不能傳遞引用類型的變量,但是在Worker內部空間里面,第一次執行后模板,被編譯后,在頁面的生命周期內是,是一直存在的,所以說第二次,第三次在渲染,使用的就是第一次編譯后的模板,這個加速是存在的。

當然了脫離頁面生命周期,這個預編譯也就失效了,在js主線程中,由於http緩存策略的原因,這種預編譯也還可以存在,但是真正對於真實需要大計算量的js計算,比如正則,條件判斷,這種預編譯起的效果值得考量,而多線程的工作模式正式為這種應用場景而生的。

后記:

后面我們會繼續分享無線前端開發特有的創新,海量離線本地存儲以及版本控制策略,敬請期待!模板技術只有結合本地存儲,把一個在線WebApp,變成一個真正的離線WebApp,才是真正的價值所在。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM