React Native列表视图FlatList使用优化实践指南

9,628 阅读6分钟

列表视图在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层再进行一层的原生组件封装,让使用与其他组件一致。