Flutter RecyclerView 移植计划(自用版)

2,082 阅读12分钟

整好了就归纳删掉

如果有更好的想法,请勿吝教~评论区永远欢迎

话说,掘金啥时候出个文章分类功能,用来放一些杂想等不需要公布的东西;

2021.1.26 发现闲鱼好像已经搞出来了

juejin.cn/post/691748…

就是不知道开不开源,先参考看着;

面临的问题

  • Flutter 中触摸动画结束前不会释放重置手势操作对象,而且这个动画啥时候结束还真是一言难尽,看上去结束了其实不然;
  • Android 的RecyclerView 缓存机制给整过来好像有点过头了,先从简单的开始?
  • Android RecyclerView的Diff算法?如何实现局部刷新呢?
  • ItemDecoration、ItemTouchHelper、SnapHelper?
  • Flutter中是通过构造sliver方式来实现的,是不是需要暴露方法用来实现onBind?这块是否是必须的?能否通过Flutter本身的机制替代这个onBind
  • more?

解决方案猜想

  • 第一个问题自定义手势竞争器,实现一个伪事件拦截分发机制,这样好像要自定义Scrollable?不行在以前改造的那个PageView基础上修改呢?那个打通了子Page调用父Page,再打通父Page调用子Page即可,同时或许可以同时实现SnapHelper方面的东西,滑动状态这块也不是拿不到;(再不行,参考extend_tab中的那个方案,强行加速动画,虽然假如实际应用的话,UI这关绝对过不去,但是至少快速滑动这块,差强人意;总之,能跑就行.jpg)
  • flutter本身的自带缓存机制好像就能实现非重构状态下的缓存处理,机制也差不多;先从引入Diff进去,后面完全流程引入再说;
  • 局部刷新,实在不行用Key找widget的方式来做,先研究下;
  • 现在设想中 Flutter的sliver==RecyclerView的layoutManager,所以ItemDecoration这种纯修改布局的问题应该属于sliver这块的部分,通过链式结构设置进去?ItemTouchHelper这个应该是要在sliver构造的子widget外面包层手势监听,然后跟当前子Widget联动处理下,从而判断drag,swip等?SnapHelper这个应该属于PageScrollPhysics 范畴;
  • 关于onBind是否在flutter中是需要的内容,等先弄出diff和局部刷新再试验下

当前进度

第一个问题(手势、事件处理)

最终还是决定还是在原来的PageView上改造,而recyclerView本身做的是被支持嵌套滑动,然后支持嵌套滑动由其他组件实现;

现在方案是仿照nestedScrollView的设计方案,在那上面将所有的事件处理交给一个协调类处理;与nestedScrollerView稍微不同的是,参照Android中的嵌套滑动机制,先访问对应子Page是否可以滑动,再将滑动剩余部分交给父Page处理;试了下好像效果还不错;

这样就不用处理倒底是谁获取了事件、事件是否结束等一系列蛋疼的问题,统统统一处理;管你是从哪里来的,什么状态,来协调类这里都要听他的;

就是工程量好像稍微有点大…………目前还是在原来的PageView基础上改的,把之前的那个快速滑动问题解决后,发现基本没剩啥不用改的,从pageVIew本身和state到内部的controller、position,甚至activity、ScrollPhysics都要改……好像也就剩一个提供基础滑动和事件的scrollable不用动了……

这实现到recyclerView上好像有点麻烦…………再说其实Android上recyclerView本身也不支持嵌套滑动…………或者说他是被支持嵌套滑动而已

要不再开一个nestedScrollerView?亮点是支持任意数量、任意层级,没有限制,同时改造pageView、tabbarView 这些也有相应的支持嵌套等特性?

不过这样问题来了,有什么方案能以最小的侵入性让任意控件支持嵌套滑动呢?好像flutter本身就没想着让太多控件支持嵌套滑动……然而列表这玩意,不管是纵向还是横向,还是蛮常用的…………看下这个协调类抽离,然后参考AutomaticKeepAliveClientMixin 这玩意一样处理下呢?理想状态下,只要后面跟个with 就可以实现嵌套滑动……

2021.1.27

想了想,先不做这块的内容,嵌套滑动机制flutter欠缺的有点严重,而且有点跑题了,先做recyclerView,recyclerView,recyclerView,而非NestedScrollingParent和NestedScrollingChild这种,这已经是另一个技能树了,recyclerView只是实现了NestedScrollingChild接口而已,其本身跟嵌套机制无关


第二个问题(缓存机制)

目前想了下,好像RecyclerView的四层缓存机制并非使用于flutter…………

Android中使用四层缓存机制的主要目的就是为了尽最大可能那个防止重建生成itemView,因为android view生成、findViewById这种性能消耗不小,一个属于xml解析,一个就是DFS,所以做好的做法是创建完itemView并初始化处理之后,后面有数据更新,只更新view展示的数据而非重新生成;

而flutter中因为没有xml这种东西,所以重建性能好很多,而且flutter 中状态更新靠的就是小部件重建,所以想了下还是按flutter中listView本身带的这个缓存机制就行,毕竟,人家就这么设计并且按这个设计方案优化的蛮不错了的嘛

2021.1.26 看了下闲鱼的文章

如果sliver内部重建会造成很大的性能影响,那确实需要使用缓存机制……

那试试将Android的RecyclerView的四层(或者说三层,去掉一个基本没用的缓存层)缓存机制?

好像闲鱼并没有使用Android的这种方式,或者说仅仅引入了复用,而没有使用onBind这种从缓存中获取view并更新的方式

好像也是,只重建需要的小部件就行了

2021.1.27 更新

仔细研究了下element这块的机制,修改了下试想中的架构,以前是像将所有都做在widget层面,现在想想,好像还是管理element还是比较好,毕竟widget这玩意说白了就是一堆炮灰,缓存管理弄得深一点管的多点比较好

另外,等大佬高性能长列表搞出来白嫖不好么?


第三个问题(局部刷新与diff算法)

NewFile -> diff_util.dart

2021.1.26

研究下闲鱼文章中提到的方案

貌似没Diff算法什么的部分,剩下的插入什么的这块的学习下

2021.1.27 局部刷新找到个东西:

ValueListenableBuilder

盲猜直接用这玩意包一层,后面试验下。

明天随便找个scrollerView试验下,今天就先到这,提瓦特大陆还等着我

2021.1.28

在listView这里试验了下,效果显著,还就是只重建valueListenableBuilder里面的内容,但是好像帧率这里提升不咋显著呢,debug模式下开performance分析,也就是从平均42提升到49……profile估计也就那样

是不是因为我这个测试demo item太简单所以看不出来效果?

diffUtil倒是翻译完了,虽然没咋看,但是复制->黏贴->改错 之后发现,好像能跑唉,先研究下这玩意有没有改出bug,顺便看下下一步怎么搞,这玩意真就这么简单就弄过来了?

如果不出太大意外,可行性测验和准备工作差不多了,

先把adapter和layoutManager弄出来


第四个问题(ItemDecoration、ItemTouchHelper、SnapHelper)

ItemDecoration 这玩意还好说 直接归属到sliver这块,没啥难点,说白了,跟Android处理方式一样,flutter中做法也无非在sliver外面包一层或者其他方式,总之对itemBuilder传入的widget拓展处理

ItemTouchHelper 这块有点复杂,我想想有什么组件能带这玩意……

SnapHelper 这玩意没啥好说的,其实就是Physics 改改,可能还需要activity支持;不过问题应该也不大,毕竟上面说的那个PageView那块已经搞出来类似PageSnapHelper这种玩意了

1.27 更新

基本可行性试验完成,这三块貌似从功能实现上来说问题不大;


杂记

关于闲鱼的element复用这块的一些想法

闲鱼这块关于element复用是这么说的

首先我们来分析 element 被回收的过程,SliverMultiBoxAdaptorElement 通过 _childElements 来缓存 elements,当滚动超出 viewport 的显示以及预加载范围或者数据源发生变化,会通过调用 collectGarbage 方法回收不需要的 elements;

我们可以通过重写 collectGarbage 的方式,在不使用 keepAlive 的情况下,截获本该 deactive 的 child element,放入缓冲池中;在需要创建 element 的时候,优先从缓冲池获取;

这么说来,设计的adapter和layoutManger应该深入到element层面,可以说,itemView == element 、 canvas == renderObject 、 holder == widget ,这么理解是否可以?

所以adapter 、 layoutManager 直接设计去控制element呢?Diff这块怎么处理呢?如果 itemViewType 有多个,那就有改变widget树的情况了,这样只在element层面做处理会不会有问题,或者Diff这块做到widget层面,element层面什么都不管,就只知道拿缓存那套?

目前方案中ItemDecoration是从render这层拿的canvas,然后通过打脏canvas强制重绘(而非重建),然后通过这种context重绘机制来触发ItemDecoration这块的canvas处理与绘制;那么这种方式的性能是否有问题?打脏触发机制是什么呢?主动打脏只发生在数据发生改变的时候,其他时候还是走ListView本身的机制呢?

这样的话,layoutManager这块itemDecoration 该怎么设计呢?或者说直接在widget层面加一层包裹呢?这个ItemDecoration是不是也加入缓存比较好? (本身就是对canvas绘制而已,不加入缓存,缓存主体还是element)

关于ItemTouchHelper这块的想法

参考下ReorderableListView?

发现一个好东西 Overlay ReorderableListView就用的这玩意

闲的蛋疼的时候才瞄一眼 开发日记

2021.2.3 摸了两天鱼后,发现摸鱼是真的快乐,那种快乐想必大家都体验过;

但是我身为一个年更博主,我的自尊不允许我这么堕落下去;

所以,我细细研究了下这flutter的三棵树,总结一下:

怎么感觉自定义引擎靠谱点?

widget创建element,renderObject;但是,renderObject拿不到widget啊; 但是Element可以,element不仅持有widget,renderObject也持有;

ItemDecoration这玩意不应该是widget,直接是一个操作RenderObject的类就可以了?????(貌似好像是可以)

另外listView的缓存模式真的蛮简单的…………貌似是没有复用机制…………好像RenderObject该重建还是重建;

或者准确的说,需要加入的应该叫复用机制……

2021.2.4

发现这个RenderObject存了很多东西啊……

比如说,firstChild和lastChild……

通过他俩能获取到存在缓存区和显示区的子RenderObject,然后直接获取parentData就可以拿到index信息,当前Render在整个列表中的偏移量……

所以问题来了:

为啥别的自动曝光框架搞的那么复杂,还要自己维护一个element队列什么的……直接从firstChild遍历到lastChild,找到第一个mainAxisDelta 大于 负·当前childrender大小范围 && 小于 当前child的render范围 的child,然后获取parentData里面的index不就完事了;

同理可获得最后一个可见项的……

要真可行的话,我就写个《即使是哥布林也能在5分钟完成的自动曝光框架》

话说有个大佬说scrollable中有个能直接跳转到对应index的方法;

但是找了半天都没有啊,感觉好像不太可行啊……flutter不是不会加载计算未展示的部分么,那除非所有item高度固定,否则没法计算出要滑动多少距离啊……谁知道没展示的部分每个的宽高什么的……

话说Android是怎么做到的……

额,Android 的做法好像就是在没找到对应View的情况下,一直发滑动10000距离的消息,直到找到对应index的View……

大佬的解析文章: juejin.cn/post/684490…

2021.2.5

发现flutter 搞成这样三棵树的设计方式,还有个很大的优点:

widget element renderObjec 基本隔离,所以wiget树是一个顺序,renderObject完全可以是另一个顺序,比如说:listView renderObject 绘制顺序是从第一个View开始;但是可以重写renderObject,让其从最后一个View开始绘制,而完全不改变交互、数据书匈奴之类的;Android中为了改改绘制层级顺序,就要改View顺序,进而影响到数据顺序……

2021.2.7

好像基本明白了怎么去做复用;

但是回收流程里面一堆assert检测,找找有没有方法能绕过这堆 assert,实在不行只能是闲鱼的那种改写 collectGarbage 方式了;这样一个在element,一个在renderObject……这tm又要跨类传数据,有点烦唉

2021.2.8

找了半天也没找到绕过的方法……这parentData配置的相关数据也要改…………

不过发现parentData里面有个keepAlive,追踪了下好像这个keepAlive就是缓存相关的啊,listView里面已经有个_keepAliveBucket 了,看创建的时候也是先去这块拿;

如果我通过修改这个keepAlive属性,是不是直接可以利用这个_keepAliveBucket 实现复用呢?

或者看下能否重写_keepAliveBucket相关的部分,将它替换为一个有容量上限的队列?加进来的时候先将其keepAlive改为true,超出上限的先抛弃最先加进来的,然后将其keepAlive改为false?

看了下parentData.keepAlive是只影响collectGarbage和其_destroyOrCacheChild,另外也就在applyParentData的时候不同的话更新一下,不参与业务逻辑;感觉有搞头啊;

当然这些都是renderObject层面的;但是感觉既然有这套机制,element估摸也差不多;实在不行,根据renderObejct保存的index来自己做个处理嘛;RenderObject层面的collectGarbage 不也是用同样的原理跑到 element层面去处理了嘛;

是不是闲鱼的自定义engine,版本太低了?怎么感觉好多东西,这不flutter官方都带上了…………