移动端ReactNative性能探究

3,435 阅读9分钟

原创:libo

关于帧

在最近接触了rn之后,也算有了一点了解,今天写的文章就来简单探讨一下关于rn性能的二三事。

首先在讨论移动端的性能之前,都必须要了解帧的概念,帧率是评判移动端应用性能的一个极为重要的标准。众所周知,不管是手机还是电脑或是视频,都是由一组静态的图片以一个稳定的速度快速变化所产生的。我们把这组图片中的每一张图片叫做一帧,而每秒钟显示的帧数直接的影响了用户界面的流畅度和真实感。用iOS举例,iOS设备提供了每秒60的帧率,这就留给了开发者和UI系统大约16.67ms来完成生成一张静态图片(帧)所需要的所有工作。如果在这分派的16.67ms之内没有能够完成这些工作,就会引发丢帧,使界面表现的不够流畅。但通常来说,保持一个应用在使用过程中维持每秒60帧的刷新率是一件比较困难的事情,不管是原生代码还是rn,都无法避免额外的不必要的性能开销,因此人工的干预是必要的。

在讨论rn性能优化的方式之前,我们先讨论一下rn的性能问题会出现在哪里吧。

关于RN的原理

首先RN为我们在移动端提供了JS的运行环境,所以前端开发者们只需要关心如何编写JS代码,画UI只需要画到virtual DOM 中,不需要特别关心具体的平台。

而同样是运行js,RN与h5页面有本质的区别,使得rn能够具备类似原生App的外观和手感,在体验上远远超过h5。RN底层干了很多很多脏活累活,可以把我们写的JS代码转化成了native代码执行,也就是说RN页面实际上渲染出来的都是原生组件,不再是webView里的东西了。

RN的本质是把中间的这个桥Bridge给搭好,让JS和native可以互相调用。 

企业咚咚20191014171601.png

关于线程

在原生iOS应用中,和js单线程不同的一点是,原生应用可以把网络请求或是一些与ui渲染无关的操作放到子线程异步执行,以此增加主线程渲染的流畅性,让主线程尽量只关注于视图渲染。这是原生应用性能优化的基本概念。

那么在rn应用中,rn又提供了一个特殊的线程即js线程,专门用来执行前端代码。所以rn应用中通常会存在如下几种线程:

  1. 主线程:UI线程 ,视图渲染,处理用户交互
  2. 子线程: 处理网络请求,进行耗时操作等
  3. js线程:运行js代码 ,对大多数React Native应用来说,业务逻辑是运行在JavaScript线程上的。

如果只进行js的编写,那么开发过程中也是接触不到上面两种线程的。但我们需要了解的是:在rn应用运行过程中,js线程和主线程是在高度协同下工作的,例如,在用户触发了UI事件响应时,会在主线程接受事件,在传递到js代码,在js线程处理该事件;在js端发起UI更新时,会同时向native端同步数据和UI结构,在主线程完成更新渲染。

更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果JavaScript线程有一帧没有及时响应,或者主线程还在忙于上一帧的渲染工作,就被认为发生了一次丢帧。 例如,如果在一个复杂应用的根组件上调用了this.setState,从而导致一次开销很大的子组件树的重绘,那么,这可能会花费一个较长的时间,比如200ms也就是整整12帧的丢失。此时,任何由JavaScript控制的动画都会卡住。只要卡顿超过100ms,用户就会明显的感觉到。

这种情况经常发生在Navigator的切换过程中:当我们push一个新的路由时,JavaScript需要绘制新场景所需的所有组件,以发送正确的命令给原生端去创建视图。由于切换是由JavaScript线程所控制,因此经常会占用若干帧的时间,引起一些卡顿。有的时候,组件会在componentDidMount函数中做一些额外的事情,这甚至可能会导致页面切换过程中多达一秒的卡顿。

另一个例子是触摸事件的响应:如果你在按钮的响应方法里处理一个跨越多个帧的工作,就可能导致按钮本身的点击动画被延迟了(颜色或是透明度的改变)。这是因为JavaScript线程太忙了,不能够处理主线程发送过来的原始触摸事件。结果就不能及时响应这些事件并命令主线程的页面去调整颜色或是透明度了。

##关于性能瓶颈 通过上面的几个例子我们不难看出一个问题,导致性能受影响的原因是在重绘上。原生应用的Navigator就从不需要注意卡顿问题。那么是什么导致rn Navigator的切换如此缓慢?

首先,native代码在设备上的运行速度毋容置疑,而JS作为脚本语言,本来就是以快著称,也就是说两边的独立运行都很快,如此看来,性能瓶颈只会出现在两端的通信上,但两边其实不是直接通信的,而是通过Bridge做中间人,查找、调用模块、接口等操作逻辑,会产生到能让UI层明显可感知的卡顿,也就是说Bridge在绘制过程中的转换过程是比较低效的。 这一点我们可以从一个例子中看出,当我们上下滚动ScrollView的时候,不论JavaScript线程繁忙到什么地步,甚至于JavaScript线程卡住,ScrollView的滑动流畅度都不会受到影响,因为ScrollView运行和滑动动画是完全在主线程之上进行的。

然而ScrollView的滚动事件会被分发到JS线程,如果我们接收每一次滚动事件并在每一次滚动里触发一次ui更新,那应用的使用效果简直是灾难性的。每一次ui更新不仅意味着js的重绘,还会触发桥接的代码装换,和主线程接受到正确的指令后的UI视图的生成和渲染。 那么很显然,性能控制就变成了如何尽量减少Bridge的逻辑。 应用在以下3种情况会需要bridge工作。

  1. UI事件响应:这块内容都发生在Native端,以事件形式传递到JS端,只是一个触发器,不会有过度性能问题
  2. UI更新:JS是决定显示什么界面,如何样式化页面的,一般都是由JS端发起UI更新,同时向native端同步大量的数据和UI结构,这类更新会经常出现性能问题,特别是界面复杂、数据变动量大、动画复杂、变动频率高的情况。
  3. UI事件响应+UI更新:如果UI更新改动不大,那么问题不大。如果UI事件触发了UI更新,同时逻辑复杂、耗时比较长,JS端和Native端的数据同步可能会出现时间差,由此会引发性能问题。

关于优化的方式

这里大致介绍一些比较常用的函数和方法。

1.使用shouldComponentUpdate和pureComponent:

react应用中的state和props的改变都会引起re-render,shouldComponentUpdate这个函数是render()函数调用前被调用的,他的两个参数nextProps和nextState,分别表示下一个props和下一个state的值。我们重写这个钩子,当函数返回false时,阻止接下来的render()调用以及组件重新渲染,反之,返回true时,组件向下走render重新渲染。

例如这样写:

shouldComponentUpdate(nextProps, nextState) {
	return nextState.b !== this.state.b
}

可以使得当前后state或者props值不一致的时候,我们才会去进行渲染,从而达到了优化的效果。

或是使用pureComponent,自定义的有状态组件可以尽量继承自pure component,而不再是component。在阅读了官方介绍后,可以发现说到底它本身只是会自动使用shouldComponentUpdate钩子的普通Component。如果组件继承自pureComponent,就无需再写shouldComponentUpdate的函数了。

2.InteractionManager: Interactionmanager 本质上一个延迟计划函数,它可以自动将一些耗时较长的工作安排到所有互动或动画完成之后再进行。这样可以保证 JavaScript 动画的流畅运行。 应用这样可以安排一个任务在交互和动画完成之后执行:

InteractionManager.runAfterInteractions(() => {
	 // ...耗时较长的同步执行的任务...
});

3.setNativeProps: setNativeProps方法可以理解为web的直接修改dom。使用该方法修改View、Text等RN自带的组件,就不会触发组件的componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate等组件生命周期中的方法。

这样的确会带来一定的性能提升,同时也会使代码逻辑难以理清,而且并没有解决从JS侧到Native侧的数据同步开销问题。因此这个方式官方都不再推荐,更推荐的做法是合理使用setState()和shouldComponentUpdate()方法解决这类问题。

4.LayoutAnimation: 实现动画时一般会采用Animated的接口,但Animated的接口一般会在JavaScript线程中计算出所需要的每一个关键帧,而LayoutAnimation则利用了原生的动画库,Core Animation,LayoutAnimation会一次性将想要实现的动画的参数传递给native,再由native完成动画,动画过程js线程就不再参与了。 但是LayoutAnimation只工作在一次性的动画上,例如如果动画可能会被中途取消,这种情况还是需要使用Animated。

5.少用状态组件,尽可能用无状态组件,无状态组件在转换成为native视图时是不会被实例化的,可以提升性能,但使用的时候也要注意,我们不能通过ref来获取对象了。