Flutter中ListView复用原理探索

Flutter三棵树之间的关系

众所周知flutter中有三颗非常重要的树形结构,分别为widget树,element树和renderObject树
其中widget树,存放渲染内容,只是一个数据结构。创建和销毁十分轻量,在页面的刷新过程中经常会被重建。就个人而言,可以把widget抽象的理解为一个存放配置信息的map。
element树,同时持有widget和renderObject,存放上下文信息,可以说是widget和renderObject的连接件。
renderObject树,负责layout和paint事件。
显然,相对于widget来说,element和renderObject的创建和销毁会使用相当多的资源,那么在这个整个flutter中,复用的应该是element和renderObject,而复用widget其实是个误区。

widget的创建流程

想要明确复用原理,就要先知道创建原理。举一个最简单的例子,对于app下只有一个text,那么这个text的创建流程是这样的。
根widget调用其build方法
之后会执行updateChild方法中
这里的newWidget就是text,不为空,child由于从未创建过所以为空,直接执行inflateWidget方法。
这个方法中实际上就是调用了该widget的createElement方法,创建了对应的element对象,之后通过mount方法将该element对象挂在到element 树的对应位置上。
之后假如该widget还有child的话,就会调用该widget的performRebuild方法,重复整个流程直到遍历完整棵树。
简单来说,就是Framework 调用Widget.createElement 创建一个Element实例,记为element
再调用 element.mount,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置。
当父Widget发生变化,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。在canUpdate方法中通过对比runtimeType和key来确定是否可以复用。
此外如果变化的widget完全相同,但处于不同的位置,将不进行销毁重建,而是直接更改其位置。

ListView复用

有了以上的基础,理解listview的复用应该会简单一些,举个例子。
这是一颗listview结构对应的树,假设左边的item为A,右边的为B,此时滑动listview,A将要滑出屏幕,假设下一个进入屏幕的item与B一样,为了进行复用,就会检查旧树上相同位置的element,按照图中的例子来说,column、text、row、row、image、text、text都可以进行复用,而新创建的只有第一个row下的text,这样对比下来,flutter中listview的复用在布局相似时资源的占用确实比较小。

一个小建议

在创建widget时,尽量保证布局的最大相似性,也即通过判断数据的有无来判断子widget是宽高为0的container还是实际的布局,将其写到同一个方法中,也就能保证最大程度的布局相似。
比如这两个item,就可以在方法中判断图片链接是否为空,为空则图片位置为宽高为0的container,否则为image,其余布局完全相同。
而对于比较极端的情况:上面的item明显是一个column,而下面的既可以是column也可以是row,那么为了资源的节省,推荐一并写成column。

相关推荐:

为什么Python中NumPy的运算速度比原生List快_详解向量化计算与C底层原理

NumPy运算更快的关键在于绕过Python解释器开销、使用连续内存与向量化指令,而非单纯“用了C”;小数组易受缓存未命中和初始化开销影响,需保持数据连续、类型单一、操作可并行。 NumPy运算比Python原生list快,不是因为“它用了C”,而是因为它绕过了Python解释器的运行时开销,并把计...

为什么Python 3.13将引入实验性JIT_探索Copy-and-Patch技术

Python3.13的JIT是仅限源码编译、需显式启用--with-jit且依赖LLVM的实验性特性,不包含在官方二进制包中,无运行时开关或API,仅对极少数纯Python字节码模式做模板化加速,不具备通用优化能力。 copy-and-patch不是用户可启用的开关,也不是运行时能调用的API。Py...

为什么Python字典在3.7版本后更省内存_解析紧凑型字典的实现原理

Python3.7+字典更省内存是因采用紧凑字典结构,平均节省20%~25%:旧版用稀疏哈希表致近1/3内存为占位符,新版拆分为小而稀疏的indices数组和大而紧凑的entries数组,消除指针、填充及哈希缓存三类冗余。 Python3.7+的字典确实更省内存,这不是错觉,而是底层结构从“稀疏哈希...

Python集合运算为什么比循环快_哈希表底层原理与位运算优化

集合交集比for循环快几十倍,本质是哈希表O(1)查找与列表O(n)线性扫描的差异;set底层用位运算哈希索引、开放寻址解决冲突,负载因子超0.75触发扩容;非查重场景或需保序时不宜盲目替换list。 集合交集&为什么比forxina:ifxinb快几十倍 根本不是语法糖,是底层哈希表查表和...

如何在函数间复用类型提示以避免重复声明

本文介绍如何通过TypeAlias和类型提取技巧,在不修改外部库源码的前提下,安全、自动地复用第三方函数的参数类型,实现跨函数的类型推导与mypy严格校验。 本文介绍如何通过`typealias`和类型提取技巧,在不修改外部库源码的前提下,安全、自动地复用第三方函数的参数类型,实现跨函数的类型推导与...

如何在不复制类型提示的情况下复用外部函数的参数类型进行类型推断

通过typing.typealias提取并复用外部函数参数类型,可避免硬编码类型、提升类型一致性与可维护性,尤其适用于无法修改源码的第三方库场景。 通过typing.typealias提取并复用外部函数参数类型,可避免硬编码类型、提升类型一致性与可维护性,尤其适用于无法修改源码的第三方库场景。 在P...