学习最忌盲目,无计划,零碎的知识点没法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概化二十篇左右文章分析,欢迎关注,共同进步。![Flutter framework]
面试
欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取个人最新文章~缓存
最近由于在作Flutter中相关的性能优化,搜刮了网上全部的文章以后,看到了闲鱼的Flutter 高性能、多功能的全场景滚动容器。但奈何该组件没有开源,所以准备从文章给出的思路尝试研究和开发一个高性能的ScrollView。这个系列预计会分为4-5篇文章,前三篇主要对现有问题研究和分析,后两篇实际的进行开发。性能优化
原理篇:markdown
一、Widget、Element、Render树到底是如何造成的?app
二、ListView的构建过程与性能问题分析less
要想分析ListView的性能问题,首先咱们得掌握ListView的构建过程。在阅读本文以前,最好已经熟悉Flutter三棵树以及基本的布局原理与Flutter滑动原理,否则构建过程的理解可能任然停留在表面。推荐Widget、Element、Render树到底是如何造成的?和总结了30个例子以后,我悟到了Flutter的布局原理,深刻进阶-一张图理清Flutter的滑动原理ide
上一期文章中咱们学习了Flutter中三棵树的构建过程,引伸出Flutter UI体系中的一个重要设计思想,即:svn
Widget -> 对于每个页面元素的抽象,便于开发者使用。
Element -> 管理整个UI的构建,桥接Widget与RenderObject,提供高效的刷新机制。 RenderObject -> 屏蔽每个元素具体的布局和渲染细节。布局
对于Flutter中任何的控件,咱们均可以从这三个类掌握它的构建渲染过程(animation阶段,build阶段,layout阶段,paint阶段)!!! 一样的,咱们也以这三个类为线索剖析ListView,先来张总览图压压惊!post
看到这张图是否是头皮发麻,别急,咱们一步步分析。
首先,咱们将聚光灯打在咱们今天的主角ListView上
上图中咱们能够看出,首先ListView继承于BoxScrollView继承于ScrollView继承于StatelessWidget。咱们知道StatelessWidget是组合类的Widget,它只是在build()方法中组合多个Widget造成嵌套结构。在这个继承关系中build()方法由ScollView实现。
这个方法中,首先调用了 List<Widget> buildSlivers(BuildContext context)
这个抽象方法获得一个Widget的集合 slivers。这个方法最终由ListView实现,返回的是一个SliverList(slivers集合中只有这一个元素)。这个集合做为参数被传入buildViewport
,而这个方法的结果被嵌套在Scrollable中
这个方法返回一个Viewport类的组件,Viewport是一个能够显示多个Widget(采用Sliver布局协议)的组件,根据滑动偏移显示不一样区域,这个滑动偏移由Scrollable收集滑动手势提供。整个Widger的主要嵌套结构就是 ScollView(ListView) -> Scrollable -> Viewport -> SliverList 。看完这两段源码以后回过去看上面的小图,是否是清晰了许多。
咱们知道,Flutter中,widget只是一个配置文件,构建过程当中主要的开销在于Element树创建与更新,由BuildOwner管理。根据上面梳理的Widget嵌套结构,咱们能够查出对应的三棵树的结构(忽略其中次要嵌套结构)
在Widget、Element、Render树到底是如何造成的一期中提到,Element树造成的过程就是根据Widget的嵌套的每个节点递归的调用Element.mount()这个方法将本身插入树中。对于组合类的Widget-ScrollView和Scrollable咱们很清楚,他的mount()过程核心在于updateChild(_child, built, slot)
方法,在第一次构建的时候这个方法会调用子节点的inflateWidget(newWidget, newSlot)
生成对应的Element对象并插入到树中。
而对于渲染类的Widget-ViewportElement在上一篇文章中也提到了,child节点的element集合会挂载到他的children属性上,RenderObject对象经过双向链表进行管理。这里因为Viewport下面只有一个child即SliverList,因此这里他只有一个子节点SliverMutiBoxAdaptorElement。而最后的SliverMultiBoxAdaptorElement节点中,咱们发现他并无重写mount()方法
因此这里执行的是父类RenderObjectElement的mount()。
RenderObjectElement.mount()这个方法咱们在上一期分析过,首先调用super.mount()将本身挂载在Element树上。以后的核心逻辑就是图中标记的方法。这个方法会向上找到最近的RenderObject,而后将本身挂载上去,造成RenderObject树。
看到这里实际上Element和Render树都只到了ListView这一层级,与每个item没有关联。那么咱们在使用ListView的时候,每个item节点到底是如何插入到这个树中的呢?
要解决上面的疑惑,先思考两个本质问题。
一、在当前Flutter的UI体系中,有没有Widget能够绕过Element树直接显示到屏幕上(不考虑Scene等底层Api)?
二、若是ListView的item在mount阶段就所有挂载到element树上了,会有什么问题?
第一个问题,若是这样的Widget,那么ListView的每个item可能不须要挂载就能够显示。但就目前个人了解,是不存在的(若是有误,欢迎评论交流)。渲染到屏幕上的Widget最终都会经过RenderObject实现绘制的细节。查看RenderObject的markNeedsPaint()方法,在其调用里面有一个关键点,就是他会依赖树形结构。而RenderObject树的造成依赖RenderObjectElement。因此ListView的每个item必定会在某个阶段并入到Element和RenderObject树中。
第二个问题,通常咱们在使用ListView的时候每每是item数量较多,若是在mount阶段一次性挂载了全部的节点,那么在构建的节点很容易发生卡顿,借鉴原生的思路也有一个重要的设计方法懒加载。
懒加载能够理解为按需加载,如何理解"按需"?"按需"就是须要显示到屏幕上的页面元素,那么咱们如何判断这个元素须要显示到页面上呢?最简单的思路就是,在布局过程过程当中,不停的布局子节点,直到当前窗口范围被布满或者没有子节点。在Flutter中,还额外增长了一个缓存区(double cacheExtent),因此这个范围变成了窗口大小加上缓存区大小(默认是250)
提到布局,那么天然咱们从RenderObject树开始捋,先看看RenderViewport的布局过程。
///布局仅由父节点决定,与child节点无关,宽高从performResize中获取
@override
bool get sizedByParent => true;
@override
void performResize() {
assert(() {
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
///抛出没有宽高限制的异常
}
return true;
}());
///尺寸为宽高的最大值
size = constraints.biggest;
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
复制代码
由于RenderViewport中sizeByParent为true,说明他的大小仅由父元素给约束决定,与子节点无关。 再看他的performLayout()
@override
void performLayout() {
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
mainAxisExtent = size.height;
crossAxisExtent = size.width;
break;
case Axis.horizontal:
mainAxisExtent = size.width;
crossAxisExtent = size.height;
break;
}
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
int count = 0;
do {
///尝试布局
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
///有偏差,修正
offset.correctBy(correction);
} else {
///没有偏差,跳出循环
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
break;
}
count += 1;
} while (count < _maxLayoutCycles);
}
复制代码
这个方法会调用_attemptLayout,最终调用layoutChildSequence对于viewport中的每个child进行layout(使用Sliver约束布局,区别于以前提到的Box约束)因为咱们的child只有一个即RenderSliverList,因此查看他的布局过程是怎样。
源码太长,以Element为线索的话,我画了时序图标明主要的流程。RenderSliverList中有个循环,当endScrollOffset小于targetEndScrollOffset的时候,会调用insertAndLayoutChild()
,这个方法最终会调用到SliverMultiBoxAdaptorElement中,由代理类SliverChildBuilderDelegate生成child(ListView的itemBuilder传递到这儿),以后对每个child进行layout,累加endScrollOffset。
有了这样的认识回去看前面的结构图,是否是要更清晰了一点。
固然这里面有两个细节咱们能够关注一下
一、在RenderSliverList的布局过程当中,child节点的element建立是运行在BuildOwner的buildScope方法中
二、ListView会对每个child节点经过delegate嵌套KeyedSubtree、AutomaticeKeepAlive、RepaintBoundary组件
最后借用upYang大佬在Flutter ListView 是如何管理 item 的?中画的两张神图表示这个过程。
一、ListView的建立
二、ListView的滚动
在粗略的了解了ListView的构建过程以后,咱们开始对ListView使用过程当中的问题进行分析。
闲鱼的Flutter 高性能、多功能的全场景滚动容器一文中提到,咱们在使用ListView的使用每每会组合刷新控件,添加加载更多的功能。当加载更多的时候,咱们通常会经过刷新列表来显示更多元素。最终会调用到SliverMultiBoxAdaptorElement.performRebuild()
清空全部 child widget 缓存,从新 build child widget,update child Element;若是遇到数据的变化,例如 insert、delete,颇有可能致使 element 没法复用,这样 rebuild 的成本会更高。经过断点发现,调用setState()以后,item的build会重走一遍。这时若是对于ListView有粒度更细的操做,例如原生上Adapter的增长删除等操做,那么在这种场景下就能带来必定的优化。
其次就是Element的复用,SliverMultiBoxAdaptorElement 经过 _childElements 来缓存 elements,当滚动超出 viewport 的显示以及预加载范围或者数据源发生变化,会经过调用 collectGarbage 方法回收不须要的 elements;
这个方法最终会调用到SliverMultiBoxAdaptorElement.removeChild(RenderBox child)
其中核心的updateChild方法的第一个参数传递的是index对应的element对象,而第二个参数变成了null,在原来我一直在错误的使用 setState()? 中提到过,在第二个参数为null的时候,那么以前的element对象会被卸载unmount()。这样在二次建立的时候,该index对应的element对象又会被再次建立。因此这里能够经过创建一个element缓存池,在建立的时候优先从缓冲池获取;
最后一点就是每个item的分帧上屏,我的感受这点比较有意义。由于即便上面咱们将加载更多的场景进行了优化,可是在ListView建立的时候,任会对屏幕上能显示的Widget进行构建,若是item较为复杂,在进入页面的时候,可能发生构建卡顿。经过占位削峰的,将复杂widget分为多帧渲染是一个不错的思路,不过暂时还没想明白如何实现。
这期源码梳理花了很长的时间,远远没有行文时的流畅,由于早期陷入了Sliver约束的布局过程当中,研究了好久。但其实咱们的优化和布局关系不是很大,要根据线索去梳理主干源码,这样才不会陷入其中没法自拔。
最后感谢一下参考学习到各位大佬的文章:
法佬:大佬,sliver的一辈子之敌 Flutter Sliver一辈子之敌 (ExtendedList)
upYang:制图大佬,对于滑动研究很是深入Flutter ListView 是如何滚动的?
TravelingLight_:大佬,能够看看各类Sliver的解析Flutter - 按部就班 Sliver
对于ListView的分析就到这儿了,下面就打算对于这几个问题开干了!理解完原理以后,对于解决问题也有了必定的思路,下一期聊聊对于这个ListView的功能设计与规划,欢迎持续关注!!
最后求个赞QAQ,你的赞是我更新路上强大的动力。