天天看點

weex新版Layout引擎以及渲染邏輯探究weex新版Layout引擎以及渲染邏輯探究

原來weex sdk使用Facebook yoga進行基礎css布局,但是由于開源協定問題選擇基于Google的FlexboxLayout了自研,此處按下不表。

一言以蔽之,Layout引擎目的是通過遞歸的方式将節點的css屬性限制析構,然後計算出節點正确的位置等基礎屬性。也就是說需要先明确一點,Layout引擎隻負責計算外部傳進來一棵節點樹,僅此而已。

而想要研究整體的渲染機制,單是Layout引擎遠遠不夠,其中最起碼還包括:髒節點染色機制、更新機制、特殊節點處理、多屬性共同決定同一屬性的優先級等等。本文隻是我近兩周在解決上百個Layout引擎核心bug時管中窺豹有所得,部分細節處有感興趣的同學可以一同讨論。

如果是要表述整套渲染邏輯,涉及的細節處就未免太多太瑣碎了,是以,在這裡我隻選取部分重要的實作機制來講講這些機制是如何運作,又是如何配合将渲染工作處理完成的,同時也帶出我自己的一些思考,便于後續研究和探讨。

渲染引擎包括布局的核心計算,主要任務是計算某個節點樹。輸入某個節點A,輸出整棵以節點A作為根節點的節點樹。

抽象來看,計算某節點的僞代碼如下:

由此可見,節點的布局是通過計算NonBFC清單中子節點屬性以及自己本身的屬性限制決定的。而BFC因為不影響父親的布局,也不受父親flex屬性影響,是以,遞歸可得BFC下子樹的布局。

是以計算自身節點的布局主要就看measureNonBFCsIfDirty這個方法中怎麼處理的。

但是因為css中屬性本身就不是單一屬性決定布局的,而是由自己的屬性、父親的屬性、孩子的屬性等共同決定的,是以measureNonBFCsIfDirty隻是靠一次周遊遞歸計算很難得到正确的布局。是以,measure方法的結構思路如下:

其中flexLine就是flexbox布局中的主軸概念,如下圖所示:

weex新版Layout引擎以及渲染邏輯探究weex新版Layout引擎以及渲染邏輯探究

父親div1中有4個子節點,布局方式是橫向布局,虛線部分是flexline,可以了解為容納子節點主軸上的單排虛拟容器,此時,因為子節點橫向布局時受到父布局的寬度限制,是以會自動産生新的flexline以容納超出的子節點。

是以,measure方法主要包括了兩次計算。第一次遞歸計算了子節點布局,并從葉子節點往上計算得到父節點的flexline清單。然後根據計算中父節點的統計的flexline清單得到父節點自己的布局并檢查子節點是否需要擴充布局,需要則染色。然後第二次遞歸計算所有被染色的子節點得到最終布局。

至此,基本上整體遞歸的架構就出來了,所需的就是将flex中的屬性布局規則按照優先級填充進架構中,完善recursiveMeasureDirtySubNodes的計算邏輯,在此就不多贅述,對flex屬性感興趣的可以參考w3c标準。

對于div而言,本身的布局就是由自身style、父親的flex限制和孩子的大小綜合得到的,但是很多特殊節點的計算方式有些許不同,這種特殊節點的計算方式并不會記入Layout引擎中,是以Layout引擎提供了外部傳入measureFunc方法參與到節點計算的形式。下面挑幾個大緻講一下:

text元件一般作為葉子節點,但是絕大多數時候,它是由attributes中value的值和style中的lines等屬性決定布局的,是以,textComponent中需要将attributeString的計算方式的方法指針傳入Layout引擎中,先根據傳入的value得到文字布局,然後再根據自身style和父親限制決定最終布局。

在iOS中,list是一個非常特殊的元件。因為list元件的底層實作是UITableView,對于tableView而言,系統提供了一整套複用機制和cell管理機制,是以上層不能簡單的手動控制每個cell的布局,而是必須通過tableview的delegate委托事件進行控制。

是以,如果将list産生的子節點樹根據component樹邏輯加入到根節點下,由Layout引擎控制布局,則會與系統原生tableView布局機制沖突,失去複用甚至布局錯亂。但是,list下面的cell或者cell内部元素同時也是使用css屬性布局的,也應該滿足Layout引擎的邏輯規則。

那麼,兩者需要怎麼相容呢?需求上是ListComponent既要利用Layout引擎計算整體子樹的布局,但是又不需要從頁面的根節點遞歸控制到cell的大小位置,是以,思路如下圖:

weex新版Layout引擎以及渲染邏輯探究weex新版Layout引擎以及渲染邏輯探究

原有邏輯是将WXComponentManager管理下的node節點(也就是weexInstance下根節點)作為輸入填進Layout引擎,進而計算出整張圖的布局,并由WXComponentManager管理的Component樹遞歸布局好。

而新邏輯則是設定辨別位,讓listComponent原來持有的node節點變成root節點下的葉子節點,原來的cell節點不加入到root節點樹下,布局不會受到WXComponentManager的直接控制。ListComponent會建立一個新節點作為根節點,傳入Layout引擎中擷取了list下整個node樹的布局(普通的component隻維護一個node,ListComponent會産生兩個node)。然後ListComponent将計算完畢的list node樹委托給系統的UITableView的回調事件進行處理,由系統維護cell的初始化、複用、銷毀。

這樣做有兩個好處:

使用原生的機制,tableView不會一次性将所有cell渲染出來,節省開銷。

使用原生tableView,不需要重複實作tableView本身的各種複雜的機制。

但是這種實作方式還有局限:

cell的使用還隻是往上堆疊WXView,并沒有真正發揮cell的複用性。

因為分割了node樹,是以component樹和node樹并不是完全對應的,邏輯性有割裂,可能導緻後續更新Layout引擎布局邏輯時,元件需要是以不斷打更新檔。

但是,還是不得不說,現有的方案,在不需要重新實作一套tableView的情況下用最小的代價完成了整體的布局邏輯,還是比較巧妙的。

WXEmbedComponent是一個異類。它是weex sdk提供的元件中唯一一個自身持有了一個WXSDKInstance的元件,這就意味着它本身即是dom樹下的一個子節點,同時也是一個weex容器。它在設計之初就是為了實作weex頁面的嵌套。因為在現有業務中大量使用到tab形式實作會場頁面,這種頁面最大的特點就是tab作為導航,tab下有完整的整套頁面邏輯,是以最适合使用頁面嵌套的形式處理。

是以傳入一個頁面的url,embed元件就能夠通過内部的WXSDKInstance将子頁面渲染出來并加載到自身節點上。

随着界面的複雜度不斷增加,整個節點樹的複雜度會不斷增加,是以重新整理一次節點樹的布局耗費的時間成本就會不斷上升,此時,如果因為某個子節點或者葉子節點屬性發生變化,觸發重新整理的時候重新周遊計算了整一棵節點樹明顯不合适,會産生很多兄弟節點的重複計算。是以,必須有标志标示需要更新的節點,這就是髒節點染色機制。

現有的染色規則下,當節點A産生以下行為時會将節點A染色并遞歸染色此分支上節點A的所有直系祖先節點:

初始化節點style

移除或者增加節點下的子節點

更新了margin、padding、border、left、right、top、bottom、width、height等布局屬性

更新了position、flexDirection、flexWrap、alignItems、flex等布局方式

但是,上述四點隻是通用的節點染色機制,對于特殊節點,例如Slider更新資料、text更新value等内容發生變化也會導緻節點被染色。

首先得先說明一下整個weex大緻的運作,weex頁面是由WXSDKInstance的執行個體控制整個生命周期的,而管理頁面下整個component樹以及node樹的是WXSDKInstance執行個體中WXComponentManager對象。

WXComponentManager執行個體中持有了一個CADisplayLink對象,本質上就是持有了一個計時器,這個計時器與螢幕的繪制重新整理保持同步,即正常流暢情況下1/60s的頻次。在WXComponentManager對象執行個體被建立的時候它就會将CADisplayLink對象建立出來并加入到目前的RunLoop中,以每秒60次的頻率檢測目前component清單中是否有節點被染色。如果有,将根節點傳入Layout引擎中,重新計算目前布局中被染髒的節點以及對應的影響,然後視圖樹重新布局作為事務傳入UI事件隊列中排隊處理。關于weex事件隊列,下一節會有講述。如果UI事件隊列中1s内沒有任何事務傳入,CADisplayLink将會暫停,直到下次觸發ComponentTask才會被喚醒。

如下圖:

weex新版Layout引擎以及渲染邏輯探究weex新版Layout引擎以及渲染邏輯探究

這套機制保證了被染色的髒節點能夠及時被處理,同時又不會因為加入了CADisplayLink導緻目前RunLoop一直在空轉中。

在weex中,計算和觸發更新是一個頻繁進行的操作,如果這個時候在主線程中進行操作,很有可能使得主線程卡頓。同時,如果此時主線程已經被占用,事件的更新就會被阻塞,是以需要異步處理JsContext傳遞的更新事件。

但是僅僅使用GCD異步處理容易造成時序混亂的問題,是以,需要底層自身維護一個子線程用與事件的異步線性化處理。

weex本身維護了一個component線程,計算相關都在此線程完成,包括遞歸生成component樹和node樹、計算節點樹布局等。可以說,主體任務都是由component線程做處理的,當conponent線程處理完畢後得到整個布局樹後會切到UI線程進行布局設定。

現在的染色邏輯是對上的,假設發生更新的是節點A,在染色自身後A會逐級向父親節點染色,然後再由根節點逐級向下計算被染色節點布局更新。但是問題在于,此時的計算會涉及到發生更新的節點A,A的直系祖先節點,或者A的父親更新時影響到A的兄弟節點的布局,但是,對于A的子孫節點就無法被更新到了。如果此時A裡面嵌套了多層,某個子孫節點受到A的布局影響,A更新了,但是無法使得此節點得到更新,因為A的子節點判斷沒有被影響,A的更新邏輯就截止了。

現有的機制下,因為flex屬性,是以必須使得産生兩次節點樹的運算,咋一看,這種計算好像是無法避免的,因為需要統計flex限制,就必須先統計各個子節點的布局,然後根據flex屬性定義更新子節點布局限制,然後再重新計算節點布局。

但是換個角度思考,這種無法避免的原因是在于基礎屬性和flex屬性不是在一個次元的東西,或者說,基礎屬性是定值,而flex屬性更像是一種“限制”。限制依靠定值才能産生作用。

但是,能不能将這種“限制”換成某種規則的組合,然後在第一次計算的時候就使得限制也能夠計算進去呢?甚至是不是在這種規則下,在元件更新時候依照這種規則的組合可以直接更新到對應元件,不需要計算沒有被“限制住”的元件呢?這塊暫時隻有個想法,具體還沒有實作。有興趣的可以找我讨論下。

雖然因為一些特殊節點定制化的原因(例見上文list元件),node樹和component樹出現了不一緻的情況。這種情況現在由于定制化是以單獨處理了,但是這種分離助長了元件的特異性,出現了子Component單獨管理一棵節點樹的情況,而Manager此時并不能擷取到整棵節點樹,給後來屬性擴充埋下了相容性的隐患。

我覺得此時是不是可以利用代理模式,将系統原生的代理傳遞給cell這種形式,用以維持node樹的完整性呢。畢竟node樹隻是布局樹,不應該因為視圖的使用方式不同将node樹截斷或扭曲。想來可行性應該挺高的。

現有w3c的css屬性規則太複雜了,尤其是組合邏輯,造成Layout引擎的邏輯非常難以維護,并且牽一發動全身,應該用規則插件化的思想結構化這些規則,或者大部分規則,這樣可讀性和維護性應該都能提高,甚至單規則或者組合規則的單測都好做很多,可作為後期Layout架構優化的方向。

weex還在不斷更新中,網絡上類似的跨平台布局解決方案也在不斷豐富中,這塊探究其實還缺少個各個平台的對比,後續再開系列吧。