摘要:所以這里為時把指向自身,因為自身的肯定符合約束的條件,也是提高布局效率的一個關鍵點。舉一個栗子,在中先讓布局之后,根據的,來設置自身的。意味著父控件要依賴子控件的,可能父控件的布局要根據子控件的來做調整。
布局約束
剛才所說的改變一個控件的高度,有時候并不像剛才所說只是改變一下屬性就能起作用,這里涉及到一個布局約束規則。
直接看BoxConstraints的實現,這個類主要定義了minWidth和maxWidth,minHeight和maxHeight這些約束條件,child布局的時候可以根據parent給予的這些條件進行對應的布局。
簡單介紹相關的一些術語:
tightly,如果最小約束(minWidth)和最大約束(maxWidth)都是一樣的
loose,如果最小約束是0.0(不管最大約束);如果最小約束和最大約束都是0.0,就同時是tightly和loose
bounded,如果最大約束不是infinite
unbounded,如果最大約束是infinite
expanding,如果最小約束和最大約束都是infinite
如果一個size滿足BoxConstraints的約束,那么它就是constrained的。
既然是parent傳遞給child的約束條件,當然是在performLayout的時候調起child.layout方法:
void layout(Constraints constraints, { bool parentUsesSize: false }) { RenderObject relayoutBoundary; if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { final RenderObject parent = this.parent; relayoutBoundary = parent._relayoutBoundary; } if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { return; } _constraints = constraints; _relayoutBoundary = relayoutBoundary; if (sizedByParent) { try { performResize(); } catch (e, stack) { _debugReportException("performResize", e, stack); } } RenderObject debugPreviousActiveLayout; try { performLayout(); markNeedsSemanticsUpdate(); assert(() { debugAssertDoesMeetConstraints(); return true; }()); } catch (e, stack) { _debugReportException("performLayout", e, stack); } _needsLayout = false; markNeedsPaint(); }
開始分析這段代碼前一小半,決定relayoutBoundary的值也就是布局邊界,一個RenderObject想要重新布局,應該從哪里開始。
parentUsesSize,如果為false也就是,parent的布局并不需要依賴child的布局結果,那么child如果要重新布局并不需要通知parent,布局的邊界就是自身了,而parentUsesSize的默認值也是為false,也就是大部分時候也只需自身重新布局。
parentUsesSize,如果為true就是parent的布局要依賴child布局(或者parent的size依賴于child的size,想象一下兩個div嵌套的情況),再看如果sizedByParent和constraints.isTight都為false,在這種情況之下relayoutBoundary要指向parent.relayoutBoundary,也就是說child如果要重新布局,必須從relayoutBoundary開始,在RenderObject.markNeedsLayout方法實現里面,最終只會把relayoutBoundary加入到_nodesNeedingLayout列表中,跟isRepaintBoundary處理是幾乎一樣的;
但是如果constraints.isTight為true,也就是minWidth和maxWidth(或者minHeight和maxHeight)值都一樣child的size沒有變化的空間,只能在限定死的約束空間中布局,這個時候relayoutBoundary也是指向自身。
最后就是sizedByParent這個屬性,說實在看名字不太明白它的意圖,但是sizedByParent卻決定performResize這個方法會不會調起,但是看了RenderBox.size的setter方法:
set size(Size value) { assert(!(debugDoingThisResize && debugDoingThisLayout)); assert(sizedByParent || !debugDoingThisResize); assert(() { if ((sizedByParent && debugDoingThisResize) || (!sizedByParent && debugDoingThisLayout)) return true; assert(!debugDoingThisResize); String contract, violation, hint; if (debugDoingThisLayout) { assert(sizedByParent); violation = "It appears that the size setter was called from performLayout()."; hint = ""; } else { violation = "The size setter was called from outside layout (neither performResize() nor performLayout() were being run for this object)."; if (owner != null && owner.debugDoingLayout) hint = "Only the object itself can set its size. It is a contract violation for other objects to set it."; } if (sizedByParent) contract = "Because this RenderBox has sizedByParent set to true, it must set its size in performResize()."; else contract = "Because this RenderBox has sizedByParent set to false, it must set its size in performLayout()."; throw new FlutterError( "RenderBox size setter called incorrectly. " "$violation " "$hint " "$contract " "The RenderBox in question is: " " $this" ); }()); assert(() { value = debugAdoptSize(value); return true; }()); _size = value; assert(() { debugAssertDoesMeetConstraints(); return true; }()); }
大致可以總結一下,一般情況下都是都是根據parent給予的約束條件來計算size,而設置size只能在performResize或者performLayout中進行,如果設置sizedByParent為true,則只能在performResize中進行,否則就只能在performLayout中與child的布局同時進行。所以sizedByParent為true也意味著這個RenderObject的size不需要依賴于child的size,完全可以根據parent給予的約束條件可以確定(取最大或者最小的寬度和高度或者根據其他算法);但是為false,自身的size就要在performLayout決定,可能要在child的size和約束條件中計算出來,應該是更為復雜,根據注釋sizedByParent默認為false永遠都不會有問題的。所以這里sizedByParent為true時把relayoutBoundary指向自身,因為自身的size肯定符合約束的條件,也是提高布局效率的一個關鍵點。
舉一個栗子,在RenderPadding中:
void performLayout() { _resolve(); assert(_resolvedPadding != null); if (child == null) { size = constraints.constrain(new Size( _resolvedPadding.left + _resolvedPadding.right, _resolvedPadding.top + _resolvedPadding.bottom )); return; } final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding); child.layout(innerConstraints, parentUsesSize: true); final BoxParentData childParentData = child.parentData; childParentData.offset = new Offset(_resolvedPadding.left, _resolvedPadding.top); size = constraints.constrain(new Size( _resolvedPadding.left + child.size.width + _resolvedPadding.right, _resolvedPadding.top + child.size.height + _resolvedPadding.bottom )); }
RenderPadding先讓child布局之后,根據child的size,來設置自身的size。這里還涉及到一個Offset的問題,因為layout只是獲取了size,但是元素在哪里開始繪制,一般也是由parent控制,當parent設置好每個child的offset之后在繪制的過程中就可以在適當的位置中繪制了。
parentUsesSize & sizedByParent個人覺得這兩個名稱是最令人迷惑,所以這里再總結一下:
sizedByParent 顧名思義控件的大小完全在父控件的約束條件下,例如約束條件maxWidth=100,minWidth=0就意味著子控件的寬度只能在0到100的范圍內。
parentUsesSize 意味著父控件要依賴子控件的size,可能父控件的布局要根據子控件的size來做調整。
還有這兩者是否是沖突的尼,能否都為true?
根據我的分析這兩個應該是不會造成沖突的,在選定布局的邊界情況下,剛才代碼中:
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { final RenderObject parent = this.parent; relayoutBoundary = parent._relayoutBoundary; }
如果parentUsesSize為true,布局邊界毫無疑問指向了parent,也就是子控件要重新布局必須先從父控件開始,因為父控件需要用到子控件重新布局后的結果,所以在選定布局邊界的問題上parentUsesSize起到決定性的作用。
如果sizedByParent和parentUsesSize都為true,例如父控件把maxWidth=100,minWidth=0這樣的約束條件傳遞給子控件,意味著子控件布局的范圍只能在0到100之間,假設子控件最終的size為50,父控件可能直接會使用子控件的size作為自身的size,這樣一看好像現在父控件的布局范圍現在應該是0到50,但其實是并不會影響一開始傳遞給子控件的約束條件0到100之間,所以不會觸發一個循環布局。
繼續PipelineOwner處理流程,在flushLayout之后就是flushCompositingBits方法,而flushCompositingBits目標是為每個RenderObject設置適當needCompositing值,這影響flutter最終會生成多少層Layer,而這些Layer會組成一棵Layer Tree并交由引擎最終composite成一幀畫面。
總結之前的,現在我們可以得出以下一張的關系圖:
再對比一下之前的Chromium文檔里面的一張圖:
這種關系不言而喻,Flutter確實就是一個super webview。
繼續flushCompositingBits方法的深入:
void flushCompositingBits() { Timeline.startSync("Compositing bits"); _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (RenderObject node in _nodesNeedingCompositingBitsUpdate) { if (node._needsCompositingBitsUpdate && node.owner == this) node._updateCompositingBits(); } _nodesNeedingCompositingBitsUpdate.clear(); Timeline.finishSync(); }
跟之前flushLayout差不多,那么啥時候RenderObject會加入到PipelineOwner._needsCompositingBitsUpdate列表上尼?
掃了一下代碼,發現除了框架初始化以外,一般都是在添加child和刪除child的時候,調起RenderObject.markNeedsCompositingBitsUpdate方法。
繼續_updateCompositingBits方法:
void _updateCompositingBits() { if (!_needsCompositingBitsUpdate) return; final bool oldNeedsCompositing = _needsCompositing; _needsCompositing = false; visitChildren((RenderObject child) { child._updateCompositingBits(); if (child.needsCompositing) _needsCompositing = true; }); if (isRepaintBoundary || alwaysNeedsCompositing) _needsCompositing = true; if (oldNeedsCompositing != _needsCompositing) markNeedsPaint(); _needsCompositingBitsUpdate = false; }
舉個栗子,最初可能是這樣的,RenderObject添加一個新的child,而這個child是被設置為alwaysNeedsCompositing
經過_updateCompositingBits處理后:
而needsComposting這個屬性會用在那里尼?同樣掃一遍代碼,都可以發現類似的處理:
if (needsCompositing) { pushLayer(new ClipRectLayer(clipRect: offsetClipRect), painter, offset, childPaintBounds: offsetClipRect); } else { canvas ..save() ..clipRect(offsetClipRect); painter(this, offset); canvas ..restore(); }
如果needsCompositing為true,都會創建一個新的Layer,所以needsCompositing更多像一個暗示的作用,在clip,transform或者設置opacity都會創建一個Layer來處理,這樣可以把一些經常變化的區域隔離開來,每次只需要繪制這部分區域來提高效率。
接著flushPaint方法:
void flushPaint() { Timeline.startSync("Paint", arguments: timelineWhitelistArguments); try { final ListdirtyNodes = _nodesNeedingPaint; _nodesNeedingPaint = []; // Sort the dirty nodes in reverse order (deepest first). for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) { if (node._needsPaint && node.owner == this) { if (node._layer.attached) { PaintingContext.repaintCompositedChild(node); } else { node._skippedPaintingOnLayer(); } } } } finally { Timeline.finishSync(); } }
在repaintCompositedChild方法里面,會為RenderObject創建一個屬于自己的Layer,其實也只限于isRepaintBoundary為true的RenderObject,因為只有這樣的RenderObject才可以加入到_nodesNeedingPaint列表中:
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) { if (child._layer == null) { child._layer = new OffsetLayer(); } else { child._layer.removeAllChildren(); } final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds); child._paintWithContext(childContext, Offset.zero); childContext._stopRecordingIfNeeded(); }
接著就是創建PaintingContext,就像前端需要獲取Canvas2D Context一樣,_paintWithContext最終會調起RenderObject.paint方法,在paint方法里面我們就可以自由繪制了,但是一般情況下CustomPaint組件就可以滿足我們的需求。
再看一下PaintingContext類中:
Canvas get canvas { if (_canvas == null) _startRecording(); return _canvas; } void _startRecording() { _currentLayer = new PictureLayer(canvasBounds); _recorder = new ui.PictureRecorder(); _canvas = new Canvas(_recorder, canvasBounds); _containerLayer.append(_currentLayer); } void _stopRecordingIfNeeded() { if (!_isRecording) return; _currentLayer.picture = _recorder.endRecording(); _currentLayer = null; _recorder = null; _canvas = null; }
當我們獲取canvas做繪制操作的時候,每次都會創建一個新的Canvas對象,并使用使用ui.PictureRecorder記錄我們在canvas的操作,最后_stopRecordingIfNeeded會從recorder上獲取到繪制的picture,感覺這里有涉及到底層解析得不太好。。。
好吧,當flushPaint完成后,Layer Tree也構建出來了,最后就是composite階段了,回到RenderView.compositeFrame方法:
void compositeFrame() { Timeline.startSync("Compositing", arguments: timelineWhitelistArguments); try { final ui.SceneBuilder builder = new ui.SceneBuilder(); layer.addToScene(builder, Offset.zero); final ui.Scene scene = builder.build(); ui.window.render(scene); scene.dispose(); } finally { Timeline.finishSync(); } }
Flutter會把所有的Layer都加入到ui.SceneBuilder對象中,然后ui.SceneBuilder會構建出ui.Scene(場景),交給ui.window.render方法去做最后真實渲染,之后就是底層引擎的工作內容,有機會再去更加深入去學習吧。
結束大致把整個布局渲染流程梳理一遍,感覺越到后面越吃力,有點超出認知的范圍,也證明自己知識面仍然有很多不足的地方,如果有什么錯漏的地方希望大家能夠指正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89627.html
摘要:入口界面的布局和繪制在每一幀都在發生著,甚至界面沒有變化,它也會存在可以想象每一幀里面,引擎都像流水線的一樣重復著幾個過程構建控件樹,布局繪制和合成,周而復始。大概可以想到的主要功能負責管理那些,讓它們進行布局和繪制。 開始 Flutter對比前端流行的框架,除了構建控件樹和控件狀態管理等,還多了布局和繪制的流程,布局和繪制以往都是前端開發可望而不可及的都被封鎖在瀏覽器渲染引擎的實現里...
摘要:但是接下來并不是討論單線程如何方便開發,而是要深入的調度器,看一下是如何安排任務,調度工作??偨Y在大部分情況下,其實并不用擔心會像游戲一樣瘋狂消耗電量,消耗電量表現應該跟原生沒有多大差別。 開始 在原生開發中(例如Android)都會強調不能阻塞主線程,但是開發中經常會遇到發送請求或者操作數據庫等,這些操作都會阻塞主線程,幾乎唯一辦法就是用多線程處理這些工作;而在Flutter中就像跟...
摘要:開始繼續接著分析相關的樣式和布局控件,但是這次內容難度感覺比較高,怕有分析不到位的地方,所以這次僅僅當做一個參考,大家最好可以自己閱讀一下代碼,應該會有更深的體會。關于屬性,指前一個組件的布局區域和繪制區域重疊了。 開始 繼續接著分析Flutter相關的樣式和布局控件,但是這次內容難度感覺比較高,怕有分析不到位的地方,所以這次僅僅當做一個參考,大家最好可以自己閱讀一下代碼,應該會有更深...
摘要:開始繼續接著分析相關的樣式和布局控件,但是這次內容難度感覺比較高,怕有分析不到位的地方,所以這次僅僅當做一個參考,大家最好可以自己閱讀一下代碼,應該會有更深的體會。關于屬性,指前一個組件的布局區域和繪制區域重疊了。 開始 繼續接著分析Flutter相關的樣式和布局控件,但是這次內容難度感覺比較高,怕有分析不到位的地方,所以這次僅僅當做一個參考,大家最好可以自己閱讀一下代碼,應該會有更深...
閱讀 2902·2021-11-23 09:51
閱讀 1547·2021-11-15 11:36
閱讀 3006·2021-10-13 09:40
閱讀 1864·2021-09-28 09:35
閱讀 13040·2021-09-22 15:00
閱讀 1367·2019-08-29 13:56
閱讀 2924·2019-08-29 13:04
閱讀 2699·2019-08-28 18:06