requestAnimationFrame帧动画

前言

动画是移动应用中的一个相当重要的组成部分,一个用户体验良好的应用通常都具有流畅、有意义的动画。类似原生平台,React
Native也为我们提供了丰富的动画API:requestAnimationFrame、LayoutAnimation、Animated。

requestAnimationFrame:帧动画,是最容易实现的一种动画,通过不断改变组件的state值,从而在视觉上产生一种动画的效果,类似于gif动画的方式。

LayoutAnimation:布局动画,当布局发生改变时的动画模块,允许在全局范围内创建和更新动画,这些动画会在下一次渲染或布局周期运行,实现单个动画非常简洁,体验和性能良好。

Animated:用于创建更精细的交互控制的动画,可进行多个动画的组合动画,具备极高的性能,是功能最强大的动画API。

本节我们先介绍requestAnimationFrame。

requestAnimationFrame帧动画的实现

requestAnimationFrame实现帧动画的原理非常粗暴简洁,即通过修改state值来不断得改变视图上的样式,从而在视觉上产生一种动画的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import React, { Component } from'react';



import {



AppRegistry,



StyleSheet,



Text,



View,



TouchableOpacity,



Platform,



} from'react-native';



exportdefaultclassFrameAnimationDemo extends Component {



constructor(props){



super(props);



this.state = {



width:200,



height:20,



};



}



_onPress() {



//每按一次增加近30宽高



varcount =0;



while(++count<30){



requestAnimationFrame(()={



this.setState({



width:this.state.width +1,



height:this.state.height +1



});



})



}



}



render() {



return(



<Viewstyle={styles.container}>




<Viewstyle={[styles.content,{width:this.state.width,height:this.state.height}]}>



<Textstyle={[{textAlign:'center'}]}>Hello World!</Text>



</View>



<TouchableOpacitystyle={styles.content}onPress={this._onPress.bind(this)}>



<Viewstyle={styles.button}>



<Textstyle={styles.buttonText}Press me!></Text>



</View>



</TouchableOpacity>



</View>



);



}



}



const styles = StyleSheet.create({



container: {



marginTop:25,



flex: 1,



},



content: {



backgroundColor: 'rgba(200, 230, 255, 0.8)',



marginBottom:10,



justifyContent:"center",



alignSelf:"center",



},



button: Platform.select({



ios: {},



android: {



elevation: 4,



// Material design blue from https://material.google.com/style/color.html
#color-color-palette



backgroundColor: '#2196F3',



borderRadius: 2,



width:100,



height:30,



},



justifyContent:"center",



alignSelf:"center",



}),



buttonText: {



alignSelf:"center",



}



});

从效果上看动画有种一顿一顿的感觉。这是由于通过修改state值,导致频繁地销毁、重绘视图,内存开销大,从而使得动画卡顿明显。另外对于帧动画而言,如果帧数较少,动画的效果会比较生硬,帧数过多又会引发性能问题。

优化

如果帧动画的方式更符合当前对动画的控制方式,我们可以对上述方法做一点优化,在requestAnimationFrame中采用setNativeProps直接修改组件的属性并触发局部刷新,不会导致重绘组件,因此在性能上优于直接修改state的方法。

修改_onPress方法,将对this.setState的直接修改改为对”Hello
World”按钮的属性修改this.refs.view1.setNativeProps。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
_onPress() {



varcount =0;



while(++count<30){



requestAnimationFrame(()={



this.refs.view1.setNativeProps({



style: {



width:this.state.width++,



height:this.state.height++



}



});



});



}



}

this.refs.view1指向的是”Hello World”文字的父视图。

1
2
3
4
5
6
7
8
9
10
<View ref="view1"style={[styles.content, {width:this.state.width,
height:this.state.height}]}>



<Textstyle={[{textAlign:'center'}]}>Hello World!</Text>



</View>

通过对比可以看出流畅顺滑多了。

每个动画API都有其适应和不适应的场景,如果要实现“弹性动画”,“缓入缓出”等效果,使用requestAnimationFrame还是比较难的,需要辅助各种函数。下一节将介绍另一种动画API——LayoutAnimation。

GitHub地址

React-Native-WebStorm代码模版

在React-
Native日常开发中,新建文件或者组件是最常用的操作。可是,在我们新建不同的文件或者组件时,一些代码固定不变,此时,需要重新写一遍就费事费力了,而最常用的操作就是复制,粘贴,修改,重复而无趣。若是不想重复如此无聊的机械动作,又想快速高效的来完成任务,编写代码,那怎么办呢?此时,解决方案出现了。–File
and Code Templates

使用此配置,可减省重复无用的劳动力,真正提升效率。

如果你觉得有用,请点个赞,或者分享给其他朋友。

一:配置有两种方式(以mac配置为例):

一:

1-: 选择Preferences;

2-:搜索框输入Templates;

3-:点击File and Code Templates

4-:选中JavaScript File

5-:删除里面的代码,把以下代码复制进去(此代码可以自定义):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import React, { Component } from 'react';



import {



  StyleSheet,



  Text,



  View,



  Image,



} from 'react-native';



export default class  ${NAME}  extends Component {



  render() {



    return (



      <View style={styles.container}>








      </View>



    );



  }



}



const styles = StyleSheet.create({



  container: {



    flex: 1,



  },



});

6-:点击Apply

示例图如下:

二:

1-: 选择Preferences;

2-:搜索框输入Templates;

3-:点击File and Code Templates

4-:点击左上角“+”号按钮

示例图如下:

注意点:

1-:注意新建文件的名字,本例子中取名为:React-Native;

2-:注意Extension的输入框填写为js;

三:使用方式:

分为两种,与上面配置方式一一对应:

一:

1-:点击New ,选择Java Script File;

2-:输入新建文件的名字,并点击OK;

3-:查看新建文件内容为填写的模版内容

二:

1-:点击New ,选择React-Native文件类型(与使用第二中配置方式时新建的名字一一对应);

2-:输入新建文件的名字,并点击OK;

3-:查看新建文件内容为填写的模版内容

四:使用效果截图:

React-Native-bundle拆分与合并之安卓篇

在网上看到携程之前拆分的一些经验

先来说一组数据,一个Helloorld的App,如果使用0.30 RN 官方命令react-native
bundle打包出来的JSBundle文件大小大约为531KB,RN框架JavaScript本身占了530KB, zip压缩之后也有148KB。

如果只有一两个业务使用,这点大小算不了什么,但是对于我们这种动辄几十个业务的场景,如果每个业务的JSBundle都需要这么大的一个RN框架本身,那将是不可接受的。

因此,我们需要对RN官方的打包脚本做改造,将框架代码拆分出来,让所有业务使用一份框架代码。

开始拆分之前, 我们先以HelloWorld的RNApp为基础介绍几个背景知识。

上述是一个HelloWorld RNApp代码的结构,基本分为3部分

头部:各依赖模块引用部分;

中间:入口模块和各业务模块定义部分;

尾部:入口模块注册部分;

上述是HelloWorld RNApp打包之后JSBundle文件的结构,基本分为3部分
头部:全局定义,主要是define,require等全局模块的定义; 中间:模块定义,RN框架和业务的各个模块定义; 尾部:引擎初始化和入口函数执行;

d是RN自定义的define,符合CommonJS规范d后面的数字是模块的id,是在RN打包过程中,解析依赖关系,自增长生成的。

如果所有业务代码,都遵照一个规则:入口JS文件首先require的都是react/react-native, 则打包生成的JSBundle里面react
/react-native相关的模块id都是固定的。

拆分方案一

基于上面2点背景知识介绍,我们很容易发现,如果将打包之后的JSBundle文件,拆分成2部分(框架部分+业务模块部分),使用的时候合并起来,然后去加载,即可实现拆分功能。

具体实现步骤:

创建一个空工程,入口文件只需要2行代码,require react/react-native即可;

使用react-native bundle命令,打包该入口文件,生成common.js;

使用react-native bundle打包业务工程(有一点要保证,业务工程入口文件前面2行代码也是require react/react-
native), 生成business_all.js;

开发工具,从business_all.js里面删除common.js的内容,剩下的就是business.js;

App加载的时候将common.js和business.js合并在一起,然后加载;

貌似功能完成,可是回到Dive into React Native performance,
这么做还是优化不了JSBundle的执行时间,因为我们不能把拆分开的2个文件分别执行,因为加载common.js会提示找不到RNApp的入口,先执行business.js,会提示一堆依赖的RN模块找不到。

显然,这种拆分方式不能满足我们这种需要。

那这个方案就完全没有价值吗?不是的,如果你做的是一个纯RNApp,native只是一个壳,里面业务全是RN开发的,完全可以使用这种方式做拆分,这种方案简单,无侵入,实现成本低,不需要修改任何RN打包代码和RN
Runtime代码。

拆分方案二

RN框架部分文件(common.js)大小530KB,如此大的js文件,占用了绝大部分的JS执行时间,这块时间如果能放到后台预先做完,进入业务也只需执行业务页面的几个JS文件,将可以大大提升页面加载速度,参考上面的RN性能瓶颈图,预估可以提升100%。

按照这个思路,能后台加载的JS文件, 实际上是就是一个RNApp,因此
我们设计了一个空白页面的FakeApp,这个FakeApp做一件事情,就是监听要显示的真实的业务JS模块,收到监听之后,渲染业务模块,显示页面。

FakeApp设计如下:

为了实现该拆包方案,需要改造react-native的打包命令;

基于FakeApp打common.js包的时候, 需要记录RN各个模块名和模块id之间的mapping关系;

打业务模块包的时候,判断,如果已经在mapping文件里面的模块,不要打包到业务包中

改造页面加载流程:

因为要能够后台加载,所以需分离UI和JS加载引擎<iOS-RCTBridge, Android-ReactInstanceManager;

进入业务RN页面时候,获取预加载好的JS引擎,然后发送消息给FakeApp,告知该渲染的业务JS模块;

通过后台预加载,省去了绝大部分的JS加载时间,似乎问题已经完美解决。

但是,如果随着业务不断膨胀,一个RN业务JS代码也达到500KB,进入这个业务页面,500多KB JS文件读取出来,执行,整个JS执行的时间瓶颈会再次出现。

拆分方案三

正在此时,我们研究RN在Facebook App里面的使用情况,发现了Unbundle,简单点说,就是将所有的JS模块都拆分成独立的文件。

下面截图就是unbundle打包的文件格式:

entry.js就是global部分定义+RNApp入口;

UNBUNDLE文件是用于标识这是一个unbundle包的flag;

12.js,13.js就是各个模块,文件名就是模块id;

在业务执行,需要加载模块(require)的时候,就去磁盘查找该文件,读取、执行。

RN里面加载模块流程说明,以require(66666)模块为例:

首先从__d<就是前文提到的define的缓存列表里面查找是否有定义过模块66666,如果有,直接返回,如果没有走到下面第二步的nativeRequire;

nativeRequire根据模块id,查找文件所在路径,读取文件内容;

定义模块,_d(66666)=eval(JS文件内容),会将这个模块ID和JS代码执行结果记录在define的缓存列表里面;

打包通过react-native unbundle 命令,可以给android平台打出这样的unbundle包。

顺便提一下,这个unbundle方案,只在android上有效,打ios平台的unbundle包,是打不出来的,在RN的打包脚本上有一行注释,大致意思是在iOS上众多小文件读取,文件IO效率不够高,android上没这样的问题,然后判断如果是打iOS的unbundle包的时候,直接return了。

相对应的,iOS开发了一个prepack的打包模式,简单点说,就是把所有的JS模块打包到一个文件里面,打包成一个二进制文件,并固定0xFB0BD1E5为文件开始
,这个二进制文件里面有个meta-
table,记录各个模块在文件中的相对位置,在加载模块(require)的时候,通过fseek,找到相应的文件开始,读取,执行。

在Unbundle的启发下,我们修改打包工具,开发了CRNUnbunle,做了简单的优化,把众多零散的JS文件做了简单的合并。

将common部分的JS文件,合并成一个common_ios(android).js.

_crn_config记录了这个RNApp的入口模块ID以及其他配置信息,详见下图:

main_module为当前业务模块入口模块ID;

module_path为业务模块JS文件所在当前包的相对路径;

666666=0.js,说明666666这个模块在0.js文件里面;

做完这个拆包和加载优化之后,我们用自己的几个业务做了下测试,下图是当时的测试验证数据。

可以看出,iOS和android基本都比官方打包方式的加载时间,减少了50%。

这是自己单机测试的数据,那上线之后,数据如何呢?

下图,是我们分析一天的数据,得出的平均值<排除掉了5s以上的异常数据,后面实测下来5s以上数据极少;

看到这个数据,发现和我们自己测试的基本一致,但是还有一个疑问,加载的时间分布,是否服从正态分布,会不会很离散,快的设备很快,慢的设备很慢呢?

然后我又进一步分析这一天的数据,按照页面加载时间区间分布统计。

看图上数据,很明显,iOS&Android基本一致,将近98%的用户都能在1s内加载完成页面,符合我们期望的正态分布,所以bundle拆分到此基本完成。

实践

我先用bundle打包命令打一个bundle出来

react-nativebundle –platform android –devfalse–entry-file index.android.js
–bundle-output finalbundle/index.android.bundle –assets-dest finalbundle/

只有一个简单的3k左右的index.android.js,打出了一个五百多k的index.android.bundle,看看里面是些什么

密密麻麻但又有规则

!function打头的是公共的头部部分

_d(function是JS文件,用ctrl+s搜索welcome,找到我们的index.android.js,原来是在第一行的_d(function,而且结尾有个参数0,其余部分其实都是公共的js

;require(120),是基础文件的配置入口,require(0)则是业务的入口

基于以上,能想到一个办法:

内置一个common.js文件,里面包含了bundle文件公共部分的代码,

业务代码单独生成一个js文件

在需要展示加载某一个页面的时,将common.js和当前页面需要加载的业务js合并,然后再加载

这个办法解决了一部分问题,但加载时还是一个整体。如果common部分能重用,就能大大提升效率。所以就来试试上面提到的unbundle命令

react-nativeunbundle –platform android –devfalse–entry-file
index.android.js –bundle-output build/index.android.bundle

生成的bundle只有14行了

但多了一个js-
modules文件夹,里面的xx.js里面的内容就是将之前的__d(xx)抽出来单独放到一个文件里面,通过require(xx)加载到内存供调用

基于unbundle命令再设计一个上面提到的fake页面用来加载相应的业务模块,这个页面可以预先在后台初始化js引擎,将公共部分的common.js文件读取到内存,然后设置一个监听事件,通过emmit方式,当需要加载某个页面的的module的时候讲这个页面的module的id传递过来,然后通过require方法调用这个模块。

思路差不多是这样了,来试试看实现起来有没什么坑。

首先

我拿例子跑了一下,瞬间明白了流程是怎么回事,有几个关键:

DeviceEventEmitter

前端发起监听,后端需要用的时候调用emit触发,通过返回模块id,然后return
React.createElement(返回的模块ID,this.props)即可定制加载

配置文件

这个配置文件之前不是很理解为什么好多等于0.js、等于1.js,现在明白其实就是不同bu的入口JS,因为都是单页路由的形式,不过这个配置其实是一套打包的一个流程,不在这里做,以后研究打包工具的时候加上。

然后

我试着把这样融入到之前的demo里。

先建两个test页面,用于测试切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
importReact, { Component }from'react';



import{



AppRegistry,



StyleSheet,



Text,



View,



}from'react-native';



classtesteightextendsComponent{



render() {



return(



<Viewstyle={styles.container}



<Textstyle={styles.welcome}



Welcome to Test 8888



</Text



</View



);



}



}



conststyles = StyleSheet.create({



welcome: {



fontSize:20,



textAlign:'center',



margin:10,



}



});



module.exports = testeight;

然后在index.android.js加入切换按钮

/**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397




* Sample React Native App



* https://github.com/facebook/react-native



* @flow



*/



importReact, { Component }from'react';



import{



AppRegistry,



StyleSheet,



Text,



View,



Image,



NativeModules,



DeviceEventEmitter,



}from'react-native';



exportdefaultclassAwesomeProjectextendsComponent{



constructor(props){



super(props);



this.state = {



content:null,showModule:false



};



DeviceEventEmitter.addListener("test", (result) = {



letmainComponent =require(result.name);



this.setState({



content:mainComponent,



showModule:true



})



});



}



render() {



let_content =null;



if(this.state.content){



_content = React.createElement(this.state.content,this.props);



return_content;



}else{



return(



<Viewstyle={styles.container}>



<Textstyle={styles.welcome}>



Welcome to React Native!



</Text>



<Textstyle={styles.instructions}>



To get started, edit index.android.js



</Text>



<Textstyle={styles.instructions}>



Double tap R on your keyboard to reload,{'\n'}



Shake or press menu button for dev menu



</Text>



<Textstyle={styles.instructions}onPress={()=this.showToast()}>



点我调用原生



</Text>



<Textstyle={styles.instructions}onPress={()=this.updateBundle()}>



点我更新bundle



</Text>



<Textstyle={styles.instructions}onPress={()=this.goNine()}>



点我加载页面9999



</Text>



<Textstyle={styles.instructions}onPress={()=this.goEight()}>



点我加载页面8888



</Text>



<Image>



source={require('./img/music_play.png')}



style={{width:92,height:92}}



/



</View>



);



}



}



updateBundle () {



NativeModules.updateBundle.check("5.0.0");



}



showToast () {



//调用原生



NativeModules.RNToastAndroid.show('from native',100);



}



goNine () {



NativeModules.BundleLoad.goPage(9999);



}



goEight () {



NativeModules.BundleLoad.goPage(8888);



}



}



const styles = StyleSheet.create({



container: {



flex: 1,



justifyContent: 'center',



alignItems: 'center',



backgroundColor: '#F5FCFF',



},



welcome: {



fontSize: 20,



textAlign: 'center',



margin: 10,



},



instructions: {



textAlign: 'center',



color: '#333333',



marginBottom: 5,



},



});



AppRegistry.registerComponent('rnandnative', () = AwesomeProject);

然后把index.android和两个test页面都用unbundle打包

1
2
3
4
5
6
7
8
9
10
11
12
react-nativeunbundle --platform android --devfalse--entry-file
index.android.js --bundle-output unbundle/index.android.bundle



react-nativeunbundle --platform android --devfalse--entry-file
bundletest1.js --bundle-output unbundle/index.android.bundle1



react-nativeunbundle --platform android --devfalse--entry-file
bundletest2.js --bundle-output unbundle/index.android.bundle2

然后把index.android.bundle1、index.android.bundle2中除了_d的那句打头的去掉,把__d(0的0改为9999、8888,把文件名改为9999.js和8888
.js丢到js-modules里,这个讲的估计不是很明白,但去看看代码就懂了。

然后建一个触发emit的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class RNBundleLoadModule extends ReactContextBaseJavaModule{



private ReactApplicationContext reactApplicationContext;



public RNBundleLoadModule(ReactApplicationContext reactApplicationContext){



super(reactApplicationContext);



}



@Override



public String  getName(){



return "BundleLoad";



}



@ReactMethod



public void goPage(final Integer pageid){



System.out.print("########"+pageid+"########");



// failedCallback.invoke();



WritableMap params = Arguments.createMap();



params.putInt("name", pageid);



reactApplicationContext



.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)



.emit("test", params);



}



}

跑起来,一切OK。

参考

https://github.com/pukaicom/reactNativeBundleBreak

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×