RenderObject
之前講過三棵樹的繪製,它們最後都是為RenderObject樹服務的,RenderObject是確定節點位置、大小,處理子父位置關係的類,當構建過程完成之後,就生成了一棵RenderObject
樹,然後進入佈局及繪製階段。
RenderObject
中包含parent
、parentData
、constraints
等屬性,layout
及paint
等抽象方法,不過其中沒有定義size、offset等具體大小和位置資訊,為了更好的擴充套件性,其大小是通過RenderBox
這個繼承至RenderObject
的類來實現的,parentData
會儲存位置等資訊,在繪製時父節點再實時計算傳給paint
。具體實現細節,我們後面來慢慢講解。
接收通知
Flutter在什麼時候開始渲染流程呢?為了讓渲染更加流暢,渲染只能被設計成非同步,因為你不知道開發者會在複雜的業務中同時發起多少次渲染,渲染本身的耗時是昂貴的,如果每次都會經歷渲染,那介面一定會卡頓。
如果只是簡單的通過Flutter的Future來進行非同步渲染,同上也會有效能問題,因為業務中也會存在多個非同步。
所以Android
和iOS
都有一個叫Vsync
的機制。Vsync(垂直同步)
是VerticalSynchronization的簡寫,讓AppUI和SurfaceFlinger可以按硬體產生的VSync節奏進行工作,以此來達到介面的重新整理和渲染保持在60FPS以內,讓人類視覺上感覺到不卡頓。
上一篇文章講了三棵樹的構建完成後會傳送一個通知,然後會等待Vsync
訊號的到來,在Flutter中,SchedulerBinding
中有一個addPersistentFrameCallback
方法來註冊回撥監聽
/// 註冊回撥監聽
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
複製程式碼
當Vsync傳送到平臺端時,會呼叫window.onDrawFrame
方法,後面會觸發到handleDrawFrame
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
try {
// 處理addPersistentFrameCallback回撥
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
// 處理addPersistentFrameCallback回撥 end
// 處理addPostFrameCallback新增的回撥,此回撥會在新增監聽的下一時刻被呼叫且只會被呼叫一次
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
// 處理addPostFrameCallback新增的回撥 end
} finally {
_schedulerPhase = SchedulerPhase.idle;
//...
_currentFrameTimeStamp = null;
}
}
複製程式碼
上面的方法會迴圈遍歷回撥佇列並執行,其中還會執行通過SchedulerBinding.instance.addPostFrameCallback
新增的回撥。
雖然正常的流程是在樹構建完成後傳送window.scheduleFrame
事件,然後通過window.onDrawFrame
來接收下一偵開始訊號,不過在首次渲染時,為了讓介面更快的顯示,runApp方法中也會在構建完成後第一時間呼叫scheduleWarmUpFrame
先進行渲染(後面也會呼叫handleDrawFrame
)。
[-> packages/flutter/lib/src/widgets/binding.dart:WidgetsBinding]
void drawFrame() {
// ...
try {
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!);
super.drawFrame();
buildOwner!.finalizeTree();
} finally {
//...
}
//...
}
複製程式碼
在應用啟動的時候,_handlePersistentFrameCallback
方法就會被註冊到_persistentCallbacks
中,上面的handleDrawFrame
就會觸發上面這個drawFrame
方法,它做了三件事情
- 呼叫
BuildOwner.buildScope
,重新構建髒節點 - 呼叫
super.drawFrame
方法,開始渲染流程 - 呼叫
buildOwner.finalizeTree
,開發模式中做一些全域性檢查(Key重複使用)
先看看渲染流程圖
其對應的原始碼也非常簡潔
[-> packages/flutter/lib/src/rendering/binding.dart:RenderBinding]
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 佈局
pipelineOwner.flushCompositingBits(); // 更新所有節點,計算待繪製區域資料
pipelineOwner.flushPaint(); // 繪製
if (sendFramesToEngine) {
renderView.compositeFrame(); // 將繪製資料提交到GPU執行緒
pipelineOwner.flushSemantics(); // 更新語義化,給一些視力障礙人士提供UI的的語義
_firstFrameSent = true;
}
}
複製程式碼
PipelineOwner
就是渲染流水線俗稱渲染管線
,它通過持有根節點的RenderObject
及所有子節點會持有它來對節點的佈局繪製的控制。
layout
layout
幾乎是所有現代前端技術必不可少的流程,它是通過一系列複雜的計算來確定每個節點具體佔據多少的位置,處理父子佈局關係及顯示位置的計算。
我們先看看佈局的大概流程圖
setSize表示設定盒子的大小
flushLayout
當PipelineOwner
呼叫了flushLayout
之後,會開始佈局流程,在flushLayout
中
[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]
void flushLayout() {
// ...
try {
while (_nodesNeedingLayout.isNotEmpty) {
// 取出需要重新佈局的所有`RenderObject`節點
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
// 呼叫節點的_layoutWithoutResize開始佈局
node._layoutWithoutResize();
}
}
} finally {
// ...
}
}
複製程式碼
_nodesNeedingLayout
是一個需要重新構建的列表,它會儲存所有需要重新佈局的節點,一般在節點構建過程中會通過markNeedsLayout
將自己新增到待重新layout列表中。有一點比較特殊,在RendererBinding
初始化時,也會提前將根節點RenderView
新增進列表中。
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void _layoutWithoutResize() {
RenderObject? debugPreviousActiveLayout;
// ...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
// ...
}
_needsLayout = false;
markNeedsPaint();
}
複製程式碼
flushLayout
中會儲存所有需要佈局的節點,然後呼叫每個節點的_layoutWithoutResize
,_layoutWithoutResize
中會呼叫performLayout
進行佈局。
performLayout
performLayout
在RenderObject
是一個抽象方法,需要由子類實現。我們來看看在Flutter使用最多的RenderObject
子類RenderProxyBox
,它的mixin——RenderProxyBoxMixin
中是這樣實現performLayout
的
[-> packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxMixin]
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
複製程式碼
其邏輯很簡單,如果當前節點存在孩子節點,則呼叫孩子節點的layout
,然後將size設定為子節點的size,如果不存在子節點,則呼叫computeSizeForNoChild
對size進行賦值。
layout、performResize
layout
存在於RenderObject
類中,它不參與真正的佈局,所以一般也不要重寫此方法。它的作用是父節點通過呼叫child.layout
來對子節點的佈局。
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// ...
RenderObject? relayoutBoundary;
// 確定當前的relayoutBoundary,一般relayoutBoundary是自己或者祖先節點
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
// ...
return;
}
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
// ...
if (sizedByParent) {
assert(() {
_debugDoingThisResize = true;
return true;
}());
try {
performResize();
// ...
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
}
// ...
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
// ...
_needsLayout = false;
markNeedsPaint();
}
複製程式碼
為了layout
的效能考慮,layout
過程中會使用_relayoutBoundary
來優化效能。它根據下面的四個條件滿足其一即可讓_relayoutBoundary
等於自己:
parentUsesSize 為false
,表示子節點的佈局不會影響父節點,父節點不會根據子節點的大小來調整自身sizedByParent 為true
,表示如果父節點傳給子節點的約束(constraints)不變,那麼子節點不會重新計算盒子大小,子節點的孩子節點的佈局變化也不會影響子節點的大小,如該節點始終充滿父節點。constraints.isTight 為true
,表示約束(constraints)確定後,盒子大小就唯一確定。比如當盒子的最大高度和最小高度一樣,同時最大寬度和最小寬度一樣時,那麼盒子大小就確定了parent不是RenderObject
時,parent是AbstractNode
型別,所以還存在parent
不是RenderObject
的情況,比如SemanticsNode
(語義輔助節點)
否則_relayoutBoundary
會指向父節點的_relayoutBoundary
。在當前類呼叫markNeedsLayout
的時候,它會從當前向父節點遍歷,直到找到節點為_relayoutBoundary
時停止,並會標記所有遍歷的節點為_needsLayout=true
,如果當前類的_relayoutBoundary
節點離自己越近越好,最好就是自己。
當sizedByParent
為true時,才會呼叫performResize
,此時其大小在performResize
中就確定了,在後面的performLayout
方法中將不會再被修改了,這種情況下performLayout
只負責佈局子節點。
RenderBox
前面說過,RenderObject
僅僅控制繪製流程,並沒有具體定義盒子的size,size都是通過RenderBox
類來定義的
RenderBox
有一些比較重要的屬性及方法
Size size
: 定義盒子大小BoxConstraints constraints
: 盒子約束,其中儲存了盒子的最大最小高度寬度限制,由父盒子傳遞Size computeDryLayout()
: 此方法在Flutter2.0中被定義,用於當sizedByParent 為true
時用來計算盒子的大小,不能在它內部進行size賦值,只需要返回其計算的大小即可。無法計算盒子大小時,返回Size.zero
。如果我們自定義的RenderObject
類中sizedByParent=true
,只需要繼承實現此方法來計算佈局大小.bool hitTest(BoxHitTestResult result,{required Offset position})
: 用於事件的命中測試,它會遍歷當前及子節點,如果事件在當前節點中發生,它會將當前節點新增到命中測試結果列表中。
確定位置及parentData
之前說layout階段會確定盒子大小和位置,那麼位置是如何儲存的呢,答案就在這個parentData
裡。
在盒子初始化時,父節點會呼叫setupParentData
來初始化子節點的parentData
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
複製程式碼
ParentData
只是一個簡單的空方法,一般需要繼承它來定義自己的資訊,比如我們最常用的BoxParentData
,它的作用就是當我們子節點只有一個的時候,儲存子節點的位置資訊
[-> packages/flutter/lib/src/rendering/box.dart:BoxParentData]
class BoxParentData extends ParentData {
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
複製程式碼
我用一個我們經常使用的Center
元件的例子來看看是如何確定位置的。
Center
元件是繼承的Align
元件,Align
元件是通過RenderPositionedBox
類來建立RenderObject
class Center extends Align {
const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
//...
class Align extends SingleChildRenderObjectWidget {
const Align({
Key? key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget? child,
});
// ...
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
// ...
}
複製程式碼
Align的建構函式中alignment
屬性預設為Alignment.center
,我們看看RenderPositionedBox
是如何確定大小及位置的
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderPositionedBox]
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
// _widthFactor表示容器大小是子盒子的倍數,如果_widthFactor不為空或者 盒子的最大長度為最長時為true(當constraints.maxWidth == double.infinity且_widthFactor為空時,盒子寬度為子盒子實際寬度)
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
if (child != null) {
// 佈局子節點
child!.layout(constraints.loosen(), parentUsesSize: true);
// 設定盒子大小
size = constraints.constrain(Size(shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity));
// 設定子節點位置
alignChild();
} else {
size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity));
}
}
複製程式碼
我們看到在它performLayout
階段結束後,會呼叫alignChild
來設定子盒子的位置
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderAligningShiftedBox]
@protected
void alignChild() {
_resolve();
// ...
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
複製程式碼
在這裡,它先拿到子節點的parentData,然後對其的offset賦值,size-child.size
表示當前盒子大小減去子盒子的大小,此時只需要將Size的長寬除以2,就可以知道子盒子相對於父盒子的Offset
,然後將這個Offset
賦值給子節點的parentData
。現在,子節點的parentData
中儲存了自己相對於父節點的相對位置資訊了。
總結
這一篇講了Flutter繪製流程的啟動及佈局過程,handleDrawFrame
會啟動繪製流程,drawFrame
方法中使用PipelineOwner
來進行佈局、合成、繪製、提交等流程,performLayout
是每個節點都會呼叫的方法,我們一般在它下面進行設定盒子大小(size
)及呼叫子節點的layout
方法,layout
方法也會呼叫performLayout
。佈局是比較複雜的,子節點會影響父節點,父節點的大小又會影響子節點,所以其中引入了sizedByParent
、parentUsesSize
等屬性,還引入了_relayoutBoundary
來優化佈局的效能。然後通過parentData
進行儲存節點位置資訊,給繪製時使用。
這一步,我們的layout
過程已經完成,接下來就可以繪製了。繪製過程又做了哪些事情,又有哪些優化呢,後面我會持續更新。