起因§
某晚餐后和几位前端同事走在路上,聊起了跨端方案相关的话题,每到这个话题永远无法避开的就是RN与Flutter;这几年原生开发者锐减,加之跨端方案越来越成熟,越来越多的的公司选择使用跨端方案来开发他们的产品,无论是从维护还是业务实现,都非常符合这个时代背景下的开发工作。
我们应该选择什么方案去立项开发,这个问题放在前几年可能会有很多答案,不过从目前来看,原生跨端方案可选择性仅剩两种主流,也就是常听到的React Native与Flutter。
这两套方案的优劣已经有很多点评,基本上形成了对立的两个阵营。在我看来它们的实际表现应该相差不大,能够在多年市场选择下脱颖而出必然有一定的实力。我2017年读大学,那年接触了原生开发,后面听说了Flutter,当时的略显青涩的我从其语法结构,编写体验、以及列表性能表现上上给它打了负分;现在看来当时的Google就有着相当大的野心,想统一移动开发乃至桌面客户端开发,从社区和开发者入手统一语言甚至操作系统(Fuchsia),从而包揽整个客户端领域。FaceBook则是想通过多年前端沉淀,通过React切入所有视觉化平台。
架构§
设计理念§
在客户端开发上,有前辈总结了一个很精辟的观点,客户端开发无非关注三件事,“数据获取”、“状态管理”、“页面渲染”;而在跨平台的领域竞争,我的理解是“虚拟机”、“引擎”、“原生通信”、“开发体验”的竞争;在这几点上无论是Flutter还是React Native都有着自己独有的解决方案。
从Flutter来看,在虚拟机上使用了Dart VM,Dart支持JIT和AOT两种变异模式;在开发阶段使用JIT编译,实时更新预览、动态加载,在发布阶段使用AOT编译为机器码,保证启动速度与数据传递的效率;渲染引擎上使用Skia引擎进行时图绘制,避开了不同平台上控件渲染差异;而且,少了这一层交互,效率也得到了一定提升。在原生交互上,因为Dart其本身跨平台的特性,底层的C++可以直接访问原生API,加上信息使用机器码进行传递(BinaryMessage),所以与原生交互效率相当的可观。
在RN中,早期架构上虚拟机使用的是JSC(JavaScript Core)进行运算,充分复用JS生态,吸引大量前端开发者参与;且由于JS跨平台特性,后续的跨端移植App也顺理成章。在渲染引擎上RN没有使用WebKit一类的Web引擎,而是通过原生进行渲染,直接复用了原生的渲染通道,带来了与原生近乎一致的使用体验。但是带来的问题就是在JSC到原生渲染这一层,使用了非常多的Bridge,并通过JSON序列化在多个线程中来回传递信息,这样的消耗在简单的交互逻辑中可能并不明显,但是在内容丰富的页面交互和渲染上会有明显的卡顿,这也是我见过最多被吐槽的点;不过在新的架构中,RN增加了一个名叫JSI的新概念来解决这些痛点,后面会提到。
似乎听起来Flutter近乎是跨端开发中完美的存在,但事实并非如此,因为Flutter是一个全新的产物,没有成熟的生态,很多的问题都需要社区提供足够多的帮助才能够解决;另一点,在互联网业务中,大部分的业务对动态性有一定依赖,Dart发布阶段使用静态编译,虽然效率得到了提升,但是同时也丧失了动态发布的能力,当然现在的市场环境下已经出现了无数Flutter的二开方案,通过损失一定性能的方案来达到动态性的目的。
核心架构§
Flutter§
Flutter 的架构分为了三层,我们大多情况只与 Flutter Framework 层交互,更多平台无关的的底层能力已被封装好。这也使得 Flutter Framework 非常的轻,如果你需要更多的原生能力,通常使用各类 Flutter Plugin 比如 Camera。这也就意味着,原生能力基本上都依赖于官方和社区的产出速度,其次就是业务开发者的适配了。
React Native§
React Native经历了一次大的架构调整,增加了一层JSI,能够更直接地与底层进行通信。
老架构§
三个线程各自负责运算,渲染,Native 交互,中间的交互使用 Bridge 与 JSON 信息格式进行传递。
新架构§
JS Bundle 不再依赖于 JSC(Javascript Core)。换句话说,它可以编译和应用在任何 JS 引擎 (V8等)。 引入 JSI 标准,基于 JSI 协议实现各自方法,使得 JS 可以直接引用 C++ 对象,反之亦然。与原生之间的交互不再用 Bridge 去做粘合。
RN Bridge 上的变化§
可以看到 Bridge 非常的重
将原先较重的 Bridge 分拆成两个模块,Farbric 处理 UI,TurboModules 处理与原生交互
核心流程§
数据获取§
网络请求§
| Flutter | React Native |
|---|---|
| http.dart 库C++ 实现 | 复用现有的 JS 库fetch, XMLHttpRequest, Axios |
状态管理§
Flutter§
正如 Flutter 将所有控件都定义为了 Widget 一样,它也分成了两种 Widget,一种是 Stateful, 另一种是 Stateless。
Stateless§
Stateless 是无状态的,不能通过 state 状态去更新控件
class MyScaffold extends StatelessWidget {
const MyScaffold({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
...
}
}
Stateful§
Stateful 是有状态的,可以通过 state 变化去更新控件
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({Key? key}) : super(key: key);
// 覆盖这个 createState 方法,实现状态管理
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}
// 状态设置通常写在私有方法这里
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
// ···
@override
Widget build(BuildContext context) {
...页面构建
}
void _toggleFavorite() {
// 用 setState 去进行状态的变更,以触发 Widget 的重新渲染
setState(() {
...
_isFavorited = false;
...
});
}
}
对于需要向上传递信息时,使用 InheritedWidget, Provider, FlutterHook 方式。
React Native§
复用了 React 里的 State 模式,同时也支持现在流行的 Hook 方式使用 state,和 React 方式近乎类似。
// React Native Counter Example using Hooks!
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
const App = () => {
const [count, setCount] = useState(0);
return (
<View style={styles.container}>
<Text>You clicked {count} times</Text>
<Button
onPress={() => setCount(count + 1)}
title="Click me!"
/>
</View>
);
};
// React Native Styles
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
页面渲染§
相同点§
在渲染方式上,理论基本一样,先是构建一颗平台无关性的虚拟树 (Virtual Dom Tree),然后通过各自不同的实现自已渲染或交给原生进行渲染。虚拟树的好处可以实现 UI 节点的局部更新,而不会全量刷新,具有平台无关性。
- 两个框架都是 UI 响应式框架(React Framework)
UI = f(state)
UI 仅依赖于它的父类与自身的状态
Flutter 在设计之初,也借鉴了很多 React 的设计思想。
- 所有组件都可被组合成一颗虚拟树虚拟树 (VDom),在真正渲染前各个框架会把它们转化为各自的渲染对象 (RenderObj / VDom)。
不同点§
| Flutter | React Native |
|---|---|
| UI 组件称为 Widget,Flutter 将所有可能的控件都封装为 Widget | RN 没有将所有控件封装,而是将样式与 Component 分离,进行自由组合 |
| 编译产物为二进制文件,不具备热更能力 | 编译产物为js文件,具有热更新下发能力 |
最终导致的书写体验就是,你不会在RN工程中看到“嵌套地狱”
绘制§
Flutter§
如前所说,Flutter 在更新完 UI Tree 后直接通过 GPU 渲染
React Native§
和 React Render 很类似,先是更新 VDom ,然后再更新真正的组件,只是 RN 是 Native 组件
性能§
渲染性能§
在大多数浏览器和手机设备上都是 60HZ 刷新频率,也就我们只能在每帧 16ms 的时间内处理完所有事情,包括渲染才能保证显示的平滑。
React Native§
在渲染效率上,官方其实也提到了,我们的大部分业务逻辑和事件处理都是在 JS 线程上的,因为架构的原因,在 JS 线程处理完数据之后,要扔给 UI 线程进行 Native 原生控件渲染,如果这个时间等于 200ms 就会丢掉 12 帧。而出现卡顿。如果任何情况下超过 100ms 就会被用户所感知。这种情况通常发生在新进一个页面时,要计算所有控件和布局进行渲染。
Flutter§
其实 Flutter 因为少了原生控件的转化,少了一步桥接上的时间消耗。但要注意的问题仍一样,业务逻辑的处理耗时,和 UI Tree 层级。
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
}
]