React Native 实现模式

Bridge 通信

Bridge(c++ 层)

JS Bridge 是封装 JSCore 作为中间适配层桥接,实现 js 层和 native 层双端通信。React Native 就运行在 JSCore 上,也不会存在 ES6 兼容性之类的问题。

在原生端提供的 Native Module 模块(如网络请求,ViewGroup 控件),和 JS 端提供的 JS Module,都会在 C++实现的 so 中保存起来,双方的通讯通过 C++ 中的保存的映射,最终实现两端的交互。通信的数据和指令,在中间层会被转为 String 字符串传输。

JS 层

在 RN 代码里写的前端组件都是 Virtual DOM 机制,比对树得到最小改动,然后交给 Native 去解析

Java / Objective-C

作为逻辑入口,启动 c++ 层的 JSCore,执行 js 通过 c++ 传递来的渲染指令,从而构建 NativeUI

最终的 Bundle

JS 会被打包压缩成 bundle 文件,添加到 App 的资源目录下


React Native 在优化的路上

RN 的优化大致是在两个方向:首屏渲染优化 + UI 更新优化

JSBundle 体积

RN 中最终 pack 出来的 JS Bundle 文件不仅仅包含了业务的页面 JS 逻辑,还包含了 RN 组件和其框架的 JS

生成 JSBundle

1
react-native bundle --entry-file index.js --bundle-output  index.ios.bundle --platform ios
1
2
3
4
5
6
7
8
var __DEV__ = true,
__BUNDLE_START_TIME__ = this.nativePerformanceNow
? nativePerformanceNow()
: Date.now(),
process = this.process || {};
process.env = process.env || {};
process.env.NODE_ENV = 'development';
...

缩小 Bundle 体积

可以参考这边RN 打包那些事儿


Lazy Require

这个点其实在前端优化性能中也比较常见(著名的雅虎 36 条军规也有说明)

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
import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';

let VeryExpensive = null;

export default class Optimized extends Component {
state = { needsExpensive: false };

didPress = () => {
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}

this.setState(() => ({
needsExpensive: true
}));
};

render() {
return (
<View style={{ marginTop: 20 }}>
<TouchableOpacity onPress={this.didPress}>
<Text>Load</Text>
</TouchableOpacity>
{this.state.needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
}

渲染更新方面

  • shouldComponentUpdate

  • PureComponent

    改变了 SCU,自动检查组件是否需要重新渲染

    公共的问题就是只会做浅比对,如果是引用对象的话,考虑上 immutable 吧 233…


从 ListView 无法支撑数据量大的列表到 FlatList

在 FlatList 之前,社区中有种种思路封装成高性能的 ListView 的方案

可以参考 FlatList 的源码从源码中可见是包装了 VirtualizedList,继承于 ScrollView

getItemLayout 的优化

如果预先知道列表中的每一项的高度(ITEM_HEIGHT)和其在父组件中的偏移量(offset)和位置(index),就能减少一次渲染。如果不做 getItemLayout 的优化,每个列表都需要事先渲染一次,动态地取得其渲染尺寸,然后再真正地渲染到页面中。

1
2
3
getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}

VirtualizedList 中的优化

VirtualizedList 的源码

FlatList 之所以节约内存、渲染快,是因为它只将用户看到的(和即将看到的)部分真正渲染出来了。而用户看不到的地方,渲染的只是空白元素。渲染空白元素相比渲染真正的列表元素需要内存和计算量会大大减少,这就是性能好的原因。

FlatList 将页面分为 4 部分。初始化部分/上方空白部分/展现部分/下方空白部分。初始化部分,在每次都会渲染;当用户滚动时,根据需求动态的调整(上下)空白部分的高度,并将视窗中的列表元素正确渲染出来。

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
if (itemCount > 0) {
_usedIndexForKey = false;
_keylessItemComponentName = '';
const spacerKey = !horizontal ? 'height' : 'width';
const lastInitialIndex = this.props.initialScrollIndex
? -1
: this.props.initialNumToRender - 1;
const { first, last } = this.state;
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
0,
lastInitialIndex,
inversionStyle
);
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
// first 就是 在视图中(包括要即将在视图)的第一个 item
if (!isVirtualizationDisabled && first > lastInitialIndex + 1) {
let insertedStickySpacer = false;
if (stickyIndicesFromProps.size > 0) {
const stickyOffset = ListHeaderComponent ? 1 : 0;
// See if there are any sticky headers in the virtualized space that we need to render.
for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) {
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const stickyBlock = this._getFrameMetricsApprox(ii);
const leadSpace =
stickyBlock.offset -
initBlock.offset -
(this.props.initialScrollIndex ? 0 : initBlock.length);
cells.push(
<View key='$sticky_lead' style={{ [spacerKey]: leadSpace }} />
);
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
ii,
ii,
inversionStyle
);
const trailSpace =
this._getFrameMetricsApprox(first).offset -
(stickyBlock.offset + stickyBlock.length);
// 从第 11 个 items (除去初始化的 10个 items) 到 first 渲染空白元素
cells.push(
<View key='$sticky_trail' style={{ [spacerKey]: trailSpace }} />
);
insertedStickySpacer = true;
break;
}
}
}
if (!insertedStickySpacer) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace =
this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
cells.push(
<View key='$lead_spacer' style={{ [spacerKey]: firstSpace }} />
);
}
}
// last 是最后一个在视图(包括要即将在视图)中的元素。
// 从 first 到 last ,即用户看到的界面渲染真正的 item
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
firstAfterInitial,
last,
inversionStyle
);
if (!this._hasWarned.keys && _usedIndexForKey) {
console.warn(
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
'item or provide a custom keyExtractor.',
_keylessItemComponentName
);
this._hasWarned.keys = true;
}
if (!isVirtualizationDisabled && last < itemCount - 1) {
const lastFrame = this._getFrameMetricsApprox(last);
const end = this.props.getItemLayout
? itemCount - 1
: Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
const endFrame = this._getFrameMetricsApprox(end);
const tailSpacerLength =
endFrame.offset + endFrame.length - (lastFrame.offset + lastFrame.length);
// last 之后的元素,渲染空白
cells.push(
<View key='$tail_spacer' style={{ [spacerKey]: tailSpacerLength }} />
);
}
}

既然要使用空白元素去代替实际的列表元素,就需要预先知道实际展现元素的高度(或宽度)和相对位置。如果不知道,就需要先渲染出实际展现元素,在获取完展现元素的高度和相对位置后,再用相同(累计)高度空白元素去代替实际的列表元素。_onCellLayout 就是用于动态计算元素高度的方法,如果事先知道元素的高度和位置,就可以使用上面提到的 getItemLayout 方法,就能跳过 _onCellLayout 这一步,获得更好的性能。

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
_onCellLayout(e, cellKey, index) {
const layout = e.nativeEvent.layout;
// 计算相关尺寸
const next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: true,
};
const curr = this._frames[cellKey];
if (
!curr ||
next.offset !== curr.offset ||
next.length !== curr.length ||
index !== curr.index
) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += curr ? 0 : 1;
this._averageCellLength =
this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredFrameIndex = Math.max(
this._highestMeasuredFrameIndex,
index,
);
this._scheduleCellsToRenderUpdate();
} else {
this._frames[cellKey].inLayout = true;
}

FlatList 优化小结

基于 ScrollView 的优化,至于为什么不直接使用 IOS/Android 的列表组件

In UITableView, when an element comes on screen, you have to synchronously render it. This means that you’ve got less than 16ms to do it. If you don’t, then you drop one or multiple frames.

UITableView 的渲染要求是视窗中的元素需要同步渲染,超过 16ms 会掉帧

The problem is in the RN render -> shadow -> yoga -> native loop. You have at least three runloop jumps (dispatch_async(dispatch_get_main_queue(), …) as well as background thread work, which all work against the required goal.

RN render 的过程到调用 native 组件,不可以保证在 16ms 内

We are actually starting to experiment more and more with synchronous method calls for other modules, which would allow us to build a native list component that could call renderItem on demand and choose whether to make the call synchronously on the UI thread if it’s hi-pri (including the JS, react, and yoga/layout calcs), or on a background thread if it’s a low-pri pre-render further off-screen. This native list component might also be able to do proper recycling and other optimizations.

终极方案大概就是在高性能场景下可以选择去直接使用 native 组件


总结

很多的优化方式,不仅是 Native,在做 Web 应用/ React 程序时是共通的地方。

官方文档中的 Performance 章节还有很多地方可以去继续看看,比如用 dev 环境和 release 的时候去除 console 这些在 Web 应用中也是需要的,更多的优化方式可以去看看雅虎优化军规。

首屏优化的话,今天腾讯二面面试官也问了这个问题,大概去考虑直出渲染这种。JSBundle 去分割,可以看看上面的链接,对理解机制还是比较有用的。FlatList 这边看了挺久,还是蛮给力的喔。


参考