最近小組在嘗試使用集團DinamicX的DSL,經過下發DSL模板,實現Flutter端的動態化模板渲染。咱們解決了性能方面的問題後,又面臨了一個新的挑戰——渲染一致性。咱們該如何在不下降渲染性能的前提下,大幅度提高Flutter與Native之間的渲染一致性呢?node
在第一版渲染架構設計當中,咱們以Widget爲中心,採用了組合的方案來完成DSL到Widget的轉化。這方面的工做在早期還算比較順利,然而隨着模板複雜度的增長,逐漸出現了一些Bad Case。服務器
咱們分析了這些Bad Case後發現,在第一版渲染架構下,沒法完全解決這些Bad Case,緣由主要爲如下兩點:架構
如需從根本上解決這些問題,咱們須要從新設計一套新的渲染架構方案,徹底理解並對齊DSL的佈局理念。ide
因爲DinamicX的DSL與Android XML十分類似,所以咱們將以Android的Measure機制來介紹其佈局理念。相信不少同窗都明白,在Android的Measure機制中,父View會根據自身的MeasureSpecMode和子View的LayoutParams來計算出子View的MeasureSpecMode,其具體計算表格以下(忽略了MeasureSpecMode爲UNSPECIFIED的狀況):佈局
咱們能夠基於上面這個表格,計算出每一個DSL Node的寬/高是EXACTLY仍是AT_MOST的。 Flutter若想理解DynamicX DSL,就須要引入MeasureSpecMode的概念。因爲第一版渲染架構以Widget爲中心,難以引入MeasureSpecMode的概念,於是咱們須要以RenderObject爲中心,對渲染架構作從新的設計。性能
咱們基於RenderObject層,設計了一個新的渲染架構。在新的渲染架構中,每個DSL Node都會被轉化爲RenderObject Tree上的一顆子樹,這棵子樹主要由三部分組成。測試
Render層爲咱們新版渲染架構中的核心層,用於表達Node轉化後的佈局規則與尺寸大小,對於理解DSL佈局理念起到了關鍵性做用,其類圖以下:優化
DXRenderBox是全部控件Render層的基類,其派生了兩個類:DXSingleChildLayoutRender和DXMultiChildLayoutRender。其中DXSingleChildLayoutRender是全部非佈局控件Render層的基類,而DXMultiChildLayoutRender則是全部佈局控件Render層的基類。ui
對於非佈局控件來講,Render層只會影響其尺寸,不影響內部顯示的內容,因此理論上View、ImageView、Switch、Checkbox等控件在Render層的表達都是相同的。DXContainerRender就是用於表達這些非佈局控件的實現類。這裏TextView因爲有maxWidth屬性會影響其尺寸以及須要特殊處理文字垂直居中的狀況,於是單獨設計了DXTextContainerRender。this
對於佈局控件來講,不一樣的佈局控件表明着不一樣的佈局規則,所以不一樣的佈局控件在Render層會派生出不一樣的實現類。DXLinearLayoutRender和DXFrameLayoutRender分別用於表達LinearLayout與FrameLayout的佈局規則。
完成新版渲染架構設計以後,咱們能夠開始設計咱們的基類DXRenderBox了。對於DXRenderBox來講,咱們須要實現它在Flutter Layout中很是關鍵的三個方法:sizedByParent、performResize和performLayout。
咱們先來簡單回顧一下Flutter Layout的原理,因爲以前已有諸多文章介紹過Flutter Layout的原理,咱們此次就直接聚焦於Flutter Layout中用於計算RenderObject的size的部分。
在Flutter Layout的過程當中,最爲重要的就是肯定每一個RenderObject的size,而size的肯定是在RenderObject的layout方法中完成的。layout方法主要作了兩件事:
爲了方便讀者閱讀,咱們將layout方法作了簡化,代碼以下:
abstract class RenderObject { Constraints get constraints => _constraints; Constraints _constraints; bool get sizedByParent => false; void layout(Constraints constraints, { bool parentUsesSize = false }) { //計算relayoutBoundary ...... //layout _constraints = constraints; if (sizedByParent) { performResize(); } performLayout(); ...... } }
能夠說只要掌握了layout方法,那麼對於Flutter Layout的過程也就基本掌握了。接下來咱們來簡單分析一下layout方法。
參數constraints表明了parent傳入的約束,最後計算獲得的RenderObject的size必須符合這個約束。參數parentUsesSize表明parent是否會使用child的size,它參與計算repaintBoundary,能夠對Layout過程起到優化做用。
sizedByParent是RenderObject的一個屬性,默認爲false,子類能夠去重寫這個屬性。顧名思義,sizedByParent表示RenderObject的size的計算徹底由其parent決定。換句話說,也就是RenderObject的size只和parent給的constraints有關,與本身children的sizes無關。
同時,sizedByParent也決定了RenderObject的size須要在哪一個方法中肯定,若sizedByParent爲true,那麼size必須得在performResize方法中肯定,不然size須要在performLayout中肯定。
performResize方法的做用是肯定size,實現該方法時須要根據parent傳入的constraints肯定RenderObject的size。
performLayout則除了用於肯定size之外,還須要負責遍歷調用child.layout方法對計算children的sizes和offsets。
sizedByParent爲true時,表示RenderObject的size與children無關。那麼在咱們的DXRenderBox中,只有當widthMeasureMode和heightMeasureMode均爲DX_EXACTLY時,sizedByParent才能被設爲true。
代碼中的nodeData類型爲DXWidgetNode,表明上文中提到的DSL Node,而widthMeasureMode和heightMeasureMode則分別表明DSL Node的寬與高對應的MeasureSpecMode。
abstract class DXRenderBox extends RenderBox { DXRenderBox({@required this.nodeData}); DXWidgetNode nodeData; @override bool get sizedByParent { return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY && nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY; } ...... }
只有sizedByParent爲true時,也就是widthMeasureMode和heightMeasureMode均爲DX_EXACTLY時,performResize方法纔會被調用。而若widthMeasureMode和heightMeasureMode均爲DX_EXACTLY,則證實nodeData的寬高要麼是具體值,要麼是match_parent,因此在performResize方法裏,咱們只須要處理寬/高爲具體值或match_parent的狀況便可。寬/高有具體值取具體值,沒有具體值則表示其爲match_parent,取constraints的最大值。
abstract class DXRenderBox extends RenderBox { ...... @override void performResize() { double width = nodeData.width ?? constraints.maxWidth; double height = nodeData.height ?? constraints.maxHeight; size = constraints.constrain(Size(width, height)); } ...... }
DXRenderBox做爲全部控件Render層的基類,無需實現performLayout。不一樣的DXRenderBox的子類對應的performLayout方法是不一樣的,這個方法也是Flutter理解DSL的關鍵。接下來咱們以DXSingleChildLayoutRender爲例子來講明performLayout的實現思路。
DXSingleChildLayoutRender的主要做用是肯定非佈局控件的大小。好比一個ImageView具體有多大,就是經過它來肯定的。
abstract class DXSingleChildLayoutRender extends DXRenderBox with RenderObjectWithChildMixin<RenderBox> { @override void performLayout() { BoxConstraints childBoxConstraints = computeChildBoxConstraints(); if (sizedByParent) { child.layout(childBoxConstraints); } else { child.layout(childBoxConstraints, parentUsesSize: true); size = defaultComputeSize(child.size); } } ...... }
首先,咱們先計算出childBoxConstraints。接着判斷DXSingleChildLayoutRender是不是sizedByParent。若是是,那麼DXSingleChildLayoutRender的size已經在performResize階段計算完成,此時只須要調用child.layout方法便可。不然,咱們須要在調用child.layout時將parentUsesSize參數設置爲true,經過child.size來計算DXSingleChildLayoutRender的size。但是咱們該如何根據child.size來計算DXSingleChildLayoutRender的size呢?
Size defaultComputeSize(Size intrinsicSize) { double finalWidth = nodeData.width ?? constraints.maxWidth; double finalHeight = nodeData.height ?? constraints.maxHeight; if (nodeData.widthMeasureMode == DXMeasureMode.DX_AT_MOST) { finalWidth = intrinsicSize.width; } if (nodeData.heightMeasureMode == DXMeasureMode.DX_AT_MOST) { finalHeight = intrinsicSize.height; } return constraints.constrain(Size(finalWidth,finalHeight)); }
1)若是寬/高所對應的measureMode爲DX_EXACTLY,那麼最終寬/高則有具體值取具體值,沒有具體值則表示其爲match_parent,取constraints的最大值。
2)若是寬/高所對應的measureMode爲DX_ATMOST,那麼最終寬/高取child的寬/高便可。
佈局控件在performLayout中除了須要肯定本身的size之外,還須要設計好本身的佈局規則。咱們以FrameLayout爲例來講明一下佈局控件的performLayout該如何實現。
class DXFrameLayoutRender extends DXMultiChildLayoutRender { @override void performLayout() { BoxConstraints childrenBoxConstraints = computeChildBoxConstraints(); double maxWidth = 0.0; double maxHeight = 0.0; //layout children visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) { if (sizedByParent) { child.layout(childrenBoxConstraints,parentUsesSize: true); } else { child.layout(childrenBoxConstraints,parentUsesSize: true); maxWidth = max(maxWidth,child.size.width); maxHeight = max(maxHeight,child.size.height); } }); //compute size if (!sizedByParent) { size = defaultComputeSize(Size(maxWidth, maxHeight)); } //compute children offsets visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) { Alignment alignment = DXRenderCommon.gravityToAlignment(childNodeData.gravity ?? nodeData.childGravity); childParentData.offset = alignment.alongOffset(size - child.size); }); } }
FrameLayout的佈局過程一共可分爲3部分
看了FrameLayout的佈局過程,是否以爲很是簡單呢?不過須要指出的是,上述FrameLayoutRender的代碼會遇到一些Bad Case,其中比較經典的問題就是FrameLayout的寬/高爲match_content,而其children的寬/高均爲match_parent。這種狀況在Android下會對同一個child進行"兩次measure",那麼在Flutter下,咱們該如何實現呢?
咱們先來看一個例子:
上圖的LinearLayout是一個豎向線性佈局,width被設爲了match_content,它包含了兩個TextView,width均爲match_parent,那麼這個例子中,整個佈局的流程應該是怎樣的呢。
首先須要依次measure兩個TextView的width,MeasureSpecMode爲AT_MOST,簡單來講,就是問它們具體須要多寬。接着LinearLayout會將兩個TextView須要的寬度的最大值設爲本身的寬度。最後,對兩個TextView進行第二次measure,此時MeasureSpecMode會被改成Exactly,MeasureSpecSize爲LinearLayout的寬度。
而常見的Flutter的layout過程爲如下兩種:
以上方案均不能知足例子中咱們想要的效果,咱們須要找到一個方案,在調用child.layout以前,便能知道child的寬高。最後咱們發現,getMinIntrinsicWidth、getMaxIntrinsicWidth、getMinIntrinsicHeight、getMaxIntrinsicHeight四個方法可以知足咱們。咱們以getMaxIntrinsicHeight爲例,來說講這些方法的用途。
double getMaxIntrinsicWidth(double height) { return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth); }
getMaxIntrinsicWidth接收一個參數height,用於肯定當height爲這個值時maxIntrinsicWidth應該是多少。這個方法最終會經過computeMaxIntrinsicWidth方法來計算maxIntrinsicWidth,計算結果會被保存。若是咱們須要重寫,不該該重寫getMaxIntrinsicWidth方法,而是應該重寫computeMaxIntrinsicWidth方法。須要注意的是這些方法並不是輕量級方法,只有在真正須要的時候纔可以使用。
或許你不由要問,這些方法計算出來的寬高準嗎?實際上每一個RenderBox的子類都須要保證這些方法的正確性,好比用於展現文字的RenderParagraph就實現了這些compute方法,所以咱們得以在RenderParagraph沒被layout以前,獲取其寬度。
咱們設計的Render層中的類也得實現compute方法,這些方法實現起來並不複雜,咱們仍是以DXSingleChildLayoutRender爲例子來講明該如何實現這些方法。
@override double computeMaxIntrinsicWidth(double height) { if (nodeData.width != null) { return nodeData.width; } if (child != null) return child.getMaxIntrinsicWidth(height); return 0.0; }
上述代碼比較簡單,再也不贅述。
那麼咱們能夠來解決例子中的問題了。咱們先經過child.getMaxIntrinsicWidth來計算每一個child須要的width。接着咱們將這些寬度的最大值肯定LinearLayout的width,最後咱們經過child.layout對每一個孩子進行佈局,傳入的constraints的maxWidth和minWidth均爲LinearLayout的width。
新版渲染架構使得Flutter能理解並對齊DSL的佈局理念,系統性解決了以前遇到的Bad Case,爲Flutter動態模板方案帶來了更多的可能性。
咱們對新老版本的渲染性能作了測試對比,在新版渲染架構下,咱們經過頁面渲染耗時對比以及FPS對比能夠發現,動態模板的渲染性能獲得了進一步的提高。
在渲染架構升級以後,咱們完全解決了以前遇到的Bad Case,併爲系統性分析解決這類問題提供了有力的抓手,還進一步提高了渲染性能,這讓Flutter動態模板渲染成爲了可能。將來咱們將繼續完善這套解決方案,作到技術賦能業務。
雙11福利來了!先來康康#怎麼買雲服務器最便宜# [並不簡單]參團購買指定配置雲服務器僅86元/年,開團拉新享三重禮:1111紅包+瓜分百萬現金+31%返現,爆款必買清單,還有iPhone 11 Pro、衛衣、T恤等你來抽,立刻來試試手氣!https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110
本文做者:閒魚技術
本文爲雲棲社區原創內容,未經容許不得轉載。