前言
長列表或者無限下拉列表是最常見的應(yīng)用場景之一。RN 提供的 ListView 組件监婶,在長列表這種數(shù)據(jù)量大的場景下旅赢,性能堪憂。而在最新的 0.43 版本中惑惶,提供了 FlatList 組件煮盼,或許就是你需要的高性能長列表解決方案。它足以應(yīng)對大多數(shù)的長列表場景带污。
使用方法
FlatList 有三個(gè)核心屬性 data renderItem getItemLayout僵控。它繼承自 ScrollView 組件,所以擁有 ScrollView 的屬性和方法鱼冀。
renderItem
和 ListView 的 renderRow 類似报破,它接收一個(gè)函數(shù)作為參數(shù),該函數(shù)返回一個(gè) ReactElement千绪。函數(shù)的第一個(gè)參數(shù)的 item 是 data屬性中的每個(gè)列表的數(shù)據(jù)( Array<object> 中的 object) 充易。這樣就將列表元素和數(shù)據(jù)結(jié)合在一起,生成了列表。這里為了測試性能,放入了一個(gè)文本和圖片
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
可選優(yōu)化項(xiàng)歉提。但是實(shí)際測試中季惯,如果不做該項(xiàng)優(yōu)化,性能會(huì)差很多。所以強(qiáng)烈建議做此項(xiàng)優(yōu)化!如果不做該項(xiàng)優(yōu)化,每個(gè)列表都需要事先渲染一次改备,動(dòng)態(tài)地取得其渲染尺寸,然后再真正地渲染到頁面中蔓倍。
如果預(yù)先知道列表中的每一項(xiàng)的高度(ITEM_HEIGHT)和其在父組件中的偏移量(offset)和位置(index)绍妨,就能減少一次渲染润脸。這是很關(guān)鍵的性能優(yōu)化點(diǎn)。
getItemLayout={(data, index) => (
console.log("index="+index),
{length: itemHeight, offset: itemHeight * index, index}
)}
注意他去,這里有個(gè)坑毙驯,如果設(shè)置了getItemLayout,那么renderItem的高度必須和這個(gè)高度一樣灾测,否則加載一段列表后就會(huì)出現(xiàn)錯(cuò)亂和顯示空白爆价。
官方文檔介紹加入優(yōu)化項(xiàng)性能會(huì)提高很多,測試的時(shí)候發(fā)現(xiàn)加不加影響不大媳搪,可能是我打開方式不正確铭段,有待后續(xù)研究
完整代碼如下:
'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){
// 到達(dá)底部,加載更多列表項(xiàng)
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);
另外一個(gè)坑序愚,運(yùn)行的時(shí)候,加入上拉加載更多和下拉刷新后等限,多下拉幾次以后爸吮,上拉加載更多就不起作用了(觸發(fā)不了onEndReached方法),有可能是是我打開方式不對望门,歡迎各位大神指出我代碼的問題形娇。
源碼分析
FlatList 之所以節(jié)約內(nèi)存、渲染快筹误,是因?yàn)樗粚⒂脩艨吹降?和即將看到的)部分真正渲染出來了桐早。而用戶看不到的地方,渲染的只是空白元素厨剪。渲染空白元素相比渲染真正的列表元素需要內(nèi)存和計(jì)算量會(huì)大大減少哄酝,這就是性能好的原因。
FlatList 將頁面分為 4 部分祷膳。初始化部分/上方空白部分/展現(xiàn)部分/下方空白部分陶衅。初始化部分,在每次都會(huì)渲染钾唬;當(dāng)用戶滾動(dòng)時(shí),根據(jù)需求動(dòng)態(tài)的調(diào)整(上下)空白部分的高度侠驯,并將視窗中的列表元素正確渲染來抡秆。
_usedIndexForKey = false;
const lastInitialIndex = this.props.initialNumToRender - 1;
const {first, last} = this.state;
// 初始化時(shí)的 items (10個(gè)) ,被正確渲染出來
this._pushCells(cells, 0, lastInitialIndex);
// first 就是 在視圖中(包括要即將在視圖)的第一個(gè) item
if (!disableVirtualization && first > lastInitialIndex) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace = this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
// 從第 11 個(gè) items (除去初始化的 10個(gè) items) 到 first 渲染空白元素
cells.push(
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
);
}
// last 是最后一個(gè)在視圖(包括要即將在視圖)中的元素吟策。
// 從 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}} />
);
}
既然要使用空白元素去代替實(shí)際的列表元素檩坚,就需要預(yù)先知道實(shí)際展現(xiàn)元素的高度(或?qū)挾?和相對位置着撩。如果不知道诅福,就需要先渲染出實(shí)際展現(xiàn)元素,在獲取完展現(xiàn)元素的高度和相對位置后拖叙,再用相同(累計(jì))高度空白元素去代替實(shí)際的列表元素氓润。_onCellLayout 就是用于動(dòng)態(tài)計(jì)算元素高度的方法,如果事先知道元素的高度和位置薯鳍,就可以使用上面提到的 getItemLayout 方法咖气,就能跳過 _onCellLayout 這一步,獲得更好的性能挖滤。
return (
// _onCellLayout 就是這里的 _onLayout
// 先渲染一次展現(xiàn)元素崩溪,通過 onLayout 獲取其尺寸等信息
<View onLayout={this._onLayout}>
{element}
</View>
);
...
_onCellLayout = (e, cellKey, index) => {
// 展現(xiàn)元素尺寸等相關(guān)計(jì)算
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);
// 重新渲染一次。最終會(huì)調(diào)用一次上面分析的源碼
this._updateCellsToRenderBatcher.schedule();
}
};
簡單分析 FlatList 的源碼后斩松,后發(fā)現(xiàn)它并沒有和 native 端復(fù)用邏輯伶唯。而且如果有些機(jī)器性能極差,渲染過慢惧盹,那些假的列表——空白元素就會(huì)被用戶看到乳幸!
實(shí)測
性能確實(shí)很高,加載圖片加文章岭参,很流暢反惕。加載到1000條左右的時(shí)候,內(nèi)存占用大概30M(和圖片質(zhì)量有關(guān)系),cpu使用在停止時(shí)候0.5%左右演侯,加載時(shí)候12%左右姿染。