RenderObject
RenderObject is a class that determines the location and size of nodes, and handles the relationship between children and parents. When the construction process is complete, a RenderObject tree is generated, and then the layout and drawing phase is entered.
RenderObject contains properties such as parent, parentData, constraints,layout, paint, and other abstract methods. However, there is no specific size and location information such as size and offset in RenderObject. RenderBox is a class that inherits its size from RenderObject. ParentData stores information such as location, which is calculated in real time and passed to Paint by the parent node at draw time. The implementation details will be explained later.
Receive notifications
When does Flutter start the rendering process? Rendering can only be asynchronous in order to make rendering smoother, because you don’t know how many times a developer will render simultaneously in a complex business, rendering itself is expensive, and the interface is bound to stagnate if you go through rendering every time.
If the Future of Flutter is simply rendered asynchronously, there will also be performance issues as there will be multiple asynchronies in the business.
So both Android and iOS have a mechanism called Vsync. Vsync, short for VerticalSynchronization, allows AppUI and SurfaceFlinger to operate at a hardware generated Vsync pace to keep interface refresh and render within 60FPS and visually smooth for humans.
On an article about the construction of the three trees after completion will send a notification, then wait for the arrival of the Vsync signal, in a Flutter, one of SchedulerBinding addPersistentFrameCallback method to register callback listener
/// Register callback listening
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
Copy the code
When Vsync is sent to the platform, the window.onDrawFrame method is called and handleDrawFrame is triggered
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
try {
/ / processing addPersistentFrameCallback callback
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in_persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ; _schedulerPhase = SchedulerPhase.postFrameCallbacks;/ / processing addPersistentFrameCallback callback end
// Handle the callback added by addPostFrameCallback, which will be called the next time the listener is added and only once
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback inlocalPostFrameCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ;// Handle the end callback added by addPostFrameCallback
} finally {
_schedulerPhase = SchedulerPhase.idle;
/ /...
_currentFrameTimeStamp = null; }}Copy the code
The above methods can iterate over the callback queue and execution, which also performs by SchedulerBinding. Instance. Add the callback addPostFrameCallback.
While the normal process is to send the window.scheduleFrame event after the tree is built and receive the next debug signal via window.onDrawFrame, the first rendering will make the interface appear faster. In the runApp method, scheduleWarmUpFrame is called first after the build is completed (handleDrawFrame is also called later).
[-> packages/flutter/lib/src/widgets/binding.dart:WidgetsBinding]
void drawFrame() {
// ...
try {
if(renderViewElement ! =null) buildOwner! .buildScope(renderViewElement!) ;super.drawFrame(); buildOwner! .finalizeTree(); }finally {
/ /...
}
/ /...
}
Copy the code
In application startup, _handlePersistentFrameCallback method will be registered to _persistentCallbacks handleDrawFrame above will trigger the drawFrame method above, it did three things
- call
BuildOwner.buildScope
To rebuild the dirty nodes - call
super.drawFrame
Method to begin the rendering process - call
buildOwner.finalizeTree
Do some global checking in development mode (Key reuse)
Take a look at the rendering flowchart
Its corresponding source code is also very concise
[-> packages/flutter/lib/src/rendering/binding.dart:RenderBinding]
@protected
void drawFrame() {
assert(renderView ! =null);
pipelineOwner.flushLayout(); / / layout
pipelineOwner.flushCompositingBits(); // Update all nodes to calculate the region data to be drawn
pipelineOwner.flushPaint(); / / to draw
if (sendFramesToEngine) {
renderView.compositeFrame(); // Commit the drawing data to the GPU thread
pipelineOwner.flushSemantics(); // Update semantics to provide UI semantics for some visually impaired people
_firstFrameSent = true; }}Copy the code
PipelineOwner is a rendering pipeline that controls the layout of a node by holding the RenderObject of the root node and all its children.
layout
Layout is almost an indispensable process of all modern front-end technologies. It determines the specific position of each node through a series of complex calculations, and deals with the parent-child layout relationship and the calculation of display position.
Let’s start with a rough flow chart of the layout
SetSize sets the size of the box
flushLayout
When PipelineOwner calls flushLayout, the layout process is started in flushLayout
[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]
void flushLayout() {
// ...
try {
while (_nodesNeedingLayout.isNotEmpty) {
// Fetch all 'RenderObject' nodes that need to be rearranged
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this)
// Call the node's _layoutWithoutResize to start the layoutnode._layoutWithoutResize(); }}}finally {
// ...}}Copy the code
_nodesNeedingLayout is a list that needs to be rebuilt. It stores all nodes that need to be rearranged. During the node building process, markNeedsLayout is used to add itself to the list of nodes to be rearranged. In particular, when the RendererBinding is initialized, the root RenderView is also added to the list ahead of time.
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void _layoutWithoutResize() {
RenderObject? debugPreviousActiveLayout;
// ...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
// ...
}
_needsLayout = false;
markNeedsPaint();
}
Copy the code
FlushLayout stores all the nodes that need to be laid out and then calls _layoutWithoutResize for each node. PerformLayout is called in _layoutWithoutResize for the layout.
performLayout
PerformLayout is an abstract method in RenderObject that needs to be implemented by subclasses. RenderProxyBox is the most commonly used subclass of Flutter. Its mixin, RenderProxyBoxMixin, implements performLayout in this way
[-> 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); }}Copy the code
The logic is simple. If the current node has a child node, the layout of the child node is called and size is set to the size of the child node. If no child node exists, the size is assigned by calling computeSizeForNoChild.
Layout, performResize
Layout exists in the RenderObject class and does not participate in the actual layout, so you should generally not override this method either. It is used by the parent node to layout the child node by calling child.layout.
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void layout(Constraints constraints, { bool parentUsesSize = false{})// ...
RenderObject? relayoutBoundary;
// Determine the current relayoutBoundary. Normally relayoutBoundary is its own or ancestor node
if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! 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();
}
Copy the code
In order to consider the performance of layout, _relayoutBoundary will be used during layout to optimize the performance. It sets _relayoutBoundary to itself by satisfying one of the following four conditions:
ParentUsesSize to false
, indicates that the layout of the child node does not affect the parent node, and the parent node does not adjust itself according to the size of the child nodeSizedByParent to true
, indicates that if the constraints from the parent to the child remain unchanged, then the child does not recalculate the box size, nor does the layout change of the child’s child affect the size of the child, such as if the child is always full of the parent.Constraints. IsTight to true
, indicating that the box size is uniquely determined after the constraints are determined. For example, if the maximum height and minimum height of the box are the same, and the maximum width and minimum width are the same, then the box size is determinedThe parent is not RenderObject
When the parent isAbstractNode
Type, so it still existsparent
notRenderObject
For exampleSemanticsNode
(Semantic auxiliary node)
Otherwise the _relayoutBoundary points to the _relayoutBoundary of the parent node. When the current class calls markNeedsLayout, it iterates from the current to the parent until it finds a _relayoutBoundary and marks all iterated nodes as _needsLayout=true. If the _relayoutBoundary node of the current class is as close to you as possible, it should be you.
PerformResize is called when sizedByParent is true, and that size is defined in performResize. It won’t be changed in later performLayout methods. In this case, performLayout is only responsible for layout child nodes.
RenderBox
As mentioned earlier, RenderObject only controls the drawing process. It does not specify the size of the box, which is defined by the RenderBox class
RenderBox has some important properties and methods
Size size
: Defines the box sizeBoxConstraints constraints
: box constraint, which holds the maximum and minimum height and width limits of the box, passed by the parent boxSize computeDryLayout()
: this method is defined in Flutter2.0 for whenSizedByParent to true
Is used to calculate the size of the box. You can’t assign size inside the box. You just need to return its calculated size. Unable to calculate box size, returnSize.zero
. If we customizeRenderObject
In the classsizedByParent=true
, just need to inherit to implement this method to calculate the layout size.bool hitTest(BoxHitTestResult result,{required Offset position})
: A hit test for an event that traverses the current and child nodes and adds the current node to the list of hit test results if an event occurs in the current node.
Determine the location and parentData
Before, the layout stage will determine the box size and position, so how to save the position, the answer is in this parentData.
When the box is initialized, the parent node calls setupParentData to initialize the parentData of the child node
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
Copy the code
ParentData is just a simple empty method that needs to be inherited to define its own information, such as the most common BoxParentData, which is used to store the location of the child node when we only have one child node
[-> packages/flutter/lib/src/rendering/box.dart:BoxParentData]
class BoxParentData extends ParentData {
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
Copy the code
I’ll use an example of the Center component we use a lot to see how location is determined.
The Center component inherits the Align component, which creates the RenderObject through the RenderPositionedBox class
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),
);
}
// ...
}
Copy the code
Align’s constructor gives the alignment property alalignment. Center by default. How does RenderPositionedBox determine the size and position
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderPositionedBox]
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
// _widthFactor indicates that the container size is a multiple of the subbox, True if _widthFactor is not empty or if the maximum length of the box is maximum (when constraints. MaxWidth == double. Infinity and _widthFactor is empty, the box width is the actual width of the subbox)
final boolshrinkWrapWidth = _widthFactor ! =null || constraints.maxWidth == double.infinity;
final boolshrinkWrapHeight = _heightFactor ! =null || constraints.maxHeight == double.infinity;
if(child ! =null) {
// Layout the child nodeschild! .layout(constraints.loosen(), parentUsesSize:true);
// Set the box sizesize = constraints.constrain(Size(shrinkWrapWidth ? child! .size.width * (_widthFactor ??1.0) : double.infinity, shrinkWrapHeight ? child! .size.height * (_heightFactor ??1.0) : double.infinity));
// Set the location of the child node
alignChild();
} else {
size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity)); }}Copy the code
We see that after its performLayout phase ends, it calls alignChild to set the position of the child box
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderAligningShiftedBox]
@protected
void alignChild() {
_resolve();
// ...
finalBoxParentData childParentData = child! .parentData!asBoxParentData; childParentData.offset = _resolvedAlignment! .alongOffset(size - child! .sizeas Offset);
}
Copy the code
Size = the size of the current box minus the size of the child box. In this case, we just need to divide the length and width of the size by 2 to know the offset of the child box relative to the parent box. The Offset is then assigned to the child’s parentData. The child node’s parentData now stores its position relative to the parent node.
conclusion
HandleDrawFrame is used to start the drawing process of Flutter. PipelineOwner is used in the drawFrame method to carry out the layout, composition, drawing and submission processes. PerformLayout is a method called by each node. Below it we usually set the size of the box and call the layout method of the child node, which also calls performLayout. The layout is quite complex, and the child node will affect the parent node, and the size of the parent node will affect the child node, so sizedByParent, parentUsesSize and other attributes are introduced, and _relayoutBoundary is also introduced to optimize the performance of the layout. ParentData is used to obtain the location information of storage nodes for drawing.
At this stage, our layout process is complete and we are ready to draw. I will keep you updated on what was done and what optimizations were made during the drawing process.