FlatList的性能高在哪裏?html
☞閱讀原文git
考考你
-
問:在數據項高度不肯定狀況下,js側不具有直接計算組件大小的能力,是怎麼知道首屏展現幾個數據項?
答:js側首屏幾個不是直接計算出來的,而是先經過設置的屬性估算出幾個數據項,同時設置數據項和列表的佈局監聽回調onLayout,回調中修正數據項個數(若是還有數據項而且屏幕還有空間,則繼續添加數據項)。
-
問:FlatList只展現屏幕中的個數,那怎麼還能快速滾動,Native底層對應的但是普通的滾動容器ScrollView?
答:FlatList中數據項個數不止屏幕中顯示的個數,會有超出屏幕外的多屏數據項(經過windowSize屬性設置)。假如實際數據項100,FlatList一屏能展現10個,此時FlatList中的個數多是30個,若是你快速滑動,只能滑出30個就到底了,須要過一會才發現還能接着滑動。
-
問:FlatList中空白項和實際未顯示的數據項個數對應麼,空白項高度怎麼計算?
答:空白項和實際未顯示個數不對應,最多隻有兩個空白項(頂部空白項和底部空白項)。若是未顯示數據項高度已經計算過(以前在界面顯示過),則直接累加便可算出,若是位置,則經過估算(已知數據項高度的平均值)。
-
問:FlatList滑動起來一點都不卡,必要的計算仍是有的,這是怎麼作到的?
答:除少數狀況下(用戶滑動到的頁面是空白),計算工做大部分在InteractionManager.runAfterInteractions(將一些耗時較長的工做安排到全部互動或動畫完成以後再進行)執行。有效的避免了和用戶交互搶佔CPU,並且是用戶交互停下來了才觸發,避免了跟隨用戶動做頻繁計算。不過弊端就是上面的少數狀況下會卡頓(預設的緩衝用完了,只能強制在交互中同步計算)或者看起來像彩蛋(快速滑動尚未顯示完全部數據就到底滑不動了、快速滑動下白屏)。
-
問:網上說FlatList的原理是「不在屏幕中的組件會被移除,經過空白替代」,和沒說同樣?
答:
1. 長列表最大的硬傷是隨着列表不斷滑動,數據項愈來愈多,內存愈來愈大,而後就OOM了。經過將屏幕外組件移除是解決該硬傷的核心思想(其實核心思想就這麼幾種,你們都能想到,困惑的實際上是怎麼作到的)。
2. FlatList首先會預緩衝不少屏數據,這樣不會影響正常顯示和滑動功能。其次就是在互動或動畫結束後再刷新緩衝區域,這樣不會卡。再次經過key保證了緩衝區是增量刷新,而且限制增量大小,確保不會卡。
-
問:getItemLayout真能提升性能麼?
答:getItemLayout能直接獲取數據項控件位置和大小,無需藉助onLayout回調,能夠提升位置計算效率。
-
問:多級吸頂怎麼作的?
答:實際使用的是ScrollView.js中自帶的吸頂功能(經過位移動畫實現)。FlatList雖然聲明的屬性沒有說支持吸頂,但經過設置隱藏屬性stickyHeaderIndices(這個屬性在VirtualizedList.js裏面用到,可是沒有顯示聲明,FlatList會將自身全部屬性直接賦值給VirtualizedList)能支持吸頂。
怎麼看
- 直接看源碼,功力不夠,也沒有這個耐心,這和看英文文章同樣,看着看着就(~﹃~)~zZ。
- 直接打斷點一步步Debug,隨便一個操做會讓你Debug到停不下來,直到懷疑人生。
- 首先網上搜一下相關文章,熟悉一下大概。其次從核心入口render方法大概看看,找到一些關鍵函數。再次就是直接打日誌(js代碼就是這個好,依賴文件直接加日誌就能夠跑),串一下思路。再再次就是日誌串不起來再再回頭斷點看看,來來回回你就懂了。
剖析
- 整個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>
);
}
}
複製代碼
- 長這樣
- 加點日誌
- VirtualizedList.js
- console.log('SSU', '\n\nVirtualizedList#render(){列表開始渲染}\n\n', this.state);
- console.log('SSU', 'VirtualizedList#render()$lead_spacer{列表添加頭部空白塊}',{lastInitialIndex, initBlock, first, firstBlock}, {[spacerKey]: firstSpace});
- console.log('SSU', 'VirtualizedList#render()$tail_spacer{列表添加尾部空白塊}', {last, lastFrame, end, endFrame},{[spacerKey]: tailSpacerLength})
- console.log('SSU', 'VirtualizedList#_pushCells(){填充列表項}',{first, last}, cells);
- console.log('SSU', 'VirtualizedList#_onCellLayout(){開始列表項佈局回調}', cellKey, index);
- console.log('SSU', 'VirtualizedList#_onCellLayout(){列表項佈局回調結束}', next, this._frames);
- console.log('SSU', 'VirtualizedList#_onLayout(){列表佈局回調}', e.nativeEvent.layout);
- console.log('SSU', 'VirtualizedList#_onLayout(){列表佈局回調修正滾動參數}this._scrollMetrics.visibleLength=', this._scrollMetrics.visibleLength);
- console.log('SSU', 'VirtualizedList#_onContentSizeChange(){列表內容請大小變化回調}', width, height, this._scrollMetrics);
- console.log('SSU', 'VirtualizedList#_onScroll(){滾動開始}', e.nativeEvent.layoutMeasurement, e.nativeEvent.contentSize, e.nativeEvent.contentOffset);
- console.log('SSU', 'VirtualizedList#_onScroll(){滾動修正滾動參數}', this._scrollMetrics);
- console.log('SSU', 'VirtualizedList#_onScroll(){滾動結束}');
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){安排列表項更新優先級}', {first, last}, {offset, visibleLength, velocity}, itemCount);
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){優先級高,直接更新列表項}', {hiPri, _averageCellLength: this._averageCellLength, _hiPriInProgress: this._hiPriInProgress});
- console.log('SSU', 'VirtualizedList#_scheduleCellsToRenderUpdate(){優先級低,等空閒再更新列表項}', {hiPri, _averageCellLength: this._averageCellLength, _hiPriInProgress: this._hiPriInProgress});
- Batchinator.js
- console.log('SSU', 'Batchinator#schedule(){安排列表項更新}');
- console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){開始執行列表項更新}');
- console.log('SSU', 'Batchinator#schedule()InteractionManager.runAfterInteractions(){結束執行列表項更新}');
- VirtualizeUtils.js
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){開始計算屏幕渲染列表項區域}', {maxToRenderPerBatch, windowSize}, prev, scrollMetrics);
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){計算屏幕渲染列表項區域}', {visibleBegin, visibleEnd}, {overscanLength, fillPreference, overscanBegin, overscanEnd});
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成計算屏幕渲染列表項區域}', prev, {first, last});
- console.log('SSU', 'VirtualizeUtils#computeWindowedRenderLimits(){完成計算屏幕渲染列表項區域}', prev, {first, last}, {overscanFirst, overscanLast, newCellCount});
- 初始化顯示
- 根據設置參數預估顯示數據項區[0,1](不用擔憂是否顯示一屏,上述Demo初始數據項個數爲1)
- 數據項佈局變化、列表佈局變化、列表內容區大小變化均會安排列表項更新(顯示數據項個數)優先級
- 根據數據項返回高度、列表高度、列表內容區大小計算出顯示不滿一屏,直接更新列表項
- 計算出當前狀態下計算出顯示列表項區間[0,8],經過setState觸發從新render
- render出9個數據項和1個尾部空白區,接着走2,由於此時一屏能夠顯示下,優先級低,因此等待空閒觸發更新列表項
- 計算出當前狀態下不須要繼續添加數據項,setState沒有變化,更新中止,頁面狀態穩定
- 滾動顯示(用力向下滑動,發現滑動到「第8個 title=8」就再也劃不動了,過一會又能夠接着滑)
- 滾動回調(_onScroll)會安排列表項更新(顯示數據項個數)優先級,優先級低,因此等待空閒觸發更新列表項
- 等到滑動到最後一個列表項時,滑不動了,此時空閒,觸發更新列表項
- 根據當前狀態,計算出顯示列表項區間[0,11]
- render出12個數據項和1個尾部空白區,等待空閒更新列表項
- 列表項計算區間沒有變化,更新中止,頁面狀態穩定
- 不斷向下滑動,找到一箇中間狀態(好比第一項顯示「第62個 title=62」發現有頂部空白和底部空白)
- 快速向上滑動,發現會有空白塊閃爍一下後再顯示出對應內容,滑動流暢
- 嘗試設置getItemLayout屬性,發現再也不有數據項的佈局回調,並且空白區的高度準確(不會出現滑動到必定位置就滑不動的彩蛋)。這麼看好像性能也沒有提升多少。
-
原理
- 整個過程就是多render幾回,而後就達到平衡態了。
- 整個過程有個窗口的概念,經過一通計算,獲得當前狀態一個合適的窗口大小。
- 各個文件的依賴關係。總體看一下基本就拼接成如今的功能,核心在VirtualizedList。