React Native中ScrollView性能探究

简介:

1 基本使用

ScrollView 是 React Native(后面简称:RN) 中最常见的组件之一。理解 ScrollView 的原理,有利于写出高性能的 RN 应用。

ScrollView 的基本使用也非常简单,如下:


  
  
  1. <ScrollView>   
  2.   <Child1 /> 
  3.   <Child2 /> 
  4.   ... 
  5. </ScrollView>  

它和 View 组件一样,可以包含一个或者多个子组件。对子组件的布局可以是垂直或者水平的,通过属性 horizontal=true/false 来控制。甚至还默认支持“下拉”刷新操作。另外还有一个特别赞的特性,超出屏幕的 View 会自动被移除,从而节省资源和提高绘制效率。我们来看如下一个例子:


  
  
  1. class ScrollViewTest extends Component { 
  2.   
  3.   render() { 
  4.     let children = []; 
  5.   
  6.     for (var i = 0; i < 20; i++) { 
  7.       children.push( 
  8.         <View key={"key_" + i} style={styles.child}> 
  9.           <Text>{"T" + i}</Text> 
  10.         </View>); 
  11.     } 
  12.     return ( 
  13.         <ScrollView style={styles.scrollView}> 
  14.           {children} 
  15.         </ScrollView> 
  16.     ); 
  17.   } 
  18.  

在 Android 上的效果如下:

如图,我们在 ScrollView 中添加了 20 个子组件,但是我们的屏幕任意时刻最多只能显示 5 个子项目。

下面我们来看实际对应的 Native 控件的情况。RN 中的 ScrollView 对应到 Native 的 RCTScrollView,自动把子组件包含在一个 ViewGroup 中(因为Android 的 ScrollView 只能有一个直接子控件),如下图中的红色框内:

注意到,我们在 JS 中添加了 20 个子组件,但是在 RCTViewGroup 中只有在屏幕上显示的 5 个子控件,在屏幕外的组件,也会自动添加到 View 树中,这与 Native 的 ScrollView 表现一致。

其实,RN 中的 ScrollView 有一个 removeClippedSubviews 属性,表示如果子 View 超出可视区域,是否自动移除,虽然默认是 true。但是也需要子 View 的 overflow: 'hidden'属性配合。所以,给子组件的 style 添加如下属性即可。


  
  
  1. <View key={"key_" + i} style={styles.child}>   
  2.   <Text>{"T" + i}</Text> 
  3. </View>; 
  4.   
  5. const styles = StyleSheet.create({   
  6.   child: { 
  7.     ... 
  8.     overflow: 'hidden'
  9.   }, 
  10. });  

得到的效果是,在使用上完全没有区别,而我们来看一下界面的 Tree View,如下图:

可见,屏幕外的子 View,就被自动从 View 树中移除了。

同时,我们来看一下 iOS 平台上的表现,与 Android 上类似:

这印证了我们前面的结论,RN 自动优化了 Native 平台 ScrollView,在这个层面,我们可以说 RN 比 Native 的性能还要高。

2 性能研究

通过上面的实例,我们可以看到,ScrollView 应该是非常高效的,它使用简单,并且还能按需构建 View 树,高效渲染,有点类似 Native 平台上的 ListView 了,是我心目完美 ScrollView 该有的样子。

但是,之前看到腾讯的 TAT.ronnie 一篇文章 探索 react native 首屏渲染最佳实践,文中提到的优化方法,主要就是针对 ScrollView 的。作者认为,在 ScrollView 中,即使不可见(例如,超出屏幕)的组件还是会绘制的。为了优化 ScrollView 的绘制性能,不可见的组件,应该在 JS 中避免添加到 ScrollView 中。

显然,这与我们前面观察到的结论是矛盾的。但是,作者的通过那样处理,确实优化了显示性能,这是怎么回事呢?为了验证,我们也和文中一样,使用 componentDidMount() 和 componentWillMount() 的时间差衡量显示速度。在 Android 上,测试 ScrollView 的子组件数量分别为 10,100,1000 的时候,显示的时间,以及 APP 所占用的内存:

子组件数量 加载时间(ms) 占用内存(MB) 绘制时间*(ms)
10 309 19.7 14.666
100 1170 21.9 15.016
1000 9461 26.5 15.025

* 注,这里的绘制时间,是在 Tree View 中获得的 Draw 时间。

从加载时间看,时间随着子组件的数量线性增加,占用内存也有类似趋势,说明 TAT.ronnie 的改进方法确实是有效的。另外我们也注意到,随着子组件的数量增加,Draw 的时间并没有明显的变化,其实 Measure 和 Layout 时间也没有明显的变化。

说明 ScrollView 虽然有 removeClippedSubviews 属性,也确实在 View Hierarchy 中去掉了不可见的 View。但是组件的加载时间消耗资源还是随着子组件的数量成正比。

3 原因分析

来看一下 RN 中 ScrollView 的相关的源码,主要分析 Android 平台的代码,iOS 类似,就不赘述了。


  
  
  1. // ScrollView.js 
  2. var AndroidScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps);   
  3. var AndroidHorizontalScrollView = requireNativeComponent(   
  4.   'AndroidHorizontalScrollView'
  5.   ScrollView, 
  6.   nativeOnlyProps 
  7. ); 
  8.   
  9. var ScrollView = React.createClass({   
  10.   render: function() { 
  11.     var contentContainer = 
  12.        <View 
  13.          ... 
  14.          removeClippedSubviews={this.props.removeClippedSubviews} 
  15.          collapsable={false}> 
  16.          {this.props.children} 
  17.        </View>; 
  18.   
  19.      var ScrollViewClass; 
  20.      if (Platform.OS === 'ios') { 
  21.        ... 
  22.      } else if (Platform.OS === 'android') { 
  23.        if (this.props.horizontal) { 
  24.          ScrollViewClass = AndroidHorizontalScrollView; 
  25.        } else { 
  26.          ScrollViewClass = AndroidScrollView; 
  27.        } 
  28.      } 
  29.   
  30.      // 为了简单,忽略有下拉刷新的情况 
  31.      return ( 
  32.       <ScrollViewClass ...> 
  33.         {contentContainer} 
  34.       </ScrollViewClass> 
  35.     ); 
  36.   } 
  37. });  

JS 部分的代码逻辑很简单。首先把 ScrollView 所有子组件包装在一个 View contentContainer 中,并继承设置了 removeClippedSubviews 属性。根据 ScrollView 是否是水平方向,决定是用 RCTScrollView 或者 AndroidHorizontalScrollView Native 组件来包含 contentContainer。

所以,我们先来看 RCTScrollView 本地组件对应的代码(AndroidHorizontalScrollView 原理也类似)。JS 中的 RCTScrollView 组件由 com.facebook.react.views.scroll.ReactScrollViewManager 提供,具体的 View 的实现是 com.facebook.react.views.scroll.ReactScrollView。

其中 ReactScrollViewManager 是最基础的 ViewManager 的实现,导出了一些属性和事件。ReactScrollView 则继承于 android.widget.ScrollView,并实现了 ReactClippingViewGroup 接口。关于 Scroll 事件相关的代码我们先忽略,我主要关心 View 绘制相关的代码。主要在下面这段代码:


  
  
  1. @Override 
  2. public void updateClippingRect() {   
  3.   if (!mRemoveClippedSubviews) { 
  4.     return
  5.   } 
  6.   ... 
  7.   View contentView = getChildAt(0); 
  8.   if (contentView instanceof ReactClippingViewGroup) { 
  9.     ((ReactClippingViewGroup) contentView).updateClippingRect(); 
  10.   } 
  11.  

可见,如果不开启 mRemoveClippedSubviews,它就和普通的 ScrollView 一样,否者,它就会调用了它的第一个(也是唯一的一个)子 View 的 updateClippingRect() 方法。从上面的 JS 中我们可以看到,它的第一个子元素应该就是一个 View 组件,对应的 Native 的控件就是 ReactViewGroup。 ReactViewGroup 是 RN for Android 中最基础的控件,它直接继承于 android.view.ViewGroup:


  
  
  1. public class ReactViewGroup extends ViewGroup implements   
  2.     ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView { 
  3.   private boolean mRemoveClippedSubviews = false
  4.   // 用来保存所有子 View 的数组,包括可见和不可见的 
  5.   private @Nullable View[] mAllChildren = null
  6.   private int mAllChildrenCount; 
  7.   // 当前 ReactViewGroup 于父 View 相交矩阵, 
  8.   // 也就是它自己在父 View 中可见区域 
  9.   private @Nullable Rect mClippingRect; 
  10.   ... 
  11.  

在 ReactViewGroup 中实现 removeClippedSubviews 的功能也非常直接,需要更新界面 Layout 的时候,遍历所有的子 View,看子 View 是否在 mClippingRect 区域内,如果在,就通过 addViewInLayout() 方法添加此 View,否者就通过 removeViewsInLayout() 方法移除它。

到这了,我们就可以解释前面的矛盾了。虽然在 ScrollView 的 View Hierarchy 中,会自动移除不显示的 View,但是实际上还是创建了所有的子 View,所以所占内存和加载时间会线性增加。

关于创建所有子 View,我这里可以多分析一下。我们知道在 Android 中,创建 View 的代价是很大的。特别是在 ScrollView 中,所有的子 View 都是同时创建的。如果 ScrollView 中子 View 的数量很多,这样的代价累加起来,对 APP 造成的延迟和卡顿是相当可观的。例如前面的测试中有 1000 个子组件,加载时间竟然长达 9.5 秒。我们用Method Tracing 看一下创建一个子 View 所花的时间,如下图:

这里只是简单的创建一个 TextView 就消耗了大约 25ms 的时间。当然 Tracing 过程本身会拖慢 APP 运行,但是不影响我们的结论。所以 Android 中列表类的控件,都内部支持对 View 的复用,尽量避免创建 View。

通过前面的分析,我们可以得到的结论是:RN 中的 ScrollView 并不像我们想象的那样高性能。

4 ListView

在这里提到 ListView,是因为 RN 中的 ListView 就是基于 ScrollView 的,但是有一些优化。这里简要介绍一些 ListView 的原理。

ListView 其实是对 ScrollView 的一个封装,对应到 Native 平台,和 ScrollView 的表现一模一样。但是 ListView 在显示列表内容的时候,会根据滑动距离,逐步向 ScrollView 中添加子组件(通过调用 renderRow() 方法)。注意到 ListView 有 initialListSize 属性,表示第一次加载的时候添加多少个子项,默认是 10,还有 pageSize 属性,表示每次需要添加的时候,增加多少个子项,默认是 1。

通过上面的分析我们可以看到,ListView 在第一次加载的时候,不论你的列表有多大,默认最多加载 initialListSize 个子项,所以能保证启动速度,如果还没有充满,或者在向下滑动过程中,再组件添加子项。这样的操作似乎比较合理,但是注意到,整个操作中,会逐渐向 ListView 中添加子项,新出现的子项,都是通过创建新的 View,而完全没有复用的过程。所以,如果在应用中,ListView 中的子项数量特别多,ListView 往下滑动的过程中,内存会逐渐上涨的。

值得一提的是,ListView 提供了 renderScrollComponent,可以使用其他 Scroll 组件来替换 ScrollView,并且 RecyclerViewBackedScrollView 组件来作为备选。看到这个名字我很欣喜,说明它支持子项的回收复用(Recycler)。首先,看到 iOS 的实现 RecyclerViewBackedScrollView.ios.js,其实它就是 ScrollView,并没有实现所谓的复用,失望了一半。继续看 Android 的实现,它实际上是对应 Native 的 com.facebook.react.views.recyclerview.AndroidRecyclerViewBackedScrollView,它继承与 Android 的 RecyclerView。看到这里,如果使用这种方法,我直观感觉 RN 的 ListView 性能在 Android 上表现应该会比 iOS 好。

我们继续来看它是怎么实现回收复用的,AndroidRecyclerViewBackedScrollView 内部实现了一个 RecyclerView.Adapter,如下:


  
  
  1. static class ReactListAdapter extends Adapter<ConcreteViewHolder> { 
  2.   
  3.   private final List<View> mViews = new ArrayList<>(); 
  4.   
  5.   public void addView(View child, int index) { 
  6.     mViews.add(index, child); 
  7.     ... 
  8.   } 
  9.   
  10.   public void removeViewAt(int index) { 
  11.     View child = mViews.get(index); 
  12.     if (child != null) { 
  13.       mViews.remove(index); 
  14.       ... 
  15.     } 
  16.   } 
  17.   
  18.   @Override 
  19.   public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 
  20.     return new ConcreteViewHolder(new RecyclableWrapperViewGroup(parent.getContext())); 
  21.   } 
  22.   
  23.   @Override 
  24.   public void onBindViewHolder(ConcreteViewHolder holder, int position) { 
  25.     RecyclableWrapperViewGroup vg = (RecyclableWrapperViewGroup) holder.itemView; 
  26.     View row = mViews.get(position); 
  27.     if (row.getParent() != vg) { 
  28.       vg.addView(row, 0); 
  29.     } 
  30.   } 
  31.   
  32.   @Override 
  33.   public void onViewRecycled(ConcreteViewHolder holder) { 
  34.     super.onViewRecycled(holder); 
  35.     ((RecyclableWrapperViewGroup) holder.itemView).removeAllViews(); 
  36.   }   
  37.  

注意到这里有一个 mViews,用来保存所有的子 View,绑定 View 的时候只是简单用一个空的 View(RecyclableWrapperViewGroup)包了一下。这样一来,RecyclerView 完全没有什么起到复用的作用呀!测试一下,确实也是这样,性能问题还是很严重。

这里我们也可以得到一个结论:RN 中的 ListView 也不是我们想象的 ListView 该有的性能。

5 改进方案

通过前面的分析,我们已经知道了 RN 中的 ScrollView 或者 ListView 的性能瓶颈了,同时也有了改进的思路。下面针对各种情况分析:

  1. 如果要优化首次加载速度,也就是启动速度:可以参考 TAT.ronnie 的文章中的方法,根据实际情况,最小化 ScrollView 或者 ListView 初始子项数量;
  2. 优化内存:因为 ScrollView/ListView 会保存所有子 View 在内存中,因为我们没法删掉子项,但是我们可以尽量减少每个子项所占的内存。例如这个项目 react-native-sglistview,它在子项不可见的时候,就把它退化成一个最基本的 View;
  3. 终极解决方案:要真正达到高性能,就需要尽量少的创建 View,要想办法真正重复利用已经创建的子项。目前只有一些想法,待我实现了,再来更新。




本文作者:佚名
来源:51CTO
目录
相关文章
|
20天前
|
JavaScript 前端开发 算法
在性能上,React和Vue有什么区别
【10月更文挑战第23天】在性能上,React和Vue有什么区别
15 1
|
1月前
|
JSON 前端开发 JavaScript
【简单粗暴】如何使用 React 优化 AG 网格性能
【简单粗暴】如何使用 React 优化 AG 网格性能
29 3
|
1月前
|
缓存 前端开发 JavaScript
10 种方案提升你 React 应用的性能
本文首发于微信公众号「前端徐徐」,作者徐徐分享了提升 React 应用性能的方法。文章详细介绍了 `useMemo`、虚拟化长列表、`React.PureComponent`、缓存函数、使用 Reselect、Web Worker、懒加载、`React.memo`、`useCallback` 和 `shouldComponentUpdate` 等技术,通过实际案例和代码示例展示了如何优化 React 应用的性能。这些方法不仅减少了不必要的重新渲染和计算,还提升了应用的响应速度和用户体验。
20 0
|
6月前
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
【4月更文挑战第30天】对比 Flutter(Dart,强类型,Google支持,快速热重载,高性能渲染)与 React Native(JavaScript,庞大生态,热重载,依赖原生渲染),文章讨论了开发语言、生态系统、性能、开发体验、学习曲线、社区支持及项目选择因素。两者各有优势,选择取决于项目需求、团队技能和长期维护考虑。参考文献包括官方文档和性能比较文章。
240 0
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
|
3月前
|
前端开发 UED 开发者
React组件优化全攻略:深度解析让你的前端应用飞速运行的秘诀——从PureComponent到React.memo的彻底性能比较
【8月更文挑战第31天】在构建现代Web应用时,性能是提升用户体验的关键因素。React作为主流前端库,其组件优化尤为重要。本文深入探讨了React组件优化策略,包括使用`PureComponent`、`React.memo`及避免不必要的渲染等方法,帮助开发者显著提升应用性能。通过实践案例对比优化前后效果,不仅提高了页面渲染速度,还增强了用户体验。优化React组件是每个开发者必须关注的重点。
65 0
|
4月前
|
前端开发 JavaScript Android开发
React Native跨平台开发实战
【7月更文挑战第21天】React Native为跨平台移动应用开发提供了一种高效且强大的解决方案。通过本文的学习,你应该能够掌握React Native的基本概念和实战步骤,并开始在你的项目中使用React Native进行开发。随着你对React Native的深入理解,你将能够利用其强大的功能来构建更加复杂和高效的移动应用。
|
5月前
|
前端开发 自动驾驶 程序员
鸿蒙? 车载?Flutter? React Native? 为什么我劝你三思,说点不一样的
本文探讨了在信息技术快速发展的背景下,开发者如何选择学习路径。作者提倡使用终局思维来规划职业发展,考虑技术的长远影响。终局思维注重长远目标、系统分析、反向规划和动态调整。以车载开发为例,预测未来智能汽车可能由语音助手主导,而非依赖平板界面。此外,作者建议不要过分投入打工状态,应思考创建自己的产品,如App,以实现技能补充和额外收入。选择对未来发展和自主性有益的技术,如Kotlin,比盲目追求热点更为重要。做减法和有标准的选择,能帮助减轻焦虑,实现更高效的成长。关注公众号“AntDream”获取更多相关内容。
132 1
|
6月前
|
开发框架 移动开发 前端开发
【Uniapp 专栏】Uniapp 与 React Native 的对比分析
【5月更文挑战第14天】Uniapp和React Native是热门的跨平台移动开发框架。Uniapp以其一套代码多端运行、丰富的组件生态和较低的学习曲线受到青睐,适合快速开发简单应用。React Native基于React,拥有活跃社区和优秀性能,适合复杂应用。React Native在性能上略胜一筹,尤其在需要接近原生体验的场景。Uniapp的官方组件弥补了社区资源不足。选择时需考虑开发效率、性能需求、团队技术栈和社区支持。
1876 1
【Uniapp 专栏】Uniapp 与 React Native 的对比分析
|
5月前
|
开发框架 前端开发 JavaScript
移动应用开发中的跨平台策略:Flutter与React Native的比较
在移动应用领域,跨平台解决方案已成为开发者追求高效、成本效益和广泛覆盖的关键。本文深入探讨了两种领先的跨平台框架——Flutter和React Native,从技术架构、性能、社区生态及实际应用案例四个维度进行全面对比分析。通过这一比较,旨在为移动应用开发者提供选择合适框架的参考依据,帮助他们根据项目需求做出明智的决策。
|
5月前
|
前端开发 iOS开发 Android开发
React Native跨平台开发实战:从零到一
学习React Native跨平台开发,首先安装Node.js和React Native CLI,设置Android/iOS环境。使用CLI创建项目,如`npx react-native init MyProject`。运行应用:`npx react-native run-android`或`run-ios`。编写组件,如在App.js中创建Hello World。添加样式,安装第三方库如react-native-vector-icons,使用react-navigation进行路由和导航。
110 2