进击ReactNative-FlatList源码解析

18,240 阅读7分钟

FlatList的性能高在哪里?

阅读原文

考考你

  1. 问:在数据项高度不确定情况下,js侧不具备直接计算组件大小的能力,是怎么知道首屏展示几个数据项?
    答:js侧首屏几个不是直接计算出来的,而是先通过设置的属性估算出几个数据项,同时设置数据项和列表的布局监听回调onLayout,回调中修正数据项个数(如果还有数据项并且屏幕还有空间,则继续添加数据项)。
  2. 问:FlatList只展示屏幕中的个数,那怎么还能快速滚动,Native底层对应的可是普通的滚动容器ScrollView?
    答:FlatList中数据项个数不止屏幕中显示的个数,会有超出屏幕外的多屏数据项(通过windowSize属性设置)。假如实际数据项100,FlatList一屏能展示10个,此时FlatList中的个数可能是30个,如果你快速滑动,只能滑出30个就到底了,需要过一会才发现还能接着滑动。
  3. 问:FlatList中空白项和实际未显示的数据项个数对应么,空白项高度怎么计算?
    答:空白项和实际未显示个数不对应,最多只有两个空白项(顶部空白项和底部空白项)。如果未显示数据项高度已经计算过(之前在界面显示过),则直接累加即可算出,如果位置,则通过估算(已知数据项高度的平均值)。
  4. 问:FlatList滑动起来一点都不卡,必要的计算还是有的,这是怎么做到的?
    答:除少数情况下(用户滑动到的页面是空白),计算工作大部分在InteractionManager.runAfterInteractions(将一些耗时较长的工作安排到所有互动或动画完成之后再进行)执行。有效的避免了和用户交互抢占CPU,而且是用户交互停下来了才触发,避免了跟随用户动作频繁计算。不过弊端就是上面的少数情况下会卡顿(预设的缓冲用完了,只能强制在交互中同步计算)或者看起来像彩蛋(快速滑动还没有显示完所有数据就到底滑不动了、快速滑动下白屏)。
  5. 问:网上说FlatList的原理是“不在屏幕中的组件会被移除,通过空白替代”,和没说一样?
    答:
    1. 长列表最大的硬伤是随着列表不断滑动,数据项越来越多,内存越来越大,然后就OOM了。通过将屏幕外组件移除是解决该硬伤的核心思想(其实核心思想就这么几种,大家都能想到,困惑的其实是怎么做到的)。
    2. FlatList首先会预缓冲很多屏数据,这样不会影响正常显示和滑动功能。其次就是在互动或动画结束后再刷新缓冲区域,这样不会卡。再次通过key保证了缓冲区是增量刷新,并且限制增量大小,确保不会卡。
  6. 问:getItemLayout真能提高性能么?
    答:getItemLayout能直接获取数据项控件位置和大小,无需借助onLayout回调,可以提高位置计算效率。
  7. 问:多级吸顶怎么做的?
    答:实际使用的是ScrollView.js中自带的吸顶功能(通过位移动画实现)。FlatList虽然声明的属性没有说支持吸顶,但通过设置隐藏属性stickyHeaderIndices(这个属性在VirtualizedList.js里面用到,但是没有显示声明,FlatList会将自身所有属性直接赋值给VirtualizedList)能支持吸顶。

怎么看

  1. 直接看源码,功力不够,也没有这个耐心,这和看英文文章一样,看着看着就(~﹃~)~zZ。
  2. 直接打断点一步步Debug,随便一个操作会让你Debug到停不下来,直到怀疑人生。
  3. 首先网上搜一下相关文章,熟悉一下大概。其次从核心入口render方法大概看看,找到一些关键函数。再次就是直接打日志(js代码就是这个好,依赖文件直接加日志就可以跑),串一下思路。再再次就是日志串不起来再再回头断点看看,来来回回你就懂了。

剖析

  1. 整个Demo
export default class App extends Component<Props> {
    renderItem = (item) => {
        var txt = '第' + item.index + '个' + ' title=' + item.item.title;
        var bgColor = item.index % 2 == 0 ? 'red' : 'blue';
        return <Text style={[{flex: 1, height: 100, backgroundColor: bgColor}, styles.txt]}>{txt}</Text>
    }

    render() {
        var data = [];
        for (var i = 0; i < 1000; i++) {
            data.push({key: i, title: i + ''});
        }

        return (
            <View style={{flex: 1}}>
                <View style={{flex: 1}}>
                    <FlatList
                        initialNumToRender={1}
                        windowSize={2}
                        renderItem={this.renderItem}
                        data={data}>
                    </FlatList>
                </View>

            </View>
        );
    }
}
  1. 长这样
    demo
  2. 加点日志
    1. VirtualizedList.js
      1. console.log('SSU', '\n\nVirtualizedList#render(){列表开始渲染}\n\n', this.state);
      2. console.log('SSU', 'VirtualizedList#render()$lead_spacer{列表添加头部空白块}',{lastInitialIndex, initBlock, first, firstBlock}, {[spacerKey]: firstSpace});
      3. console.log('SSU', 'VirtualizedList#render()$tail_spacer{列表添加尾部空白块}', {last, lastFrame, end, endFrame},{[spacerKey]: tailSpacerLength})
      4. console.log('SSU', 'VirtualizedList#_pushCells(){填充列表项}',{first, last}, cells);
      5. console.log('SSU', 'VirtualizedList#_onCellLayout(){开始列表项布局回调}', cellKey, index);
      6. console.log('SSU', 'VirtualizedList#_onCellLayout(){列表项布局回调结束}', next, this._frames);
      7. console.log('SSU', 'VirtualizedList#_onLayout(){列表布局回调}', e.nativeEvent.layout);
      8. console.log('SSU', 'VirtualizedList#_onLayout(){列表布局回调修正滚动参数}this._scrollMetrics.visibleLength=', this._scrollMetrics.visibleLength);
      9. console.log('SSU', 'VirtualizedList#_onContentSizeChange(){列表内容请大小变化回调}', width, height, this._scrollMetrics);
      10. console.log('SSU', 'VirtualizedList#_onScroll(){滚动开始}', e.nativeEvent.layoutMeasurement, e.nativeEvent.contentSize, e.nativeEvent.contentOffset);
      11. console.log('SSU', 'VirtualizedList#_onScroll(){滚动修正滚动参数}', this._scrollMetrics);
      12. console.log('SSU', 'VirtualizedList#_onScroll(){滚动结束}');
      13. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){安排列表项更新优先级}', {first, last}, {offset, visibleLength, velocity}, itemCount);
      14. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级高,直接更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, _hiPriInProgress: this._hiPriInProgress});
      15. console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){优先级低,等空闲再更新列表项}', {hiPri, _averageCellLength: this._averageCellLength, _hiPriInProgress: this._hiPriInProgress});
    2. Batchinator.js
      1. console.log('SSU', 'Batchinator#schedule(){安排列表项更新}');
      2. console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){开始执行列表项更新}');
      3. console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){结束执行列表项更新}');
    3. VirtualizeUtils.js
      1. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){开始计算屏幕渲染列表项区域}', {maxToRenderPerBatch, windowSize}, prev, scrollMetrics);
      2. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){计算屏幕渲染列表项区域}', {visibleBegin, visibleEnd}, {overscanLength, fillPreference, overscanBegin, overscanEnd});
      3. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last});
      4. console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成计算屏幕渲染列表项区域}', prev, {first, last}, {overscanFirst, overscanLast, newCellCount});
    4. 初始化显示
      1. 根据设置参数预估显示数据项区[0,1](不用担心是否显示一屏,上述Demo初始数据项个数为1)

      2. 数据项布局变化、列表布局变化、列表内容区大小变化均会安排列表项更新(显示数据项个数)优先级
      3. 根据数据项返回高度、列表高度、列表内容区大小计算出显示不满一屏,直接更新列表项
      4. 计算出当前状态下计算出显示列表项区间[0,8],通过setState触发重新render
      5. render出9个数据项和1个尾部空白区,接着走2,因为此时一屏可以显示下,优先级低,所以等待空闲触发更新列表项
      6. 计算出当前状态下不需要继续添加数据项,setState没有变化,更新停止,页面状态稳定
    5. 滚动显示(用力向下滑动,发现滑动到“第8个 title=8”就再也划不动了,过一会又可以接着滑)
      1. 滚动回调(_onScroll)会安排列表项更新(显示数据项个数)优先级,优先级低,所以等待空闲触发更新列表项
      2. 等到滑动到最后一个列表项时,滑不动了,此时空闲,触发更新列表项
      3. 根据当前状态,计算出显示列表项区间[0,11]
      4. render出12个数据项和1个尾部空白区,等待空闲更新列表项
      5. 列表项计算区间没有变化,更新停止,页面状态稳定
    6. 不断向下滑动,找到一个中间状态(比如第一项显示“第62个 title=62”发现有顶部空白和底部空白)
    7. 快速向上滑动,发现会有空白块闪烁一下后再显示出对应内容,滑动流畅
    8. 尝试设置getItemLayout属性,发现不再有数据项的布局回调,而且空白区的高度准确(不会出现滑动到一定位置就滑不动的彩蛋)。这么看好像性能也没有提高多少。

原理

  1. 整个过程就是多render几次,然后就达到平衡态了。
  2. 整个过程有个窗口的概念,通过一通计算,得到当前状态一个合适的窗口大小。
  3. 各个文件的依赖关系。整体看一下基本就拼接成现在的功能,核心在VirtualizedList。