前言
長列表或者無限下拉列表是最常見的應用場景之一。RN 提供的 ListView 組件,在長列表這種數據量大的場景下,性能堪憂。而在最新的 0.43 版本中,提供了 FlatList 組件,或許就是你需要的高性能長列表解決方案。它足以應對大多數的長列表場景。
使用方法
FlatList 有三個核心屬性 data renderItem getItemLayout。它繼承自 ScrollView 組件,所以擁有 ScrollView 的屬性和方法。
renderItem
和 ListView 的 renderRow 類似,它接收一個函數作為參數,該函數返回一個 ReactElement。函數的第一個參數的 item 是 data屬性中的每個列表的數據( Array<object> 中的 object) 。這樣就將列表元素和數據結合在一起,生成了列表。這里為了測試性能,放入了一個文本和圖片
renderItem({item, index}) {
return <View style={styles.listItem}>
<View style={styles.text}>
<Text >{item.title}</Text>
</View>
<View style={styles.image}>
<Image source={{uri:item.imgsource}} style={styles.image} resizeMode='stretch'></Image>
</View>
</View>;
}
getItemLayout
可選優化項。但是實際測試中,如果不做該項優化,性能會差很多。所以強烈建議做此項優化!如果不做該項優化,每個列表都需要事先渲染一次,動態地取得其渲染尺寸,然后再真正地渲染到頁面中。
如果預先知道列表中的每一項的高度(ITEM_HEIGHT)和其在父組件中的偏移量(offset)和位置(index),就能減少一次渲染。這是很關鍵的性能優化點。
getItemLayout={(data, index) => (
console.log("index="+index),
{length: itemHeight, offset: itemHeight * index, index}
)}
注意,這里有個坑,如果設置了getItemLayout,那么renderItem的高度必須和這個高度一樣,否則加載一段列表后就會出現錯亂和顯示空白。
官方文檔介紹加入優化項性能會提高很多,測試的時候發現加不加影響不大,可能是我打開方式不正確,有待后續研究
完整代碼如下:
'use strict';
import React, {Component} from 'react';
import {
FlatList,
AppRegistry,
StyleSheet,
Text,
View,
Image,
} from 'react-native';
export default class ViewPager extends Component {
constructor(props) {
super(props);
this.state = {
listData: this.getData(0),
myindex: 1,
};
}
getData(index) {
var list = [];
for (let i = 0; i < 20; i++) {
let imgsource;
if (i % 5 == 0) {
imgsource = 'http://photo.l99.com/bigger/01/1417155508319_k38f29.jpg';
} else if (i % 5 == 1) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SI5.jpg';
} else if (i % 5 == 2) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH4.jpg';
} else if (i % 5 == 3) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH6.jpg';
} else if (i % 5 == 4) {
imgsource = 'http://img.tupianzj.com/uploads/allimg/160411/9-1604110SH7.jpg';
}
list.push({title: 'title' + (i + (index * 20)), key: 'key' + (i + (index * 20)), imgsource: imgsource});
}
return list;
}
renderItem({item, index}) {
return <View style={styles.listItem}>
<View style={styles.text}>
<Text >{item.title}</Text>
</View>
<View style={styles.image}>
<Image source={{uri:item.imgsource}} style={styles.image} resizeMode='stretch'></Image>
</View>
</View>;
}
render() {
return (
<View style={styles.view}>
<FlatList
data={this.state.listData}
renderItem={this.renderItem}
onEndReached={()=>{
if(this.state.myindex<2){
// 到達底部,加載更多列表項
this.setState({
listData: this.state.listData.concat(this.getData(this.state.myindex)),
myindex:this.state.myindex+1
});
}
console.log("onEndReached=" + this.state.listData.length);
}}
refreshing={false}
onRefresh={() => {
this.setState({
listData: this.getData(0),
myindex:1,
});
console.log("onRefresh=" + this.state.listData.length);
}}
debug={true}
numColumns={1}
getItemLayout={(data, index) => (
// 120 是被渲染 item 的高度 ITEM_HEIGHT。
console.log("index="+index),
{length: itemHeight, offset: itemHeight * index, index}
)}
ListFooterComponent={this.footerView}
onScroll={this._scrollSinkY}
/>
</View>
)
}
footerView() {
return <View style={{flex:1,height:70,justifyContent:'center',alignItems:'center'}}>
<Text>上啦加載更多</Text>
</View>
}
}
const itemHeight = 200;
const styles = StyleSheet.create({
view: {
flex: 1
},
listItem: {
flexDirection: 'row',
flex: 1,
height: itemHeight,
borderBottomWidth: 1,
borderBottomColor: 'red'
},
image: {
height: 180,
width: 150,
},
text: {
height: 180,
width: 100,
},
});
AppRegistry.registerComponent('ViewPager', () => ViewPager);
另外一個坑,運行的時候,加入上拉加載更多和下拉刷新后,多下拉幾次以后,上拉加載更多就不起作用了(觸發不了onEndReached方法),有可能是是我打開方式不對,歡迎各位大神指出我代碼的問題。
源碼分析
FlatList 之所以節約內存、渲染快,是因為它只將用戶看到的(和即將看到的)部分真正渲染出來了。而用戶看不到的地方,渲染的只是空白元素。渲染空白元素相比渲染真正的列表元素需要內存和計算量會大大減少,這就是性能好的原因。
FlatList 將頁面分為 4 部分。初始化部分/上方空白部分/展現部分/下方空白部分。初始化部分,在每次都會渲染;當用戶滾動時,根據需求動態的調整(上下)空白部分的高度,并將視窗中的列表元素正確渲染來。
_usedIndexForKey = false;
const lastInitialIndex = this.props.initialNumToRender - 1;
const {first, last} = this.state;
// 初始化時的 items (10個) ,被正確渲染出來
this._pushCells(cells, 0, lastInitialIndex);
// first 就是 在視圖中(包括要即將在視圖)的第一個 item
if (!disableVirtualization && first > lastInitialIndex) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace = this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
// 從第 11 個 items (除去初始化的 10個 items) 到 first 渲染空白元素
cells.push(
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
);
}
// last 是最后一個在視圖(包括要即將在視圖)中的元素。
// 從 first 到 last ,即用戶看到的界面渲染真正的 item
this._pushCells(cells, Math.max(lastInitialIndex + 1, first), last);
if (!this._hasWarned.keys && _usedIndexForKey) {
console.warn(
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
'item or provide a custom keyExtractor.'
);
this._hasWarned.keys = true;
}
if (!disableVirtualization && last < itemCount - 1) {
const lastFrame = this._getFrameMetricsApprox(last);
const end = this.props.getItemLayout ?
itemCount - 1 :
Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
const endFrame = this._getFrameMetricsApprox(end);
const tailSpacerLength =
(endFrame.offset + endFrame.length) -
(lastFrame.offset + lastFrame.length);
// last 之后的元素,渲染空白
cells.push(
<View key="$tail_spacer" style={{[!horizontal ? 'height' : 'width']: tailSpacerLength}} />
);
}
既然要使用空白元素去代替實際的列表元素,就需要預先知道實際展現元素的高度(或寬度)和相對位置。如果不知道,就需要先渲染出實際展現元素,在獲取完展現元素的高度和相對位置后,再用相同(累計)高度空白元素去代替實際的列表元素。_onCellLayout 就是用于動態計算元素高度的方法,如果事先知道元素的高度和位置,就可以使用上面提到的 getItemLayout 方法,就能跳過 _onCellLayout 這一步,獲得更好的性能。
return (
// _onCellLayout 就是這里的 _onLayout
// 先渲染一次展現元素,通過 onLayout 獲取其尺寸等信息
<View onLayout={this._onLayout}>
{element}
</View>
);
...
_onCellLayout = (e, cellKey, index) => {
// 展現元素尺寸等相關計算
const layout = e.nativeEvent.layout;
const next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: true,
};
const curr = this._frames[cellKey];
if (!curr ||
next.offset !== curr.offset ||
next.length !== curr.length ||
index !== curr.index
) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += (curr ? 0 : 1);
this._averageCellLength = this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
// 重新渲染一次。最終會調用一次上面分析的源碼
this._updateCellsToRenderBatcher.schedule();
}
};
簡單分析 FlatList 的源碼后,后發現它并沒有和 native 端復用邏輯。而且如果有些機器性能極差,渲染過慢,那些假的列表——空白元素就會被用戶看到!
實測
性能確實很高,加載圖片加文章,很流暢。加載到1000條左右的時候,內存占用大概30M(和圖片質量有關系),cpu使用在停止時候0.5%左右,加載時候12%左右。