本系列将从Flutter框架runApp()运行开始,结合框架源码,分析flutter UI渲染、更新机制,布局、绘制过程,以及解析flutter主要的生命周期过程。认真读完本系列,读者一定会对Flutter运行过程了如指掌、胸有成竹。
本系列将有小量源码出没,建议读者打开编译器,配合框架源码食用,效果更佳。
开始的开始
前文提到,Flutter通过注册VSync信号监听,来更新”脏“元素,那Flutter是如何显示第一帧的呢?也需要等待VSync信号回调吗?
本文主要介绍Flutter渲染第一帧时所做的工作。
Flutter App的入口参数是runApp()
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Counter Demo",
...
theme: ThemeData(primarySwatch: Colors.blue),
);
}
}
Flutter不仅需要在runApp()中完成对框架的初始化,同时还需要将app的第一帧画面渲染显示出来。
接着上文的内容,我们在上文提到,在ensureInitialized()中,主要进行framework的初始化工作,包括BuildOwner、RenderView等重要对象的初始化,以及注册绘制及各种回调函数等等。
那紧跟后面联调的scheduleAttachRootWidget()和scheduleWarmUpFrame()负责什么内容呢?
scheduleAttachRootWidget()
先看看scheduleAttachRootWidget()
// WidgetsBinding类
@protected
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}
attachRootWidget()的实现如下:
// WidgetsBinding类
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}
首先,调用RenderObjectToWidgetAdapter()构造方法,并将renderView,以及rootWidget传入。
- renderView:在ensureInitialized()阶段完成初始化,是一个RenderObject对象,也是整个页面RenderObject树的根
- rootWidget:是我们需要构建的widget,例子中是一个StatelessWidget对象。
RenderObjectToWidgetAdapter类结构如下,是一个RenderObjectWidget对象:
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {}
创建完Widget对象后,调用attachToRenderTree(),把buildOwner,renderViewElement对象传入,其中
- buildOwner:也是初始化阶段赋值的对象,它负责整个页面的组件管理工作,内部维护了一个dirtyElements列表,更新时会遍历这个列表,进行rebuild。
- renderViewElement:它是一个RenderObjectElement对象,值取自_renderViewElement,此时值为null
// RenderObjectToWidgetAdapter类
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
一、element为null
调用createElement()
,返回一个RenderObjectElement对象,创建element时把widget作为参数,传给了element构造方法,此时element就持有了widget对象。
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
@override
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
}
接着调用element.assignOwner(owner);
,将这个element与页面的buildOwner对象绑定,此时就与BuildOwner对象建立了联系,当该element需要更新时,会被加入到BuildOwner对象中的dirtElements列表中。
随后调用element.mount()
,参数传了两个null,因为是根element,所以自然不会有parent,而slot是个什么对象呢?它是描述子element在父element树中具体的配置信息,可以理解为子element将在父element中的位置,slot也为null。
为什么需要slot?我们知道element以widget作为配置信息来创建,一个widget配置信息可能会用于创建多个element,但这多个element最终可能会被挂在同一棵树上,widget是不负责决定生成的element最终会被挂在树上的哪个地方,所以需要这个slot对象区分各个element。
class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_rebuild();
}
}
内部做了那些事?
-
调用
super.mount()
,其内部又通过super.mount(),一步步调用到了Element.mount(),这个方法一共做了那些事?// Element.mount() _parent = parent; _slot = newSlot; _lifecycleState = _ElementLifecycle.active; _depth = _parent != null ? _parent!.depth + 1 : 1; if (parent != null) { _owner = parent.owner; } final Key? key = widget.key; if (key is GlobalKey) { owner!._registerGlobalKey(key, this); } _updateInheritance(); // RenderObjectElement.mount() _renderObject = widget.createRenderObject(this); attachRenderObject(newSlot); _dirty = false;
Element.mount()
在
Element.mount()
中实际上只是做一些赋值操作,也能理解,Element属于最底层的对象,里面正常都是进行各种Element的通用操作,比如配置赋值,如果该widget设置了global key,则会在这里进行注册。GlobalKey是有关Element复用的,如果widget提供了key,那么会把该element存在BuildOwner的复用列表中,后面有需要再根据key,取出对应的element达到复用效果,这里简单提一下,咱先不理
RenderObjectElement.mount()
在
RenderObjectElement.mount()
中,调用widget.createRenderObject()
,并把Element对象传入,这个widget是我们前面创建的RenderObjectToWidgetAdapter对象,看看这个类的实现。// RenderObjectToWidgetAdapter类 @override RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
这个container其实就是我们一开始构建Widget对象时传入的renderView(RenderObject),此时用于窗口绘制的三个顶层对象Widget、Element、RenderObject已经集齐了。
接着调用attachRenderObject(),并把该Element的位置参数slot传入,将创建的RenderObject对象挂载到父element的RenderObject树中,但因为该Element本来就是Element树中的根Element,所以实际上在渲染第一帧时attachRenderObject()里并没有做什么操作。
最后设置_dirty = false,mount()操作完成。
总结一下,mount主要完成以下工作:一是创建RenderObject对象,二是如果有父Element,就将RenderObject挂载到父Element的RenderObject树中
-
super.mount()执行完毕,此时根Widget、Element、RenderObject都准备好了,调用_rebuild()操作。
// RenderObjectToWidgetElement类,继承自RootRenderObjectElement void _rebuild() { try { _child = updateChild(_child, widget.child, _rootChildSlot); } catch (exception, stack) { final FlutterErrorDetails details = FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: ErrorDescription('attaching to the render tree'), ); FlutterError.reportError(details); final Widget error = ErrorWidget.builder(details); _child = updateChild(null, error, _rootChildSlot); } }
实际上也只是执行了updateChild(),这个方法内部负责页面内容的构建。
传入的参数有:
-
_child,是个Element对象,表这个根Element的子Element,它其实就是与widget.child对应的element,此时为null,updateChild()工作完成后赋值。
-
widget.child,其实就是我们在runApp时传入的MyApp(),也即我们页面要显示的widget。
-
_rootChildSlot,一个静态slot对象,用于第一次往根Element插入子Element时的配置信息。
static const Object _rootChildSlot = Object();
updateChild() 内部实现
// Element类 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) { if (newWidget == null) { if (child != null) deactivateChild(child); return null; } final Element newChild; if (child != null) { bool hasSameSuperclass = true; if (hasSameSuperclass && child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); newChild = child; } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); newChild = child; } else { deactivateChild(child); newChild = inflateWidget(newWidget, newSlot); } } else { newChild = inflateWidget(newWidget, newSlot); } return newChild; }
首先,newWidget为null时,可以理解为要显示一个空白的页面,那么就需要清空之前页面中的元素,调用deactivateChild()会解除child与父element的绑定、detach该child下所有子RenderObject、并将该element加入到BuildOwner的_inactiveElements列表中等待销毁或重用。
接下来分两种情况,一是当之前页面中已经有显示的内容了(child不为null),二是未显示过内容,是一个新页面(符合第一帧的情况)
-
child != null,判断下该child的widget配置信息是否没有变化,没有的话,如果slot有更新,就更新个slot就够了,不需要重新创建element。
或者当canUpdate()返回true,重写Element的update()方法自定义更新element,否则就执行默认流程,deactivateChild()去掉原来显示内容,然后通过inflateWidget()新建一个element。
-
child = null,通过inflateWidget()新建一个element。
毫无疑问,渲染第一帧时,会走第二种情况,接下来看看inflateWidget()做了些啥。
// Element类 Element inflateWidget(Widget newWidget, Object? newSlot) { final Key? key = newWidget.key; if (key is GlobalKey) { final Element? newChild = _retakeInactiveElement(key, newWidget); if (newChild != null) { newChild._activateWithParent(this, newSlot); final Element? updatedChild = updateChild(newChild, newWidget, newSlot); return updatedChild!; } } final Element newChild = newWidget.createElement(); newChild.mount(this, newSlot); return newChild; }
如果该element之前通过global key缓存了,则看看能不能从globalKey列表里直接取出element进行复用,可以复用的话,再调用_activateWithParent()把它挂在到父element的renderObject树上并把该element加入到dirtyElements列表中,等待更新。
然后调用updateChild()更新子布局。
不能复用则调用newWidget.createElement()重新生成一个element,再调用它的mount()方法,前文提过,mount方法主要做两件事,一是创建RenderObject,将RenderObject挂在父element上,二是更新该Widget下的子布局
不同element可能对mount()的实现不同,但是大体上都是这两个步骤,详见SingleChildRenderObjectElement,MultiRenderObjectElement,ComponentElement对mount()的不同实现,这些是日常中主要会用到的element。
调用mount()方法就形成了一个递归,如此反复,直到所有的子RenderObject都被挂载到树中,至此_rebuild()操作完成。
-
回到RenderObjectToWidgetAdapter类的attachToRenderTree()方法中
// RenderObjectToWidgetAdapter类
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
二、element不为null
element为null,也即渲染第一帧的情况分析完了。当element不为null时,会走element的更新流程,实际上就是更新element的widget配置信息,然后将这个element加入到BuildOwner的_dirtyElements列表中。
接着返回上层
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
scheduleAttachRootWidget()操作完成了,回顾下这个方法里面都做了些什么操作,概括来说,其实就是两步操作
- 创建根窗口的Widget、Element和RenderObject对象
- 构建根窗口下的子布局,将所有子布局的RenderObject都挂载到根RenderObject树上。
scheduleWarmUpFrame()
在上一步,我们已经把RenderObject树准备好了,在这一阶段,需要将准备好的RenderObject树,渲染到屏幕上显示出来,完成第一帧的绘制。
在Flutter运行过程(一):一文搞懂Widget更新机制文章里提过,Flutter更新是通过VSync信号回调,通过PlatformDispatcher.scheduleFrame()触发onBeginFrame
和onDrawFrame
,进入flutter的更新流程。
而第一帧的绘制不需要等待VSync信号的监听。
void scheduleWarmUpFrame() {
...
handleBeginFrame(null);
handleDrawFrame();
...
}
可以看到schudleWarmUpFrame()的具体实现,Flutter在绘制第一帧时直接调用了handleBeginFrame()和handleDrawFrame(),强制渲染根布局。
这两个方法内部的实现,在上一篇文章中已经详细分析过了,所以这里不再花大的篇幅去讨论,
还记得我们在初始化窗口的根RenderObject时,调用了RenderObject.prepareInitialFrame()
,把根RenderObject加入到了_nodesNeedingLayout
列表,和_nodesNeedingPaint
列表中
// RendererBinding类
void initRenderView() {
assert(!_debugIsRenderViewInitialized);
assert(() {
_debugIsRenderViewInitialized = true;
return true;
}());
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.prepareInitialFrame();
}
// RenderView类
void prepareInitialFrame() {
// 加入到_nodesNeedingLayout列表
scheduleInitialLayout();
// 加入到_nodesNeedingPaint列表
scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
}
所以在layout、paint阶段,就会遍历这两个列表,执行每个RenderObject的layout和paint过程,最后将计算好的视图合成成一个layer,发送给GPU渲染。
以上就是Flutter渲染第一帧的全部内容。
最后的最后
研究第一帧的渲染过程,也是我学习和熟悉Flutter的过程,只有对Flutter的运行过程充分熟悉,才能更好地理解Flutter的特性及其原理,希望对你有所帮助。
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
拜托拜托,谢谢各位同学!