1基本使用ScrollView是ReactNative(以下简称:RN)中最常用的组件之一。了解ScrollView的原理,有利于编写高性能的RN应用。ScrollView的基本使用也很简单,如下:...可以像View组件一样包含一个或多个子组件。子组件的布局可以是垂直或水平的,由属性horizo??ntal=true/false控制。默认情况下甚至支持“下拉”刷新操作。此外,还有一个特别好的特点。超出屏幕的view会被自动移除,从而节省资源,提高绘图效率。让我们看下面的例子:child}{"T"+i});}return({children});}}在Android上的效果如下:如图所示,我们给ScrollView添加了20个子组件,但是我们的屏幕任何时候最多只能显示5个子项。我们来看看实际对应的Native控件。RN中的ScrollView对应NativeRCTScrollView,自动包含一个ViewGroup中的子组件(因为AndroidScrollView只能有一个直接子控件),如下图红框所示:注意我们在JS中添加了20个子组件组件,但在RCTViewGroup中显示在屏幕上的子控件只有5个。屏幕外的组件也会自动添加到View树中,这与NativeScrollView的表现是一致的。其实RN中的ScrollView有一个removeClippedSubviews属性,表示如果子View超出可见区域是否自动移除,虽然默认是true。但是还需要子View的overflow:'hidden'属性来配合。所以,只需在子组件的样式中添加如下属性即可。{"T"+i};conststyles=StyleSheet.create({child:{...溢出:'隐藏',},});得到的效果在使用上完全没有区别,再来看看界面的TreeView,如下图所示:可以看到自动移除了屏幕外的子View从中删除的视图树。同时我们看一下在iOS平台上的表现,和Android上差不多:这印证了我们之前的结论,RN自动优化了Native平台上的ScrollView。在这个层面上,我们可以说RN比Native有更高的性能。2.性能研究通过上面的例子,我们可以看出ScrollView应该是非常高效的。简单易用,还可以按需构建View树,高效渲染。有点类似于Native平台上的ListView。是我心目中完美的ScrollView。它应该是这样的。不过看到腾讯TAT.ronnie的一篇文章探讨ReactNative首屏渲染的最佳实践。文中提到的优化方法主要针对ScrollView。作者认为,在ScrollView中,即使是不可见的组件(例如屏幕外)仍然会被绘制。为了优化ScrollView的绘制性能,JS中ScrollView不要添加不可见的组件。显然,这与我们之前观察到的结论相矛盾。但是,作者确实通过这样做优化了显示性能。到底是怎么回事?为了验证,我们也像文中一样使用componentDidMount()和componentWillMount()的时间差来衡量显示速度。在Android上,当ScrollView的子组件个数为10、100、1000个时,显示时间和APP占用内存如下:子组件个数加载时间(ms)占用内存(MB)绘制时间*(ms)1030919.714.666100117021.915.0161000946126.515.025*注意,这里的绘制时间是在TreeView中得到的绘制时间。从加载时间来看,时间随着子组件的数量呈线性增加,内存占用也有类似的趋势,说明TAT.ronnie的改进方法确实有效。此外,我们还注意到,随着子组件数量的增加,Drawtime并没有明显变化。事实上,Measure和Layout时间也没有太大变化。说明一下,虽然ScrollView有removeClippedSubviews属性,但是它确实移除了ViewHierarchy中不可见的View。但是,组件加载时间的资源消耗仍然与子组件的数量成正比。3原因分析先看一下RN中ScrollView的相关源码,主要分析Android平台的代码,iOS类似,不再赘述。//ScrollView.jsvarAndroidScrollView=requireNativeComponent('RCTScrollView',ScrollView,nativeOnlyProps);varAndroidHorizo??ntalScrollView=requireNativeComponent('AndroidHorizo??ntalScrollView',ScrollView,nativeOnlyProps);varScrollView=React.createClass({render:function(){varcontentContainer={this.props.children};varScrollViewClass;if(Platform.OS==='ios'){...}elseif(Platform.OS==='android'){if(this.props.horizo??ntal){ScrollViewClass=AndroidHorizo??ntalScrollView;}else{ScrollViewClass=AndroidScrollView;}}//为简单起见,忽略下拉刷新return({contentContainer});}});JS部分的代码逻辑很简单。首先将ScrollView的所有子组件包裹在一个ViewcontentContainer中,继承并设置removeClippedSubviews属性。根据ScrollView是否水平,决定是使用RCTScrollView还是AndroidHorizo??ntalScrollViewNative组件来包含contentContainer。那么,我们先来看RCTScrollView本地组件对应的代码(AndroidHorizo??ntalScrollView原理类似)。JS中的RCTScrollView组件由com.facebook.react.views.scroll.ReactScrollViewManager提供,View的具体实现是com.facebook.react.views.scroll.ReactScrollView。其中ReactScrollViewManager是ViewManager最基础的实现,它会导出一些属性和事件。ReactScrollView继承自android.widget.ScrollView并实现了ReactClippingViewGroup接口。Scroll事件相关的代码我们先忽略,我主要关心View绘制相关的代码。主要在下面的代码中:@OverridepublicvoidupdateClippingRect(){if(!mRemoveClippedSubviews){return;}...ViewcontentView=getChildAt(0);if(contentViewinstanceofReactClippingViewGroup){((ReactClippingViewGroup)contentView).updateClippingRect();}}可见,如果未启用mRemoveClippedSubviews,它与普通的ScrollView相同,否则,它将调用其第一个(也是唯一一个)子视图的updateClippingRect()方法。从上面的JS我们可以看出,它的第一个子元素应该是一个View组件,对应的Native控件是ReactViewGroup。ReactViewGroup是RNforAndroid中最基础的控件。直接继承自android.view.ViewGroup:publicclassReactViewGroupextendsViewGroupimplementsReactInterceptingViewGroup,ReactClippingViewGroup,ReactPointerEventsView,ReactHitSlopView{privatebooleanmRemoveClippedSubviews=false;//用来保存所有子视图的数组,包括可见和不可见private@NullableView[]mAllChildren=null;privateintmAll/ChildrenCount;//当前ReactViewGroup与父View矩阵相交,//也就是自己在父View中的可见区域private@NullableRectmClippingRect;...}ReactViewGroup中removeClippedSubviews的功能也很直接,当需要更新界面Layout时,遍历所有子View,查看子View是否在mClippingRect区域,如果在,则通过addViewInLayout()方法添加View,否则通过removeViewsInLayout()方法移除。至此,我们就可以解释前面的矛盾了。虽然在ScrollView的ViewHierarchy中,没有显示的View会被自动移除,但是实际上所有的子View都是创建的,所以内存占用和加载时间会线性增加。关于所有子View的创建,我这里可以多分析。我们知道在Android中,创建一个View的成本是非常高的。特别是在ScrollView中,所有的子View都是同时创建的。如果ScrollView中的子View数量很多,这样的成本加起来,给APP带来的延迟和滞后是相当可观的。比如之前1000个子组件的测试,加载时间长达9.5秒。下面我们使用MethodTracing来看一下创建一个子View所花费的时间,如下图所示:这里简单的创建一个TextView大约需要25ms。当然,Tracing过程本身会拖慢APP的运行速度,但不影响我们的结论。所以Android中的列表类控件在内部都支持View的复用,尽量避免创建View。通过前面的分析,我们可以得出结论,RN中的ScrollView并没有我们想象的那么高性能。4ListView这里提到ListView是因为RN中的ListView是基于ScrollView的,但是有一些优化。这里简单介绍一下ListView的一些原理。ListView其实是对ScrollView的封装,对应Native平台,和ScrollView的性能完全一样。但是ListView在显示列表内容的时候,会根据滑动的距离逐渐给ScrollView添加子组件(通过调用renderRow()方法)。注意ListView有一个initialListSize属性,表示第一次加载时添加多少个子项,默认为10,还有一个pageSize属性,表示每次需要添加时添加多少个子项,默认为1。通过上面的分析我们可以看到,ListView在第一次加载的时候,不管你的list有多大,默认都会加载到initialListSize个子项,所以启动速度是可以保证的。如果没有满,或者在下滑的过程中,再给组件添加子项。这样的操作看似合理,但注意在整个操作过程中,子项会逐渐添加到ListView中,新的子项会通过创建一个新的View来创建,根本没有任何复用过程。因此,如果在应用中ListView的子项数量特别多,内存会随着ListView的下滑而逐渐增加。值得一提的是ListView提供了renderScrollComponent,可以用其他Scroll组件替换,RecyclerViewBackedScrollView组件作为备选。看到这个名字我很高兴,意思是它支持子项(Recycler)的回收再利用。首先看到iOSRecyclerViewBackedScrollView.ios.js的实现,其实就是ScrollView,并没有实现所谓的复用。我失望了一半。继续看Android的实现,其实对应的是Native的com.facebook.react.views.recyclerview.AndroidRecyclerViewBackedScrollView,继承自Android的RecyclerView。看到这里,如果用这个方法,我直觉感觉RN的ListView在Android上的性能应该比在iOS上好。让我们继续看看它是如何实现回收再利用的。AndroidRecyclerViewBackedScrollView内部实现了一个RecyclerView.Adapter,如下:staticclassReactListAdapterextendsAdapter{privatefinalListmViews=newArrayList<>();公共voidaddView(Viewchild,intindex){mViews。添加(索引,子);...}publicvoidremoveViewAt(intindex){Viewchild=mViews.get(索引);如果(子!=null){mViews.remove(索引);...}}@OverridepublicConcreteViewHolderonCreateViewHolder(ViewGroupparent,intviewType){returnnewConcreteViewHolder(newRecyclableWrapperViewGroup(parent.getContext()));}@OverridepublicvoidonBindViewHolder(ConcreteViewHolderholder,intposition){RecyclableWrapperViewGroupvg=(RecyclableWrapperViewGroup)holder.itemView;Viewrow=mViews.get(position);if(row.getParent)=vg){vg.addView(row,0);}}@OverridepublicvoidonViewRecycled(ConcreteViewHolderholder){super.onViewRecycled(holder);((RecyclableWrapperViewGroup)holder.itemView).removeAllViews();}}注意这里有一个mViews,用来保存所有的子View,只有在绑定View的时候它只是用一个空视图(RecyclableWrapperViewGroup)包裹起来。这样RecyclerView就完全没有复用效果了!测试一下,确实一样,性能问题还是很严重。这里我们也可以得出一个结论:RN中的ListView并不是我们想象中的ListView应该有的表现。5改进计划通过前面的分析,我们已经知道了RN中ScrollView或者ListView的性能瓶颈,也有了改进的想法。下面根据各种情况进行分析:如果想优化首次加载速度,也就是启动速度:可以参考TAT文章中的方法。ListView会将所有的子View保存在内存中,因为我们不能删除子项,但是我们可以尽量减少每个子项占用的内存。比如这个项目,react-native-sglistview,在子item不可见的时候,退化为最基本的View;终极解决方案:要真正做到高性能,需要创建尽可能少的View,并想办法真正复用已经创建的子键。目前只有一些想法,等我想到了再更新。