临冬之际,移动端跨平台在经历数年沉浮之后,如今还能在舞台聚光灯下雀跃的, 也只剩下 React Native 和 Flutter 了,作为沉淀了数年的 “豪门” 与 19 年当红的 “新贵” ,它们之间的 “针锋相对” 也成了开发者们关心的事情。
过去曾有人问我:“他即写 Java 又会 Object-C ,在 Android 和 IOS 平台上可以同时开发,为什么还要学跨平台呢?”
而我的回答是:跨平台的市场优势不在于性能或学习成本,甚至平台适配会更耗费时间,但是它最终能让代码逻辑(特别是业务逻辑),无缝的复用在各个平台上,降低了重复代码的维护成本,保证了各平台间的统一性, 如果这时候还能保证一定的性能,那就更完美了。
无论是 React Native 还是 Flutter ,都需要 Android 和 IOS 的开发环境,也就是 JDK 、Android SDK、Xcode 等环境配置,而不同点在于 :
从配置环境上看, Flutter 的环境搭配相对简单,而 React Native 的环境配置相对复杂,而且由于 node_module 的“黑洞”属性和依赖复杂度等原因,目前在个人接触的例子中,首次配置运行成功率 Flutter 是高于 React Native 的,且 Flutter 失败的原因则大多归咎于网络。
同时跨平台开发首选 Mac ,没有为什么。
在 Android 和 IOS 上,默认情况下 Flutter 和 React Native 都需要一个原生平台的 Activity / ViewController 支持,且在原生层面属于一个“单页面应用”, 而它们之间最大的不同点其实在于 UI 构建 :
React Native 是一套 UI 框架,默认情况下 React Native 会在 Activity 下加载 JS 文件,然后运行在 JavaScriptCore 中解析 Bundle 文件布局,最终堆叠出一系列的原生控件进行渲染。
简单来说就是 通过写 JS 代码配置页面布局,然后 React Native 最终会解析渲染成原生控件,如 标签对应 ViewGroup/UIView , 标签对应 ScrollView/UIScrollView , 标签对应 ImageView/UIImageView 等。
所以相较于如 Ionic 等框架而言, React Native 让页面的性能能得到进一步的提升。
如果说 React Native 是为开发者做了平台兼容,那 Flutter 则更像是为开发者屏蔽平台的概念。
Flutter 中只需平台提供一个 Surface 和一个 Canvas ,剩下的 Flutter说:“你可以躺下了,我们来自己动”。
Flutter 中绝大部分的 Widget 都与平台无关, 开发者基于 Framework 开发 App ,而 Framework 运行在 Engine 之上,由 Engine 进行适配和跨平台支持。这个跨平台的支持过程,其实就是将 Flutter UI 中的 Widget “数据化” ,然后通过 Engine 上的 Skia 直接绘制到屏幕上 。
所以从以上可以看出:React Native 的 Learn once, write anywhere 的思路,就是只要你会 React ,那么你可以用写 React 的方式,再去开发一个性能不错的App;而 Flutter 则是让你忘掉平台,专注于 Flutter UI 就好了。
额外补充一点,React 的虚拟 DOM 的概念相信大家都知道,这是 React 的性能保证之一,而 Flutter 其实也存在类似的虚拟 DOM 概念。
看过我 Flutter 系列文章可能知道,Flutter 中我们写的 Widget , 其实并非真正的渲染控件,这一点和 React Native 中的标签类似,Widget 更像配置文件, 由它组成的 Widget 树并非真正的渲染树。
Widget 在渲染时会经过 Element 变化, 最后转化为 RenderObject 再进行绘制, 而最终组成的 RenderObject 树才是 “真正的渲染 Dom” , 每次 Widget 树触发的改变,并不一定会导致RenderObject 树的完全更新。
所以在实现原理上 React Native 和 Flutter 是完全不同的思路,虽然都有类似“虚拟 DOM 的概念” ,但是React Native 带有较强的平台关联性,而 Flutter UI 的平台关联性十分薄弱。
React Native 使用的 JavaScript 相信大家都不陌生,已经 24 岁的它在多年的发展过程中,各端各平台中都出没着它的身影,在 Facebook 的 React 开始风靡之后,15 年移动浪潮下推出的 React Native ,让前端的 JS 开发者拥有了技能的拓展。
Flutter 的首选语言 Dart 语言诞生于 2011 年,而 2018 年才发布了 2.0,原本是为了用来对抗 JavaScript 而发布的开发语言,却在 Web 端一直不温不火,直到 17年 才因为 Flutter 而受关注起来,之后又因为 Flutter For Web 继续尝试后回归 Web 领域。
编程开发所涉及的点较多,后面主要从 开发语言 、界面开发 、状态管理 、原生控件 四个方面进行对比介绍。
至于最多吐槽之一就是为什么 Flutter 团队不选择 JS ,有说因为 Dart 团队就在 Flutter 团队隔壁,也有说谷歌不想和 Oracle 相关的东西沾上边。 同时 React Native 更新快 4 年了,版本号依旧没有突破 1.0 。
3.1、 语言
因为起初都是为了 Web 而生,所以 Dart 和 JS 在一定程度上有很大的通识性。
如下代码所示, 它们都支持通过 var 定义变量,支持 async/await 语法糖,支持 Promise(Future) 等链式异步处理,甚至 */yield 的语法糖都类似(虽然这个对比不大准确),但可以看出它们确实存在“近亲关系” 。
/// JS
var a = 1
async function doSomeThing() {
var result = await xxxx()
doAsync().then((res) => {
console.log("ffff")
})
}
function* _loadUserInfo () {
console.log("**********************");
yield put(UpdateUserAction(res.data));
}
/// Dart
var a = 1;
void doSomeThing() async {
var result = await xxxx();
doAsync().then((res) {
print('ffff');
});
}
_loadUserInfo() async* {
print("**********************");
yield UpdateUserAction(res.data);
}
复制代码
但是它们之间的差异性也很多,而最大的区别就是: JS 是动态语言,而 Dart 是伪动态语言的强类型语言。
如下代码中,在 Dart 中可以直接声明 name 为 String 类型,同时 otherName 虽然是通过 var 语法糖声明,但在赋值时其实会通过自推导出类型 ,而 dynamic 声明的才是真的动态变量,在运行时才检测类型。
// Dart
String name = 'dart';
var otherName = 'Dart';
dynamic dynamicName = 'dynamic Dart';
复制代码
如下图代码最能体现这个差异,在下图例子中:
动态语言和非动态语言都有各种的优缺点,比如 JS 开发便捷度明显会高于 Dart ,而 Dart 在类型安全和重构代码等方面又会比 JS 更稳健。
3.2、界面开发
React Native 在界面开发上延续了 React 的开发风格,支持 scss/sass 、样式代码分离、在 0.59 版本开始支持 React Hook 函数式编程 等等,而不同 React 之处就是更换标签名,并且样式和属性支持因为平台兼容做了删减。
如下图所示,是一个普通 React Native 组件常见实现方式,继承 Component 类,通过 props 传递参数,然后在 render 方法中返回需要的布局,布局中每个控件通过 style 设置样式 等等,这对于前端开发者基本上没有太大的学习成本。
如下所示,如果再配合 React Hooks 的加持,函数式的开发无疑让整个代码结构更为简洁。
Flutter 最大的特点在于: Flutter 是一套平台无关的 UI 框架,在 Flutter 宇宙中万物皆 Widget。
如下图所示,Flutter 开发中一般是通过继承 无状态 StatelessWidget 控件或者 有状态 StatefulWidget 控件 来实现页面,然后在对应的 Widget build(BuildContext context) 方法内实现布局,利用不同 Widget 的 child / children 去做嵌套,通过控件的构造方法传递参数,最后对布局里的每个控件设置样式等。
而对于 Flutter 控件开发,目前最多的吐槽就是 控件嵌套和样式代码不分离 ,样式代码分离这个问题我就暂不评价,这个真要实际开发才能更有体会,而关于嵌套这里可以做一些 “洗白” :
Flutter 中把一切皆为 Widget 贯彻得很彻底,所以 Widget 的颗粒度控制得很细 ,如 Padding 、Center 都会是一个单独的 Widget,甚至状态共享都是通过 InheritedWidget 共享 Widget 去实现的,而这也是被吐槽的代码嵌套样式难看的原因。
事实上正是因为颗粒度细,所以你才可以通过不同的 Widget , 自由组合出多种业务模版, 比如 Flutter 中常用的 Container ,它就是官方帮你组合好的模板之一, Container 内部其实是由 Align、 ConstrainedBox 、DecoratedBox 、Padding 、Transform 等控件组合而成 ,所以嵌套深度等问题完全是可以人为控制,甚至可以在帧率和绘制上做到更细致的控制。
当然,官方也在不断地改进优化编写和可视化的体验,如下图所示,从目前官方放出的消息上看,未来这个问题也会被进一步改善。
最后总结一下,抛开上面的开发风格,React Native 在 UI 开发上最大的特点就是平台相关,而 Flutter 则是平台无关,比如下拉刷新,在 React Native 中, 会自带平台的不同下拉刷新效果,而在 Flutter 中,如果需要平台不同下拉刷新效果,那么你需要分别使用 RefreshIndicator 和 CupertinoSliverRefreshControl 做显示,不然多端都会呈现出一致的效果。
3.3、状态管理
前面说过, Flutter 在很多方面都借鉴了 React Native ,所以在状态管理方面也极具“即视感”,比如都是调用 setState 的方式去更新,同时操作都不是立即生效的 ,当然它们也有着差异的地方,如下代码所示:
/// JS
this.state = {
name: ""
};
···
this.setState({
name: "loading"
});
···
this.state.name
/// Dart
var name = "";
setState(() {
name = "loading";
});
···
Text(name)
复制代码
当然它们两者的内部实现也有着很大差异,比如 React Native 受 React diff 等影响,而 Flutter 受 isRepaintBoundary 、markNeedsBuild 等影响。
而在第三方状态管理上,两者之间有着极高的相似度,如早期在 Flutter 平台就涌现了很多前端的状态管理框架如:flutter_redux 、fish_redux 、 dva_flutter 、flutter_mobx 等等,它们的设计思路都极具 React 特色。
同时 Flutter 官方也提供了 scoped_model 、provider 等具备 Flutter 特色的状态管理。
所以在状态管理上 React Native 和 Flutter 是十分相近的,甚至是在跟着 React 走。
3.4、原生控件
在跨平台开发中,就不得不说到接入原有平台的支持,比如 在 Android 平台上接入 x5 浏览器 、接入视频播放框架、接入 Lottie 动画框架等等。
这一需求 React Native 先天就支持,甚至在社区就已经提供了类似 lottie-react-native 的项目。 因为 React Native 整个渲染过程都在原生层中完成,所以接入原有平台控件并不会是难事 ,同时因为发展多年,虽然各类第三方库质量参差不齐,但是数量上的优势还是很明显的。
而 Flutter 在就明显趋于弱势,甚至官方在开始的时候,连 WebView 都不支持,这其实涉及到 Flutter 的实现原理问题。
因为 Flutter 的整体渲染脱离了原生层面,直接和 GPU 交互,导致了原生的控件无法直接插入其中 ,而在视频播放实现上, Flutter 提供了外界纹理的设计去实现,但是这个过程需要的数据转换,很明显的限制了它的通用性, 所以在后续版本中 Flutter 提供了 PlatformView 的模式来实现集成。
以 Android 为例子,在原生层 Flutter 通过 Presentation 副屏显示的原理,利用 VirtualDisplay 的方式,让 Android 控件在内存中绘制到 Surface 层。 VirtualDisplay 绘制在 Surface 的 textureId ,之后会通知到 Dart 层,在 Dart 层利用 AndroidView 定义好的 Widget 并带上 textureId ,那么 Engine 在渲染时,就会在内存中将 textureId 对应的数据渲染到 AndroidView 上。
PlatformView 的设计必定导致了性能上的缺陷,最大的体现就是内存占用的上涨,同时也引导了诸如键盘无法弹出#19718和黑屏等问题,甚至于在 Android 上的性能还可能不如外界纹理。
所以目前为止, Flutter 原生控件的接入上是仍不如 React Native 稳定。
React Native 和 Flutter 都是支持插件开发,不同在于 React Native 开发的是 npm 插件,而 Flutter 开发的是 pub 插件。
React Native 使用 npm 插件的好处就是:可以使用丰富的 npm 插件生态,同时减少前端开发者的学习成本。
但是使用 npm 的问题就是太容易躺坑,因为 npm 包依赖的复杂度和深度所惑,以至于你都可能不知道 npm 究竟装了什么东西,抛开安全问题,这里最直观的感受就是 :“为什么别人跑得起来,而我的跑不起来?” 同时每个项目都独立一个 node_module ,对于硬盘空间较小的 Mac 用户略显心酸。
Flutter 的 pub 插件默认统一管理在 pub 上,类似于 npm 同样支持 git 链接安装,而 flutter packages get 文件一般保存在电脑的统一位置,多个项目都引用着同一份插件。
win 一般是在 C:\Users\xxxxx\AppData\Roaming\Pub\Cache 路径下 mac 目录在 ~/.pub-cache
如果找不到插件目录,也可以通过查看 .flutter-plugins 文件,或如下图方式打开插件目录,至于为什么需要打开这个目录,感兴趣的可以看看这个问题 13# 。
最后说一下 Flutter 和 React Native 插件,在带有原生代码时不同的处理方法:
所以在插件这一块的体验, Flutter 是略微优于 React Native 的。
React Native 编译后的文件主要是 bundle 文件,在 Android 中是 index.android.bunlde 文件,而在 IOS 下是 main.jsbundle 。
Flutter 编译后的产物在 Android 主要是 :
⚠️注意,1.7.8 之后的版本,Android 下的 Flutter 已经编译为纯 so 文件。
在 IOS 主要是 App.framework ,其内部也包含了 kDartVmSnapshotData 、kDartVmSnapshotInstructions 、 kDartIsolateSnapshotData 、kDartIsolateSnapshotInstructions 四个部分。
接着看完整结果,如下图所示,是空项目下 和 GSY 实际项目下, React Native 和 Flutter 的 Release 包大小对比。
可以看出在 React Native 同等条件下, Android 比 IOS 大很多 ,这是因为 IOS 自带了 JSCore ,而 Android 需要各类动态 so 内置支持,而且这里 Android 的动态库 so 是经过了 ndk 过滤后的大小,不然还会更大。
Flutter 和 React Native 则是相反,因为 Android 自带了 skia ,所以比没有自带 skia 的 IOS 会小得多。
以上的特点在 GSY 项目中的 Release 包也呈同样状态。
值得注意的是,Google Play 最近发布了 《8月不支持 64 位,App 将无法上架 Google Play!》 的通知 ,同时也表示将停止 Android Studio 32 位的维护,而 arm64-v8a 格式的支持,React Native 需要在 0.59 以后的版本才支持。
至于 Flutter ,在打包时通过指定 flutter build apk --release --target-platform android-arm64 即可。
说到性能,这是一个大家都比较关心的概念,但是有一点需要注意,抛开场景说性能显然是不合适的,因为性能和代码质量与复杂度是有一定联系的。
先说理论性能,在理论上 Flutter 的设计性能是强于 React Native ,这是框架设计的理念导致的,Flutter 在少了 OEM Widget ,直接与 CPU / GPU 交互的特性,决定了它先天性能的优势。
这里注意不要用模拟器测试性能,特别是IOS模拟器做性能测试,因为 Flutter 在 IOS模拟器中纯 CPU ,而实际设备会是 GPU 硬件加速,同时只在 Release 下对比性能。
代码的实现方式不同,也可能会导致性能的损失,比如 Flutter 中 skia 在绘制时,saveLayer 是比较消耗性能的,比如 透明合成、clipRRect 等等,都会可能需要 saveLayer 的调用, 而 saveLayer 会清空GPU绘制的缓存,导致性能上的损耗,从而导致开发过程中如果掉帧严重。
最后如下图所示,是去年闲鱼用 GSY 项目做测试对比的数据,原文在《流言终结者- Flutter和RN谁才是更好的跨端开发方案?》 ,可以看出在去年的时候, Flutter的整体帧率和绘制就有了明显的优势。
额外补充一点,JS 和 Dart 都是单线程应用,利用了协程的概念实现异步效果,而在 Flutter 中 Dart 支持的 isolate ,却是属于完完全全的异步线程处理,可以通过 Port 快捷地进行异步交互,这大大拓展了 Flutter 在 Dart 层面的性能优势。
之前一篇 《为什么 Airbnb 放弃了 React Native?》 文章,让众多不明所以的吃瓜群众以为 React Native 已经被放弃,之后官方发布的 《Facebook 正在重构 React Native,将重写大量底层》 公示,又再一次稳定了军心。
同时 React Native 在 0.59 版本开始支持 React Hook 等特性,并将原本平台的特性控件从 React Native 内部剥离到社区,这样控件的单独升级维护可以更加便捷,同时让 React Native 与 React 之间的界限越发模糊。
Flutter UI 平台的无关能力,让 Flutter 在跨平台的拓展上更为迅速,尽管 React Native 也有 Web 和 PC 等第三方实现拓展支持,但是由于平台关联性太强,这些年发展较为缓慢, 而 Flutter 则是短短时间又宣布 Web 支持,甚至拓展到 PC 和嵌入式设备当中。
这里面对于 Flutter For Web 相信是大家最为关心的话题, 如下图所示,在 Flutter的设计逻辑下,开发 Flutter Web 的过程中,你甚至感知不出来你在开发的是 Web 应用。
Flutter Web 保留了 大量原本已有的移动端逻辑,只是在 Engine 层利用 Dart2Js 的能力实现了差异化, 不过现阶段而言,Flutter Web 仍处在技术预览阶段,不建议在生产环境中使用 。
由此可以推测,不管是 Flutter 或者 React Native,都会努力将自己拓展到更多的平台,同时在自己的领域内进一步简化开发。
其他需要,也可以在我的 Github 上查看