列表视图在app中是非常常见的,目前React Native比较严重的性能问题集中在FlatList大列表等地方,以下通过js层的优化,甚至原生层的优化封装,使性能媲美原生。
FlatList
React Native 0.43版本推出FlatList替代ListView,FlatList实现继承自VirtualizedList,底层的VirtualizedList提供更高的灵活性,但使用便捷性不如FlatList,如无特殊需求无法满足直接使用FlatList。VirtualizedList实现继承自ScrollView,所以FlatList继承了VirtualizedList和ScrollView全部的props,在查阅相关文档时,如在FlatList中找不到相应的prop或者方法可以使用另外两个组件的。React Native的FlatList与android listview、ios uitableview相似,将屏幕外的视图组件回收,达到高性能的目的。
用法
以下实例代码均使用typescript
基本使用
<FlatList<number>
// 数据数组
data={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
// key
keyExtractor={(item, index) => index.toString()}
// item渲染
renderItem={({item: num}) => (
<Text>{num}</Text>
)}
/>
复制代码
常用props
extraData
有除data以外的数据用在列表中,在此属性中指定,否则界面很可能不会刷新
horizontal
设置为 true 则变为水平布局模式
inverted
翻转滚动方向,多用于聊天列表之类反向展示数据
numColumns
指定一列显示多少个item
常用方法
scrollToEnd
滑动到视图底部
scrollToIndex
滑动到指定位置
scrollToOffset
滑动到指定像素
上拉加载
<FlatList
// 上拉回调
onEndReached={() => console.log('上拉加载')}
// 滑动到最后视图内容比例,设置为0-1,例如0.5则表示滑到最后一个视图一半开始回调
onEndReachedThreshold={0.1}
/>
复制代码
下拉刷新
<FlatList
// true显示刷新组件
refreshing={this.state.refreshing}
// 下拉回调
onRefresh=(async () => {
this.setState({
refreshing: true
});
await 耗时操作
this.setState({
refreshing: false
});
});
/>
复制代码
滑动事件
onTouchStart
手指按下开始滑动,调用一次,用于监听交互开始
onTouchMove
手指滑动,调用多次
onTouchEnd
手指松开,调用一次,开始惯性滚动,用于监听交互结束
onMomentumScrollBegin
惯性滚动开始,调用一次,用于监听滑动惯性动画开始
onMomentumScrollEnd
惯性滚动结束,调用一次,用于监听滑动惯性动画结束
onScroll
滑动中,调用多次,用于监听滑动位置
onScrollBeginDrag
开始滑动,调用一次,用于监听滑动开始
onScrollEndDrag
滑动结束,调用一次,用于监听滑动结束
分页
用以开发简单轮播视图,分页滑动查看内容等
// 当前视图索引
private index = 0;
// 必须与this绑定,否则抛出异常
private viewabilityConfig = {viewAreaCoveragePercentThreshold: 100};
handleViewableItemsChanged = (info: { viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
// index为当前可见视图在view的索引
this.index = info.changed[0].index!;
}
<FlatList
// 每次滑动后一个item停留在整个视图
pagingEnabled={true}
// 可见视图设置,1-100,50表示一半可见时回调,100表示全部可见时回调
viewabilityConfig={this.viewabilityConfig}
// 可见视图变更回调
onViewableItemsChanged={this.handleViewableItemsChanged}
// onViewableItemsChanged会多次回调,监听惯性滑动结束判断分页滑动结束,如需要实时判断视图索引显示,则直接使用onViewableItemsChanged
onMomentumScrollEnd={() => console.log('滑动至', this.index)}
/>
复制代码
优化
removeClippedSubviews
移除在屏幕外组件,默认为true,对性能有最大的影响,不要修改为false
windowSize
保持视图个数,即在屏幕外也不移除,默认值为11,在高耗性能组件中,可以适当设置小的值,在会快速滑动的视图中,设置大的值如300,避免快速滑动后当前视图还没有渲染出现空白。
getItemLayout
获取高度,如视图高度固定,设置该属性可以大大改善性能,避免了渲染过程中每一次都需要重新计算视图高度。
getItemLayout={(data, index) => ({length: height, offset: height * index, index})}
key
合理设置key提高react对组件的复用,能很大的优化性能,在组件移出屏幕外,被回收后复用。
原生优化
在要求极高的列表视图中,数据达上千甚至上万,在部分情况FlatList已经无法满足,特别是android设备。以下介绍如何直接使用原生android RecyclerView视图来完成高要求的列表视图。
原生视图代码
public class MyFlatListManager extends SimpleViewManager<MyFlatListManager.MyRecyclerView> {
// 自定义RecyclerView
public static class MyRecyclerView extends RecyclerView {
// 数据列表
public List<Data> list = new ArrayList<>();
// 适配器
public MyAdapter myAdapter;
// 布局管理器
public LinearLayoutManager mLayoutManager;
public MyRecyclerView(Context context) {
super(context);
myAdapter = new MyAdapter(this, list);
// 设置为垂直方向
mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
setLayoutManager(mLayoutManager);
// 固定高度避免重新测量,提高性能
setHasFixedSize(true);
// 禁止数据变更时动画,避免闪烁
setItemAnimator(null);
setAdapter(myAdapter);
}
@Override
public void requestLayout() {
super.requestLayout();
// react native android根视图requestLayout为空函数,避免加入新视图无法显示或者高度宽度不正确,手动执行测量
post(measureAndLayout);
}
public final Runnable measureAndLayout = new Runnable() {
@Override
public void run() {
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
Log.d(TAG, "measureAndLayout");
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
}
private static class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemView) {
super(itemView);
}
}
private static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
private List<MyViewHolder> holders;
private List<Data> list;
private MyRecyclerView recyclerView;
public MyAdapter(MyRecyclerView recyclerView, List<VideoInfo> list) {
this.list = list;
this.holders = new ArrayList<>();
this.recyclerView = recyclerView;
}
// 视图创建
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.movie_list_row, parent, false);
// 手动重新设置高度,match parent
itemView.getLayoutParams().height = parent.getHeight();
itemView.getLayoutParams().width = parent.getWidth();
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
Data data = list.get(position);
// Log.i(TAG, "setTag " + position);
holder.itemView.setTag(position);
// 绑定视图数据
}
@Override
public int getItemCount() {
return list.size();
}
}
private static final String TAG = "MyFlatListViewManager";
@Override
public String getName() {
return "MyFlatListViewManager";
}
@Override
protected MyRecyclerView createViewInstance(final ThemedReactContext reactContext) {
return new MyRecyclerView(reactContext);
}
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
Map<String, Integer> commandsMap = new HashMap<>();
commandsMap.put("addData", 1);
return commandsMap;
}
@Override
public void receiveCommand(MyRecyclerView root, int commandId, @Nullable ReadableArray args) {
MyAdapter myAdapter = (MyAdapter) root.getAdapter();
switch (commandId) {
case 1:
if (args == null) return;
Log.i(TAG, "addData size: " + args.size());
Integer position = root.list.size();
for (int i = 0; i < args.size(); i++) {
// 初始化值,getData为从map中获取data的函数,自行根据结构实现
Data data = getData(args.getMap(i));
Log.i(TAG, "add data " + data);
root.list.add(data);
}
Log.i(TAG, "addDatas old position " + position + " size " + args.size());
// 通知变更
myAdapter.notifyItemRangeInserted(position, args.size());
break;
}
}
}
复制代码
需要注意的有几个地方
- setHasFixedSize 如果视图高度固定,设置固定高度能提高性能
- setItemAnimator 动画可能会导致在加载图片等的时候闪烁
- requestLayout 必须重新手动触发测量视图,在android中这部分机制被react native屏蔽
- onCreateViewHolder 必须手动设定itemView高度和宽度
react反模式
在原生组件和js层进行props传递,如数据量太大,使用props直接传递已经不合适,数据可能已经达到几m甚至更大。react的props模式已经不再适合这样的场景,在web中也是,大量的数据每一次单个数据的变更都全部重新传递,会导致严重的性能问题。在这种情况下,使用组件ref调用函数来一个一个添加或者一个一个移除相关数组这些大的对象,会很好的提升性能。在android的代码中,不再使用prop传递FlatList的data,而是使用add的方法来添加,然后在js层再进行一层的原生组件封装,让使用与其他组件一致。