進擊ReactNative-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. 長這樣


  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”就再也劃不動了,過一會又可以接着滑)
      [圖片上傳失敗...(image-a897f6-1563364857791)]

      1. 滾動回調(_onScroll)會安排列表項更新(顯示數據項個數)優先級,優先級低,所以等待空閒觸發更新列表項
      2. 等到滑動到最後一個列表項時,滑不動了,此時空閒,觸發更新列表項
      3. 根據當前狀態,計算出顯示列表項區間[0,11]
      4. render出12個數據項和1個尾部空白區,等待空閒更新列表項
      5. 列表項計算區間沒有變化,更新停止,頁面狀態穩定
    6. 不斷向下滑動,找到一箇中間狀態(比如第一項顯示“第62個 title=62”發現有頂部空白和底部空白)


    7. 快速向上滑動,發現會有空白塊閃爍一下後再顯示出對應內容,滑動流暢


    8. 嘗試設置getItemLayout屬性,發現不再有數據項的佈局回調,而且空白區的高度準確(不會出現滑動到一定位置就滑不動的彩蛋)。這麼看好像性能也沒有提高多少。
      [圖片上傳失敗...(image-28ff22-1563364857791)]

原理

  1. 整個過程就是多render幾次,然後就達到平衡態了。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9c0d5ed14b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
  2. 整個過程有個窗口的概念,通過一通計算,得到當前狀態一個合適的窗口大小。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9cbef91719?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
  3. 各個文件的依賴關係。整體看一下基本就拼接成現在的功能,核心在VirtualizedList。
    [https://user-gold-cdn.xitu.io/2019/7/17/16bffc9d13784962?imageView2/0/w/1280/h/960/format/webp/ignore-error/1]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章