Flutter 高性能、多功能的全场景滚动容器,一定要看!(1),百度Android岗一面+二面内容

事件回调:我们将事件分类,外部使用时可只监听需要的回调。

刷新配置:为了提升刷新的灵活性,我们将刷新单出抽出,既可以使用我们提供的标准刷新组建,也可自定义。

功能完善


我们为 PowerScrollView 完善了业务使用的核心诉求,包括自动曝光、滚动到某个 index 、瀑布流、刷新加载更多等能力。下面将重点介绍前两部分。

自动曝光能力

在 Flutter 中,通常不得不将曝光放在 build 函数中,这使得曝光会错乱,不在屏幕上但是在屏幕缓冲区的部分将会被错误曝光,且有多次曝光问题,代码臃肿混乱,这都使得业务层非常头疼。曝光能力是各种业务都必须的核心诉求,我们在 PowerScrollView 中统一进行了封装,通过事件回调给使用者。

前面我们知道,在 PowerScrollView 中,我们用 cell 封装了最小粒度的 item,因为对 item 的封装,使得我们的掌控力大大增强。正因为此,我们自定义了 cell 的 StatefulElement,在 element 的生命周期中 mount、unmount 记录当前 element,利用 InheritedWidget ,将树上的 element 维护在外面的列表中。

在 PowerScrollView 的滚动过程中,我们会遍历检查 element 数组,筛选屏幕中的元素进行曝光回调。其中被筛选掉的即为缓冲区的元素,同时维护个数组避免单元素当次屏幕中多次曝光。

为了减少滚动中的多次遍历检查 element 数组,我们加入了控制滚动采样率的可配参数,通过此参数,我们可以控制滚动一定距离后才进行检查。

在复杂场景中,会存在 cell 高度先为 0,下载模板渲染后再撑开的情况,这种情况下整个 element list 数据会非常大,且数据并不正确,我们需要过滤掉这种。但是当 cell 刷新之后,有了真实的高度,我们需要进行正确的曝光。所以我们在 cell 中监听了 size 的变化,当高度由 0 到非 0 的时候,通知上层进行一次曝光。

滚动到某个index

Flutter 本身提供了滚动到 position 距离的能力,但一般业务场景下,我们不知道要滚动的距离,最多知道要滚动到第几个,这使得在 Flutter 侧很多交互无法实现。这个问题我们会分几种场景进行分析。

场景一:当要滚动的目标 index 的 cell 在视图树中(当前屏幕及缓冲区),由于我们已经维护了一个屏幕及缓冲区的element数组,我们可以遍历找到,然后将其滚动到可见区域即可。

场景二:当要滚动的目标 index 的 cell 不在视图树中时,首先我们根据当前屏幕的 index 与目标 index 进行比较,判断是需要往上滚动还是往下滚动。然后,以较快的速度进行特定距离的滚动,滚动之后再递归,直到找到目标 index。由于滚动距离与时间的不确定性,极端情况下会没有动画效果,普通的动画效果可能也会有些生硬。

性能优化


为什么要做局部刷新

在实际的流式业务场景中,经常会因为数据源的更新而刷新整个列表容器:例如加载了下一页的数据、删除或者插入某一个 cell,甚至某个 cell 的一个按钮状态的变化;

刷新范围过大往往是造成列表容器卡顿、流畅度降低的主要原因,严重影响了用户的操作体验。所以我们需要尽量减少 Widget tree 打脏刷新的范围,减少 Element rebuild 的调用,实现局部刷新的能力。

Viewport 刷新的过程

为什么说整个列表容器打脏刷新会带来这面严重的耗时呢?我们来简单看一下 Viewport 的刷新过程。

列表容器被打脏之后,会做两个关键的操作:

Viewport 所有 sliver 的 Element 都会 rebuild;

Viewport 也会重新 layout,进而所有的 sliver 也会重新 layout;

我们来先看 Viewport layout 的过程:这个方法的核心,首先找到当前的 center sliver(默认是第一个child)的位置,然后向上、向下遍历Viewport每一个sliver;每个 child sliver 根据当前 Viewport 在 Scrollview 中的 scrollOffset,Viewport的大小以及cacheExtent大小等信息 (SliverConstraints),计算当前需要展示的child的index范围,layout 每一个在可显示范围的child;

以下图例,SliverList可视范围内需要layout的child index为2~3;SliverGrid需要layout的child index为0~3;

再来看 Viewport 所有 sliver 的 Element rebuild 的过程,这个过程才是列表容器刷新耗时的关键;

我们先来看一下常见的几种布局 SliverList、SliverGrid 以及我们自定义的瀑布流布局 SliverWaterfall 的实现,它们都继承自SliverMultiBoxAdaptorWidget,一个管理多 child(Box模型)的 sliver 的基类;它对应的 Element 是 SliverMultiBoxAdaptorElement,主要负责 child 的创建、更新、移除等生命周期相关的工作,这正是局部刷新需要精细处理的地方。

SliverMultiBoxAdaptorElement 内部维护两个 Map,缓存 child element 以及 child widget,在 ViewPort 需要的时候(上面提到的layout过程)lazily build 自己的 child;

rebuild 过程之所以耗时是因为要清空所有 child widget 缓存,重新 build child widget,update child Element;如果遇到数据的变化,例如 insert、delete,很有可能导致 element 无法复用,这样 rebuild 的成本会更高。

局部刷新的实现原理

摸清了基本原理之后,我们就在思考,当列表容器内容发生变化的时候(比如 insert、delete、LoadMore),是否可以做出一些优化,只让发生变化的部分去 build、layout 呢?

首先我们认为 sliver 的 Element 全部 rebuild 的做法过于简单粗暴,我们可以通过更精准的控制 sliver element 中,childWidgets 与 childElements,来实现局部刷新的目的;

下面我们来看看针对与具体的场景,如何实现精准的 childWidgets 与 childElements 控制,实现局部刷新的能力的。

可变的 child count

在常见的需要局部刷新的场景,容器元素的数量往往会发生变化。在常见的 CustomScrollview 使用中,childCount 都是创建时指定的,当 childCount 方式变化,就需要重新 build 列表容器;

第一步就是避免因为 sliver 内部元素数量变化,必须重新build整个容器的问题;

虽然也可以使用childCount为空,根据builder返回null来决定是否为最后一个child的方式实现可变childCount的目的,但这种方式并不太符合常用的习惯,对使用方也会增加额外成本,所以并未采用这种方式。

做法比较简单,通过继承自SliverChildBuilderDelegate,修改childCount获取方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D4Lrk4S7-1612336882848)(https://upload-images.jianshu.io/upload_images/24957688-78c067342b61a505.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

局部刷新之 LoadMore

LoadMore的实现相对会比较简单,需要做的主要有两点:

  1. 清理widgets缓存,防止不算加载的过程中内存占用过大;保存与 _childElements 中 index 相同的 widget;这里有一个需要特别注意的点:要过滤为 null 的 widget,否则这个位置的 widget 无法正常展示;(_childWidgets 最后一个 index 会是一个为 null 的值,具体为什么插入一个为 null 的 widget 大家可以阅读源码寻找答案)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryEHpvfJ-1612336882849)(https://upload-images.jianshu.io/upload_images/24957688-0efe4d1ea3d7e692.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

  1. 最后打脏sliver,重新layout children:

使用 Dart DevTools 的 TimeLine 数据对比两种 LoadMore 方式的耗时情况如下图:

SetState 的 timeline:

LoadMore 的 timeline:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCCr2kh3-1612336882851)(https://upload-images.jianshu.io/upload_images/24957688-aca4817353ae164a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

局部刷新之 Delete

首先整理 childWidgets 的内容,根据 delete 的 index,重新调整 childWidgets 中 widget 与 index 的对应关系;

接下来是 _childElements 的处理,如果需要删除的 index 还未创建,只需要把当前 sliver 的 RenderObject 的 layout 信息标脏,重新 layout 自己即可。注意这个过程是不会重新 layout 当前 viewport 已经展示的 child 的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUJu4Otl-1612336882852)(https://upload-images.jianshu.io/upload_images/24957688-86350bab365b7ef3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

否则要找到要删除的 child element,deactivate 对应的 element,其对应的 RenderObject 从 Render tree 上移除:

这个过程同时会维护好 child 的 RenderObject 中 ParentData 的 previousSibling 和 nextSibling 的关系;

接下来调整 _childElements 中 Element 与 index 的对应关系;

最后更新每一个 child 的 slot:

最后将sliver的RenderObject标脏,下一帧重新layout刷新。

局部刷新之 Insert

Insert的实现过程与上面的类似,可以根据上面的过程自行实现,这里就不做赘述;

Element 复用能力

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

  • Android前沿技术大纲

  • 全套体系化高级架构视频

Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

GZGpbD-1712511031565)]

  • 全套体系化高级架构视频

    [外链图片转存中…(img-9JTQ3WQm-1712511031566)]

Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值