likes
comments
collection
share

React Native列表组件:FlatList、RecyclerListView对比

作者站长头像
站长
· 阅读数 11

文章前言

永远把别人对你的批评记在心里,别人的表扬,就把它忘了。Hello 大家好~!我是南宫墨言

在京东首页、淘宝首页、抖音视频、微信朋友圈这类信息流页面中,你看一页,还有下一页,看完下一页还有下下页,无穷无尽。这时就需要用到列表组件,而且必须是高性能的列表组件,不能翻着翻着就卡顿起来,否则会给用户极差的体验

在React Native发展早期,也就是2016年,当时没有RecyclerListView,也没有FlatList,当时使用的是官方提供的ListView列表组件,ListView没有内存回收机制,翻一页内存就涨一点,再翻一页内存再涨一点,前5页滚动的时候还是非常流畅,第10页开始感觉到卡顿了,到50页的时候,基本就滑不动了。导致其卡顿的原因就是无限列表太吃内存了,如果手机可使用内存不够了,卡顿就会发生,组件所体现的性能极差,这也是React Native刚出来时被吐槽的做多的地方。因为Listview会一次性创建所有的列表单元格-cell,如果列表数据比较多,则会创建很多的视图对象,而视图对象是非常消耗性能的,所以在业务中遇到无限列表时不建议使用ListView

社区生态再发展一年,也就是2017年,React Native官方推出第二代列表组件FlatList,第一代列表组件ListView也就被废弃了,这时候无限列表性能变得好一些了。但之后随着业务越来越复杂,FlatList的性能表现变得更加糟糕了

React Native在社区上使用比较广泛的列表组件有以下几种:官方提供的高性能列表组件FlatList、开源社区提供的RecyclerListView,下面将针对这几种不同的组件进行分析

通常评判列表卡顿的指标是UI线程的帧率和JavaScript线程的帧率

在业内有人实验过,在已渲染完成的页面中,通过死循环把JavaScript线程卡死,页面依旧能够滚动。这是因为滚动本身是在UI线程进行的,和JavaScript线程无关。但当用户下滑,需要渲染新的列表项时,就需要JavaScript线程参与进来了。如果这时候JavaScript掉帧了,新的列表项就渲染不出来,即便能滚动,用户看到的也是空白页,一样影响用户体验

经过调研发现开源社区所提供的RecyclerListView比官方的FlatList性能更好,那么FlatList和RecyclerListView的优化原理是什么呢?通过查阅资料得知FlatList和RecyclerListView的底层实现都是滚动组件ScrollView,所以先来看看这个ScrollView时何方神圣吧

观看到文章最后的话,如果觉得不错,可以点个关注或者点个赞哦!感谢~❤️

文章主体

感谢各位观者的耐心观看,React Native列表组件正片即将开始,且听南宫墨言QAQ娓娓道来

React Native列表组件:FlatList、RecyclerListView对比

ScrollView

ScrollView是一个支持横向和纵向的滚动组件,在页面中频繁使用到,该组件在Android底层实现用的是ScrollView和HorizontalScrollView,在iOS的底层实现用的是UIScrollView

ScrollView组件类似于Web中的html和body标签,浏览器中的页面之所以能上下滚动是因为html或body标签默认有一个overflow-y:scroll的属性,如果把标签的属性设置为overflow-y:hidden,页面就不能滚动了

我们看下使用ScrollView来实现无限列表会怎么样,看下面代码:

/** 10个item 就能填满整个屏幕,所以渲染很快*/
/** 1000个item相当于100+个屏幕的高度,所以渲染的很慢*/

const NUM_ITEMS = 1000

const renderContent = (itemsCount: number) => {
    return Array(itemsCount).fill(1).map((_, index)=>{
       return (
           <Pressable key={index} >
               <Text>{`Item ${i}`}</Text>
           </Pressable>
       )
    })
}

const App = () => {
    return (
        <SafeAreaView style={{flex:1}}>
            <ScrollView>
                {renderContent(NUM_ITEMS)}
            </ScrollView>
        </SafeAreaView>
    )
}

上面这段代码,说的是使用ScrollView组件一次性直接渲染1000个子视图,这里没有做任何懒加载优化。用户进入页面后第一眼看到的只有屏幕中的信息,一般不超过10条。一次性渲染10条信息,其实很快,就是一眨眼的功夫。但是如果1000数据一次性渲染,算力、内存、耗时都要相应翻100倍,就会导致渲染速度慢下来了,也就是会把大量的计算和内存浪费在用户看不到的地方

使用ScrollView组件时,ScrollView的所有内容都会在首次刷新时进行渲染。内容少的情况下当然无所谓,但是内容多了起来之后,速度也就慢了下来,用户体验也就糟糕起来,针对这种情况的优化方案就是按需渲染,一次渲染少量的数据

FlatList

FlatList列表组件就是“自动”按需渲染的,它是React Native官方提供的第二代高性能列表组件。FlatList组件底层使用的是VirtualizedList,VirtualizedList底层组件使用的是ScrollView组件,因此VirtualizedList和ScrollView组件中的大部分属性在FlatList组件中也可以使用。

FlatList为什么可以自动按需渲染?首先我们要知道,列表组件和滚动组件的核心区别是,列表组件把其内部子组件看做成一个个列表项组成的集合,每一个列表项都可以单独渲染或者卸载。而滚动组件是把其内部子组件看做一个整体,只能整体渲染。而自动按需渲染的前提就是每个列表项可以独立渲染或卸载

简单的讲,FlatList性能比ScrollView好的原因是,FlatList列表组件利用按需渲染机制减少了首次渲染的视图,利用空视图的占位机制回收了原有视图的内存,可以对比一下二者的区别:

/** 从上到下滚动时的渲染方式 */
/** SrcollView 渲染方式:一次渲染所有视图 */
SrcollView0_9  = [{👁},{ },{ },{ },{ }]  // 浏览0~9条列表项
SrcollView10_19 = [{ },{👁},{ },{ },{ }] // 浏览10~19条列表项
SrcollView20_29 = [{ },{ },{👁},{ },{ }] // 浏览20~29条列表项
SrcollView30_39 = [{ },{ },{ },{👁},{ }] // 浏览30~39条列表项
SrcollView40_49 = [{ },{ },{ },,{ },{👁}] // 浏览40~49条列表项

/** FlatList 渲染方式:按需渲染,看不见的地方用 $empty 占位 */
FlatList0_9  = [{👁},{ }]                        // 浏览0~9条列表项
FlatList10_19 = [{ },{👁},{ }]                   // 浏览10~19条列表项
FlatList20_29 = [$empty,{},{👁},{ }]             // 浏览20~29条列表项
FlatList30_39 = [$empty,$empty,{ },{👁},{ }]     // 浏览30~39条列表项
FlatList40_49 = [$empty,$empty,$empty,,{ },{👁}] // 浏览40~49条列表项

在上面的示例中,同样渲染50条数据。ScrollView一次性渲染了50条列表,无论你滚动到哪儿,所有的列表项都是渲染好的。但是FlatList在你浏览0~9条列表项时,只渲染0~19条列表,剩余的20~49条列表项时没有渲染的。当你浏览滚动到第10~19条列表项时,FlatList把20~29条列表项提前加载出来了,这就是按需渲染加载机制。当你继续浏览滚动到20~29条列表项时,FlatList会把0~9条列表项回收,用空元素empty代替,当你再浏览滚动到30~39条列表项时,同理10~19条列表项也会被空元素$empty代替,以此类推,这就是内存回收

实现FlatList自动按需渲染的思路具体可以分为三步:

  1. 通过滚动事件的回调参数,计算需要按需渲染的区域

    每次滚动页面,都会触发ScrollView组件的异步回调onScroll时间。在onScroll事件中,我们可以获取当前滚动的偏移量offset等信息。以当前滚动的偏移量为基础,默认向上数10个屏幕的高度,向下数10个屏幕的高度,这样一共是21个屏幕的内容就是需要按需渲染的区域,其他区域都是无需渲染的区域。这样子即便是异步渲染,我们也不能保证所有JavaScript执行的渲染任务都实时地交由UI线程处理,立刻展示出来。但因为有10个屏幕的内容作为缓冲,用户无论是向上还是向下滚动,都不至于一滚动就看到白屏

  2. 通过需要按需渲染的区域,计算需要按需渲染的列表项索引

    现在我们知道了按需渲染的区域,接着要计算的就是按需渲染列表项的索引。FlatList内部实现的就是通过setState改变按需渲染区域第一个索引和最后一个索引的值,来实现按需渲染

    在计算按需渲染列表项索引时,分两种情况,第一种是列表项高度确定的情况,另外一种是列表项高度不确定的情况

    如果设计师给的列表项的高度是固定的,那我们就可以通过获取列表项布局属性getItemLayout告诉FlatList。在列表项高度确定且知道按需渲染区域的情况下,求按需渲染列表项的索引就是一个简单的四则运算

    如果设计师给的UI稿中是不定高的列表项,换句话说就是高度由渲染内容决定的,那么就没办法直接讲列表项的高度告诉FlatList了,只能先把列表项渲染出来才能获取高度。对于高度未知的情况,FlatList会启用列表项的布局函数onLayout,在onLayout中会有大量的动态测量高度的计算,包括每个列表项的准确高度和整体的平均高度

    在列表项高度不确定且给定按需渲染区域的情况下,我们可以通过列表项的平均高度,把按需渲染的列表项的索引大致估算出来了,即便有误差,比如预计按需渲染区域为上下10个屏幕,实际渲染时只有上下7、8个屏幕也是能够接受的,大部分情况下用户时感知不到屏幕外内容渲染的

    实际生产中,如果你不填getItemLayout属性,不把列表项的高度提前告诉FlatList,让FlatList通过onLayout的布局回调动态计算,用户时可以感觉到滑动变卡的。因此,如果你使用FlatList,又提前知道列表项的高度,我建议你吧getItemLayout属性填上

  3. 只渲染需要按需渲染的列表项,不需要渲染的列表项用空视图代替

    有了索引后,渲染列表项就变得很简单,用setState即可。假设一个屏幕高度的内容由10个列表项组成。在首次渲染的时候,按需渲染的列表项索引是0~110,这时会渲染11个屏幕高度的内容。当用户滑到第11个屏幕时,索引变为0~210,这时再在后面渲染10个屏幕高度的内容。当用户滑到第21个屏幕时,索引是100~310,又会再在后面渲染10个屏幕高度的内容,同时把前面10个屏幕的内容用空视图代替。当然这个过程时顺滑的,列表项时一个个渲染的,而不是一个屏幕渲染或10个屏幕渲染的

综上,我们可以看出FlatList实现原理是将列表中不在可视区域内的视图进行回收(它是将不可见的视图回收:从内存中清除了,下次需要的时候再重新创建)

这种实现方法就要求设备在滚动的时候,能快速的创建出需要的视图,才能让列表流畅的展现在用户面前

需要注意的是FlatList在Android设备上的表现并不是很友好,因为Android设备老化,计算能力跟不上,加上React Native中JS层和Native层之间的交互问题,导致创建视图的速度达不到列表流畅滚动的要求

FlatList是仅渲染将要出现在屏幕上的项目,将不可见的视图从内存中移除,并替换为适当间隔的空白区域。这是一个比较好的优化手段,但同时也导致了大量的视图重新创建以及垃圾回收

RecyclerListView

RecyclerListView是开源社区提供的列表组件,它的底层实现和FlatList一样也是ScrollView,它也要求开发者必须将内容整体分割成一个个列表项,在首次渲染时,RecyclerListView只会渲染首屏内容和用户即将看到的内容,所以它的首次渲染速度很快,在滚动渲染时只会渲染屏幕内的和屏幕附近250像素的内容,距离屏幕太远的内容都是空的。

RecyclerListView的实现灵感来源于Android RecyclerView和iOS UICollectionView原生组件,根据这个启发进行了两方面的优化 :

  1. 和FlatList一致,仅创建可见区域的视图

  2. 和FlatList处理单元格方式不一样,这里采用重用单元格

    使用cell recycling来重用不再可见的视图来呈现项目,而不是创建新的视图对象。因为对于程序而言,视图对象的创建是十分昂贵的,并且伴随着内存的消耗,这意味着如果不断的创建视图,在列表滚动过程中,内存占用量会不断增加

在Android上,动态列表RecyclerView在列表项视图滚出屏幕时,不会将其销毁,相反会把滚动到屏幕外的元素,复用到滚动到屏幕内的新的列表项上。这种复用方法可以显著提高性能,改善应用的响应能力,并降低功耗

如果你开发过Web,可以这样理解复用,原来你要销毁一个浏览器中的DOM,再重新创建一个新的DOM,现在你只改变了原有DOM的属性,并把原有的DOM挪到新的位置上

RecyclerListView的复用机制是,可以把列表比作数组list,把列表项比作数组的元素。用户移动ScrollView时,相当于往数组list后面push新的元素对象,而RecyclerListView相当于把list的第一项挪到了最后一项中。挪动对象位置用到计算资源少,也不用在内存中开辟一个新的空间。而创建新的对象,占用的计算资源多,同时占用新的内存空间

RecyclerListView通过对不可见视图对象进行缓存以及重复利用,一方面不会创建大量的视图对象,另一方面也不需要视图对象和垃圾回收,所以RecyclerListView的性能会优于FlatList

总结

PK:SctrollView、FlatList、RecyclerListView

从底层原来看:

  • ScrollView 内容的布局方式是从上到下依次排列的,你给多少内容,ScrollView就会渲染多少内容
  • FlatList 内容的布局方式也是从上到下依次排列的,它通过更新第一个和最后一个列表项的索引控制渲染区域,默认渲染当前屏幕和上下10屏幕高度的内容,其他地方用空白视图进行占位
  • RecyclerListView 性能较好,但是使用它的前提是列表项类型可枚举且高度确定或者大致确定

在内存上,FlatList要管理21个屏幕高度内容,而RecyclerListView只要管理大概1个多点屏幕高度的内容,那么RecyclerListView使用的内存肯定少

在计算量上,FlatList要实时地销毁新建Native的UI视图,RecyclerListView只是改变UI视图的内容和位置,RecyclerListView在UI主线程计算量肯定少

标题ScrollViewFlatListRecyclerListView
标题从上到下,一次性全部渲染从上到下,按需渲染,空白占位绝对定位,按需渲染,复用同类组件
渲染区域所有当前屏幕+上下10个屏幕当前屏幕+上下250像素
滚动性能视情况而定iOS优秀,Android低端机能用iOS优秀,Android低端机良好
使用场景内容少的页面或者手动优化场景高度不确定场景高度确定或大致确定场景

除了以上几种列表组件,还有shopify官方推出的flashlist列表组件,通过对该组件简单的了解,该组件也是基于recyclerlistview近一层封装的,但是使用上又和flatlist十分相似,后续我会对该列表组件进一步研究并补充。有兴趣的小伙伴可以前往flashlift项目官网进一步了解,谢谢观看

列表是一个很大的话题,牵涉到的性能优化细节和事件内容很多。受限于手机性能,无线列表是经常出现性能问题的重灾区

转载自:https://juejin.cn/post/7252684645979242533
评论
请登录