日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

做一个高一致性、高性能的Flutter动态渲染,真的很难么?

發(fā)布時間:2024/8/23 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 做一个高一致性、高性能的Flutter动态渲染,真的很难么? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

Flutter動態(tài)模板渲染架構(gòu)升級

? 最近小組在嘗試使用集團DinamicX的DSL,通過下發(fā)DSL模板,實現(xiàn)Flutter端的動態(tài)化模板渲染。我們解決了性能方面的問題后,又面臨了一個新的挑戰(zhàn)——渲染一致性。我們該如何在不降低渲染性能的前提下,大幅度提升Flutter與Native之間的渲染一致性呢?

挑戰(zhàn)與思路

? 在初版渲染架構(gòu)設計當中,我們以Widget為中心,采用了組合的方案來完成DSL到Widget的轉(zhuǎn)化。這方面的工作在早期還算比較順利,然而隨著模板復雜度的增加,逐漸出現(xiàn)了一些Bad Case。

? 我們分析了這些Bad Case后發(fā)現(xiàn),在初版渲染架構(gòu)下,無法徹底解決這些Bad Case,原因主要為以下兩點:

  • 我們使用了Stack來代表FrameLayout,Column/Row來代表LinearLayout,它們看似功能相似,實則內(nèi)部實現(xiàn)差異較大,使用過程中引起了很多難以解決的Bad Case。
  • 初版我們嘗試通過自定義Widget對DSL的布局理念做了初步的理解,但是未能做到完全對齊,使得Bad Case無法得到系統(tǒng)性解決。
  • ? 如需從根本上解決這些問題,我們需要重新設計一套新的渲染架構(gòu)方案,完全理解并對齊DSL的布局理念。

    新版渲染架構(gòu)設計

    ? 由于DinamicX的DSL與Android XML十分相似,因此我們將以Android的Measure機制來介紹其布局理念。相信很多同學都明白,在Android的Measure機制中,父View會根據(jù)自身的MeasureSpecMode和子View的LayoutParams來計算出子View的MeasureSpecMode,其具體計算表格如下(忽略了MeasureSpecMode為UNSPECIFIED的情況):

    ? 我們可以基于上面這個表格,計算出每個DSL Node的寬/高是EXACTLY還是AT_MOST的。 Flutter若想理解DynamicX DSL,就需要引入MeasureSpecMode的概念。由于初版渲染架構(gòu)以Widget為中心,難以引入MeasureSpecMode的概念,因而我們需要以RenderObject為中心,對渲染架構(gòu)做重新的設計。

    ? 我們基于RenderObject層,設計了一個新的渲染架構(gòu)。在新的渲染架構(gòu)中,每一個DSL Node都會被轉(zhuǎn)化為RenderObject Tree上的一顆子樹,這棵子樹主要由三部分組成。

  • Decoration層:Decoration層用于支持背景色、邊框、圓角、觸摸事件等,這些我們可以通過組合方式實現(xiàn)。
  • Render層:Render層用于表達Node在轉(zhuǎn)化后的布局規(guī)則與尺寸大小。
  • Content層:Content層負責顯示具體內(nèi)容,對于布局控件來說,內(nèi)容就是自己的children,而對于非布局控件如TextView、ImageView等,內(nèi)容將采用Flutter中的RenderParagraph、RenderImage來表達。
  • ? Render層為我們新版渲染架構(gòu)中的核心層,用于表達Node轉(zhuǎn)化后的布局規(guī)則與尺寸大小,對于理解DSL布局理念起到了關(guān)鍵性作用,其類圖如下:

    ? DXRenderBox是所有控件Render層的基類,其派生了兩個類:DXSingleChildLayoutRender和DXMultiChildLayoutRender。其中DXSingleChildLayoutRender是所有非布局控件Render層的基類,而DXMultiChildLayoutRender則是所有布局控件Render層的基類。

    ? 對于非布局控件來說,Render層只會影響其尺寸,不影響內(nèi)部顯示的內(nèi)容,所以理論上View、ImageView、Switch、Checkbox等控件在Render層的表達都是相同的。DXContainerRender就是用于表達這些非布局控件的實現(xiàn)類。這里TextView由于有maxWidth屬性會影響其尺寸以及需要特殊處理文字垂直居中的情況,因而單獨設計了DXTextContainerRender。

    ? 對于布局控件來說,不同的布局控件代表著不同的布局規(guī)則,因此不同的布局控件在Render層會派生出不同的實現(xiàn)類。DXLinearLayoutRender和DXFrameLayoutRender分別用于表達LinearLayout與FrameLayout的布局規(guī)則。

    新版渲染架構(gòu)實現(xiàn)

    ? 完成新版渲染架構(gòu)設計之后,我們可以開始設計我們的基類DXRenderBox了。對于DXRenderBox來說,我們需要實現(xiàn)它在Flutter Layout中非常關(guān)鍵的三個方法:sizedByParent、performResize和performLayout。

    Flutter Layout的原理

    ? 我們先來簡單回顧一下Flutter Layout的原理,由于之前已有諸多文章介紹過Flutter Layout的原理,我們這次就直接聚焦于Flutter Layout中用于計算RenderObject的size的部分。

    ? 在Flutter Layout的過程中,最為重要的就是確定每個RenderObject的size,而size的確定是在RenderObject的layout方法中完成的。layout方法主要做了兩件事:

  • 確定當前RenderObject對應的relayoutBoundary
  • 調(diào)用performResize或performLayout去確定自己的size
  • 為了方便讀者閱讀,我們將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方法。

    ? 參數(shù)constraints代表了parent傳入的約束,最后計算得到的RenderObject的size必須符合這個約束。參數(shù)parentUsesSize代表parent是否會使用child的size,它參與計算repaintBoundary,可以對Layout過程起到優(yōu)化作用。

    ? sizedByParent是RenderObject的一個屬性,默認為false,子類可以去重寫這個屬性。顧名思義,sizedByParent表示RenderObject的size的計算完全由其parent決定。換句話說,也就是RenderObject的size只和parent給的constraints有關(guān),與自己children的sizes無關(guān)。

    ? 同時,sizedByParent也決定了RenderObject的size需要在哪個方法中確定,若sizedByParent為true,那么size必須得在performResize方法中確定,否則size需要在performLayout中確定。

    ? performResize方法的作用是確定size,實現(xiàn)該方法時需要根據(jù)parent傳入的constraints確定RenderObject的size。

    ? performLayout則除了用于確定size以外,還需要負責遍歷調(diào)用child.layout方法對計算children的sizes和offsets。

    如何實現(xiàn)sizedByParent

    ? sizedByParent為true時,表示RenderObject的size與children無關(guān)。那么在我們的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;@overridebool get sizedByParent {return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY &&nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY;}...... }

    如何實現(xiàn)performResize

    ? 只有sizedByParent為true時,也就是widthMeasureMode和heightMeasureMode均為DX_EXACTLY時,performResize方法才會被調(diào)用。而若widthMeasureMode和heightMeasureMode均為DX_EXACTLY,則證明nodeData的寬高要么是具體值,要么是match_parent,所以在performResize方法里,我們只需要處理寬/高為具體值或match_parent的情況即可。寬/高有具體值取具體值,沒有具體值則表示其為match_parent,取constraints的最大值。

    abstract class DXRenderBox extends RenderBox {......@overridevoid performResize() {double width = nodeData.width ?? constraints.maxWidth;double height = nodeData.height ?? constraints.maxHeight;size = constraints.constrain(Size(width, height));}...... }

    非布局控件如何實現(xiàn)performLayout

    ? DXRenderBox作為所有控件Render層的基類,無需實現(xiàn)performLayout。不同的DXRenderBox的子類對應的performLayout方法是不同的,這個方法也是Flutter理解DSL的關(guān)鍵。接下來我們以DXSingleChildLayoutRender為例子來說明performLayout的實現(xiàn)思路。

    ? DXSingleChildLayoutRender的主要作用是確定非布局控件的大小。比如一個ImageView具體有多大,就是通過它來確定的。

    abstract class DXSingleChildLayoutRender extends DXRenderBoxwith RenderObjectWithChildMixin<RenderBox> {@overridevoid performLayout() {BoxConstraints childBoxConstraints = computeChildBoxConstraints();if (sizedByParent) {child.layout(childBoxConstraints);} else {child.layout(childBoxConstraints, parentUsesSize: true);size = defaultComputeSize(child.size);}}...... }

    ? 首先,我們先計算出childBoxConstraints。接著判斷DXSingleChildLayoutRender是否是sizedByParent。如果是,那么DXSingleChildLayoutRender的size已經(jīng)在performResize階段計算完成,此時只需要調(diào)用child.layout方法即可。否則,我們需要在調(diào)用child.layout時將parentUsesSize參數(shù)設置為true,通過child.size來計算DXSingleChildLayoutRender的size。可是我們該如何根據(jù)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的寬/高即可。

    布局控件如何實現(xiàn)performLayout

    ? 布局控件在performLayout中除了需要確定自己的size以外,還需要設計好自己的布局規(guī)則。我們以FrameLayout為例來說明一下布局控件的performLayout該如何實現(xiàn)。

    class DXFrameLayoutRender extends DXMultiChildLayoutRender { @overridevoid performLayout() {BoxConstraints childrenBoxConstraints = computeChildBoxConstraints();double maxWidth = 0.0;double maxHeight = 0.0;//layout childrenvisitDXChildren((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 sizeif (!sizedByParent) {size = defaultComputeSize(Size(maxWidth, maxHeight));}//compute children offsetsvisitDXChildren((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部分

  • layout所有的children,如果FrameLayoutRender不是sizedByParent,需要同時計算所有children的最大寬度與最大高度,用于計算自身size。
  • 計算自身size,其中計算方案defaultComputeSize詳見上一小節(jié)
  • 將gravity轉(zhuǎn)化為alignment,計算所有children的offsets。
  • ? 看了FrameLayout的布局過程,是否覺得非常簡單呢?不過需要指出的是,上述FrameLayoutRender的代碼會遇到一些Bad Case,其中比較經(jīng)典的問題就是FrameLayout的寬/高為match_content,而其children的寬/高均為match_parent。這種情況在Android下會對同一個child進行"兩次measure",那么在Flutter下,我們該如何實現(xiàn)呢?

    Flutter如何解決"兩次Measure"的問題

    我們先來看一個例子:

    ? 上圖的LinearLayout是一個豎向線性布局,width被設為了match_content,它包含了兩個TextView,width均為match_parent,那么這個例子中,整個布局的流程應該是怎樣的呢。

    ? 首先需要依次measure兩個TextView的width,MeasureSpecMode為AT_MOST,簡單來說,就是問它們具體需要多寬。接著LinearLayout會將兩個TextView需要的寬度的最大值設為自己的寬度。最后,對兩個TextView進行第二次measure,此時MeasureSpecMode會被改為Exactly,MeasureSpecSize為LinearLayout的寬度。

    ? 而常見的Flutter的layout過程為以下兩種:

  • 先在performResize中計算自身size,再通過child.layout確定children sizes
  • 先通過child.layout確定children sizes,再根據(jù)children sizes計算自身size
  • ? 以上方案均不能滿足例子中我們想要的效果,我們需要找到一個方案,在調(diào)用child.layout之前,便能知道child的寬高。最后我們發(fā)現(xiàn),getMinIntrinsicWidth、getMaxIntrinsicWidth、getMinIntrinsicHeight、getMaxIntrinsicHeight四個方法能夠滿足我們。我們以getMaxIntrinsicHeight為例,來講講這些方法的用途。

    double getMaxIntrinsicWidth(double height) {return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth); }

    ? getMaxIntrinsicWidth接收一個參數(shù)height,用于確定當height為這個值時maxIntrinsicWidth應該是多少。這個方法最終會通過computeMaxIntrinsicWidth方法來計算maxIntrinsicWidth,計算結(jié)果會被保存。如果我們需要重寫,不應該重寫getMaxIntrinsicWidth方法,而是應該重寫computeMaxIntrinsicWidth方法。需要注意的是這些方法并非輕量級方法,只有在真正需要的時候才可使用。

    ? 或許你不禁要問,這些方法計算出來的寬高準嗎?實際上每個RenderBox的子類都需要保證這些方法的正確性,比如用于展示文字的RenderParagraph就實現(xiàn)了這些compute方法,因此我們得以在RenderParagraph沒被layout之前,獲取其寬度。

    ? 我們設計的Render層中的類也得實現(xiàn)compute方法,這些方法實現(xiàn)起來并不復雜,我們還是以DXSingleChildLayoutRender為例子來說明該如何實現(xiàn)這些方法。

    @overridedouble 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。

    成果與展望

    效果展示

    ? 新版渲染架構(gòu)使得Flutter能理解并對齊DSL的布局理念,系統(tǒng)性解決了之前遇到的Bad Case,為Flutter動態(tài)模板方案帶來了更多的可能性。

    性能對比

    ? 我們對新老版本的渲染性能做了測試對比,在新版渲染架構(gòu)下,我們通過頁面渲染耗時對比以及FPS對比可以發(fā)現(xiàn),動態(tài)模板的渲染性能得到了進一步的提升。

    展望

    ? 在渲染架構(gòu)升級之后,我們徹底解決了之前遇到的Bad Case,并為系統(tǒng)性分析解決這類問題提供了有力的抓手,還進一步提升了渲染性能,這讓Flutter動態(tài)模板渲染成為了可能。未來我們將繼續(xù)完善這套解決方案,做到技術(shù)賦能業(yè)務。


    雙11福利來了!先來康康#怎么買云服務器最便宜# [并不簡單]參團購買指定配置云服務器僅86元/年,開團拉新享三重禮:1111紅包+瓜分百萬現(xiàn)金+31%返現(xiàn),爆款必買清單,還有iPhone 11 Pro、衛(wèi)衣、T恤等你來抽,馬上來試試手氣👉 ?https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110

    原文鏈接
    本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。

    總結(jié)

    以上是生活随笔為你收集整理的做一个高一致性、高性能的Flutter动态渲染,真的很难么?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。