flutter-状态管理1
一 . 什么是状态管理? 为什么需要状态管理?
原生开发的同学可能对"状态"没有什么概念,因为原生开发大多使用命令式框架.比如:new 一个控件,通过set方法改变它的值;
前端同学用过 React/Vue 的,可能对声明式编程和状态管理会熟悉些.
flutter使用 widgets 描述 UI,当用户界面发生变化时,flutter 不会修改旧的实例,而是构造新的 widget 实例,当然为了性能只会构造需要构造实例,这一切都是由框架来计算完成的。
根据Widget描述可知,widget是不可变的,它是对Element的一个描述;如果要实现Widget根据数据变化,需要借助State,使用StatefulWidget完成状态的变化(setState)
image.png
image.png
状态管理 --就是管理数据变化和widget的更新.
为什么需要状态管理? --当我们的项目功能交互很复杂时,一个StatefulWidget里面可能会存在很多子Widget需要刷新,就会调用很多次setState来更新控件,这势必对于性能以及代码的可阅读性带来一定的影响.
二 . 状态管理方式
1.StatefulWidget:最重要的方式 setState,支持规模较小的程序.使用setState会使整个Widget重新构建,如果页面很复杂,就会导致严重的性能损耗.Widget=>Element=>RenderObject
2.inheritedWidget:它提供了一种数据在widget树中从上到下传递、共享的方式,可以实现跨组件传递共享数据.(无法跨页面)
3.scoped_model/redux/bloc/provide/provider/Get...框架
三 .setState为什么可以刷新页面
image.png由方法实现可以看出,setState仅仅做了两件事
- 调用VoidCallback fn
- _element!.markNeedsBuild();
下面我们来看一下_element!.markNeedsBuild();里面到底做了什么
image.png
image.png
- 将自己标记为dirty
- owner.scheduleBuildFor(this);将自己加入_dirtyElements集合中
总结:
setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements合中
上述流程好像并没有看出,setState是如何刷新页面的,想要继续探索,我们首先需要了解一下Flutter渲染机制.
在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。
CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
操作系统在呈现图像时遵循了这种机制,而 Flutter 作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释 Flutter 的绘制原理。
image.png
开始FrameWork层会通知Engine表示自己可以进行渲染了,在下一个Vsync信号到来之时,Engine层会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。其实Windows.onDrawFrame 是绑定到了SchedulerBinding 的 _handleDrawFrame()
我们重点来看下SchedulerBinding 的 _handleDrawFrame
image.png
image.png
-
SchedulerBinding.handleDrawFrame()中对_persistentCallbacks和_postFrameCallbacks集合进行了回调。根据上面的描述可知,_persistentCallbacks中是一些固定流程的回调,例如build,layout,paint。跟踪- - 这个_persistentCallbacks这个集合,发现在RendererBinding.initInstances()初始化中调用了addPersistentFrameCallback(_handlePersistentFrameCallback)方法。
image.png -
_handlePersistentFrameCallback方法
image.png -
drawFrame()
image.png
这里调用了布局,绘制,渲染帧的。而且看类名,这是负责渲染的Binding,并没有调用Widget的构建。这是因为WidgetsBinding是on RendererBinding的,其中重写了drawFrame(),实际上调用的应该是WidgetsBinding.drawFrame()
- WidgetsBinding#drawFrame()
@override
void drawFrame() {
try {
if (renderViewElement != null)
// buildOwner就是前面提到的负责管理widgetbuild的对象
// 这里的renderViewElement是整个UI树的根节点
buildOwner.buildScope(renderViewElement);
super.drawFrame();
//将不再活跃的节点从UI树中移除
buildOwner.finalizeTree();
} finally {
/·················/
}
}
在super.drawFrame()之前,先调用 buildOwner.buildScope(renderViewElement)。
image.png
刚才我们说过,setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements集合中;
而这个buildOwner.buildScope方法会对集合内的每一个对象调用rebuild()。rebuild()这个方法最终走到performRebuild(),这是一个Element中的一个抽象方法。
这个流程,我们也可以通过断点查看
image.png
四 . 为什么setState ()会消耗性能
performRebuild()
image.png这个方法直接调用子类的build方法返回了一个Widget(built),对应调用前面的页面中的build方法。
将这个新build()出来的widget和之前挂载在Element树上的_child(Element类型)作为参数,传入updateChild(_child, built, slot)中.
updateChild(_child, built, slot)
这个方法的上有这样的注释
newWidget == null | newWidget != null | |
---|---|---|
child == null | Returns null. | Returns new [Element]. |
child != null | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
如果之前的位置child为null
A、如果newWidget为null的话,说明这个位置始终没有子节点,直接返回null即可。
B、如果newWidget不为null,说明这个位置 新增加了 子节点, 调用inflateWidget(newWidget, newSlot)生成一个新的Element返回
如果之前的child不为null
C、如果newWidget为null的话,说明这个位置需要移除以前的节点,调用 deactivateChild(child)移除并且返回null
D、如果newWidget不为null的话,先调用Widget.canUpdate(child.widget, newWidget)对比是否能更新。这个方法会对比两个Widget的runtimeType和key,如果一致则说明子Widget没有改变,只是需要根据newWidget(配置清单)更新下当前节点的数据child.update(newWidget);如果不一致说明这个位置发生变化,则deactivateChild(child)后返回inflateWidget(newWidget, newSlot)
如果一个页面是StatefulWidget,我们使用了setState来更新页面,并且没有显示的指定key,这里的setState会走child.update(newWidget);
child.update(newWidget)
update(covariant Widget newWidget)是一个抽象方法,不同element有不同实现,以StatulElement为例
image.png
这个方法调用了State的生命周期 didUpdateWidget,并在最后再次调用了rebuild(),再次走到performRebuild(),不断的递归直到页面的最子一级节点.
-
注意这次调用rebuild()的已经不是PageState了,而是他的第一个子节点Scaffold。
image.png
所以,如果我们在一个较为复杂的页面中, 直接在页面节点调用setState()将会重新调用所有Widget(包括他们中的各种嵌套)的build()方法,会导致严重的性能损耗.
仅为本人学习记录,您也有收获的话,点一点小心心哦~
参考 https://blog.csdn.net/weixin_34221653/article/details/112267280
Flutter渲染机制 https://juejin.cn/post/6974363413942108197#heading-6
关于setState https://juejin.cn/post/6905996819445055495