React Native 的 ListView 性能问题已解决
长列表或者无限下拉列表是最常见的应用场景之一。RN 提供的 ListView 组件,在长列表这种数据量大的场景下,性能堪忧。而在最新的 0.43 版本中,提供了 FlatList 组件,或许就是你需要的高性能长列表解决方案。它足以应对大多数的长列表场景。 测试数据FlatList 到底行不行,光说不行,先动手测试一下吧。 性能瓶颈主要体现在 Android 这边,所以就用魅族 MX5 测试机,测试无限下拉列表,列表为常见的左文右图的形式。 测试数据如下:
内存方面,ListView 滑动到 1000 条时,已经涨到 350M。这时机器已经卡的不行了,所以没法滑到 2000 条并给出相关数据。而 FlatList 滑到 2000 条时的内存,也比 ListView 1000 条时的内存少不少。说明,FlatList 对内存的控制是很优秀的。 主观体验方面:FlatList 快速滑动至 2000 条的过程中全程体验流畅,没有出现卡顿或肉眼可见的掉帧现象。而ListView 滑动到 200 条开始卡顿,页面滑动变得不顺畅,到 500 条渲染极其缓慢,到 1000 条时已经滑不动了。 通过以上的简单的测试,可以看出,FlatList 已经能够应对简单的无限列表的场景。 使用方法FlatList 有三个核心属性 data 和 ListView 不同,它没有特殊的 [{title: 'Title Text',key: 'item1'}] renderItem 和 ListView 的 _renderItem = ({item}) => ( <TouchableOpacity onPress={() => this._onPress(item)}> <Text>{item.title}}</Text> <TouchableOpacity/> ); ... <FlatList data={[{title: 'Title Text',key: 'item1'}]} renderItem={this._renderItem} /> getItemLayout 可选优化项。但是实际测试中,如果不做该项优化,性能会差很多。所以强烈建议做此项优化! 如果预先知道列表中的每一项的高度(ITEM_HEIGHT)和其在父组件中的偏移量(offset)和位置(index),就能减少一次渲染。这是很关键的性能优化点。 getItemLayout={(data,index) => ( {length: ITEM_HEIGHT,offset: ITEM_HEIGHT * index,index} )} 完整代码如下: // 这里使用 getData 获取假数据 // 数据结构类似于 [{title: 'Title Text',key: 'item1'}] import getData from './getData'; import TopicRow from './TopicRow'; // 引入 FlatList import FlatList from 'react-native/Libraries/CustomComponents/Lists/FlatList'; export default class Wuba extends Component { constructor(props) { super(props); this.state = { listData: getData(),}; } renderItem({item,index}) { return <TopicRow {...item} id={item.key} />; } render() { return ( <FlatList data = {this.state.listData} renderItem={this.renderItem} onEndReached={()=>{ // 到达底部,加载更多列表项 this.setState({ listData: this.state.listData.concat(getData()) }); }} getItemLayout={(data,index) => ( // 120 是被渲染 item 的高度 ITEM_HEIGHT。 {length: 120,offset: 120 * index,index} )} /> ) } } 源码分析FlatList 之所以节约内存、渲染快,是因为它只将用户看到的(和即将看到的)部分真正渲染出来了。而用户看不到的地方,渲染的只是空白元素。渲染空白元素相比渲染真正的列表元素需要内存和计算量会大大减少,这就是性能好的原因。 FlatList 将页面分为 4 部分。初始化部分/上方空白部分/展现部分/下方空白部分。初始化部分,在每次都会渲染;当用户滚动时,根据需求动态的调整(上下)空白部分的高度,并将视窗中的列表元素正确渲染出来。
_usedIndexForKey = false; const lastInitialIndex = this.props.initialNumToRender - 1; const {first,last} = this.state; // 初始化时的 items (10个) ,被正确渲染出来 this._pushCells(cells,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}} /> ); } 既然要使用空白元素去代替实际的列表元素,就需要预先知道实际展现元素的高度(或宽度)和相对位置。如果不知道,就需要先渲染出实际展现元素,在获取完展现元素的高度和相对位置后,再用相同(累计)高度空白元素去代替实际的列表元素。 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 端复用逻辑。而且如果有些机器性能极差,渲染过慢,那些假的列表——空白元素就会被用户看到! 那么为什么要基于 RN 的 ScrollView 组件进行性能优化,而不直接使用 Android 或 iOS 提供的列表组件呢? 最简单回答就是:太难了! 由于本人对 RN 底层原理实现只有简单理解。只能引用 Facebook 大神的解释,起一个抛砖引玉的作用。 以 iOS 的
但是问题是,从 RN render 到真正调用 native 代码这个过程本身是异步的,过程中消耗的时间也并不能保证在 16ms 以内。
那么解决方案就是,在一些需要高性能的场景下,让 RN 能够同步的调用 native 代码。这个答案或许就是 ListView 性能问题的终极解决方案。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |