再谈移动端跨平台框架 Flutter 与 React Native

现如今,在跨平台方案上仍活跃于市场的,仅剩 RN 与 Flutter。让我们通过框架的设计初衷,架构,开发环境,代码风格,等多个层面看看它们的差异。看看哪一个更适合你。

前言

这几年在大前端的开发领域,选择跨端方案的公司和部门越来越多,一方面是跨平台的前端框架越来越成熟,另一方面也是因原生开发者正逐年减少。所以,在当下掌握一门跨平台的技术栈还是很有必要的,无论从广度还是从深度都会有所帮助。

那我们应该选择哪种技术方案呢?如果这个问题放在几年前,答案可能会有很多。不过现在看来,市面上仅剩两种主流方案,就是经常听到的 React Native 和 Flutter。一个出自 Facebook,一个出自 Google。

这两个方案的优劣已有很多点评,基本上形成了两种阵营。但在我看来,它们其实没有明显的差距。如果有,早就被市场所淘汰了。现在看来所谓的劣势,很快就会被那帮天才工程师们,想出解决方案而弥补上了。这也许是竟争帮助了整个生态的完善。反而是 Apple 一直没有跟上,可能还是源于闭源生态,没有另外两家那么的急于变革。

反观 Google 的野心其实是很大的,想通过跨平台方案(无论是 Flutter 还是 Kotlin),从社区和开发者入手一统语言,甚至操作系统(Fuchsia),从而扩展更大的版图。Facebook 则想利用自己多年在前端领域积累的丰富经验,通过 React 切入所有平台。这可能成为了两套框架的设计初衷。

Microsoft 到是另辟蹊径,在 IDE(VSCode)上花大力气,帮助大家建立更好的开发体验,统一了开发环境。

SDK 版本

Flutter: 2.5.x React Native: 0.64.x

1. 架构

1.1 设计理念

在端上的开发,有前辈总结了一个很精辟的观点:端上的开发无外乎三件事,“数据获取”,“状态管理”,“页面渲染”。而在跨端领域的竟争,我理解是“虚拟机”,“渲染引擎”,“原生交互”,“开发环境”的竟争。而在这几点上,无论是 Flutter 还是 React Native (以下简称 RN) 都有非常棒的解决方案。

首先从 Flutter 来看,在虚拟机上使用了 Dart VM,Dart 支持 JIT 与 AOT 两种编译模式,也就是我们所说的动态编译与静态编译。在开发阶段使用 JIT 编译,实现热更新预览,动态加载等,而在发布阶段使用 AOT 模式编译为机器码,保证启动速度和跨端信息的传递效率。在渲染引擎上,Flutter 使用了 Skia 渲染引擎进行视图绘制,避开了不同平台上控件渲染差异。而且,少了这一层的交互,使得效率也得到提升。而在原生交互上,因为 Dart 本身跨平台的特性,底层 C++ 可以直接访问到原生的 API,加上信息使用机器码进行传递 (BinaryMessage),所以与原生交互的效率非常高。

然后再说 RN ,在早期的架构上虚拟机使用的是 JSC (Javascript Core) 执行运算,这样它可以充分复用 JS 生态,吸引大量前端开发者参与。而且由于 JS 天生跨平台的特点,跨端移值 App 也顺理成章。在渲染引擎上 RN 没有直接使用 WebKit 或其它 Web 引擎,因为之前 Web 在构建复杂页面时带来的计算消耗,远比不上纯原生引擎的渲染。所以它直接复用了原生的渲染通道,这样就可以带来与原生近乎一致的体验。

不过说到这儿,你可能发现虽然早期的 RN 架构充分利用了现有生态,但毕竟不像 Flutter 那样从头到尾都自己来,那么的撤底。带来的问题就是,在 JSC 到原生渲染这一层,用了非常多的 Bridge,并通过 JSON 序列化在多个线程里来回传递信息,这样的消耗在简单的交互过程中可能不明显,而在大量的交互与渲染上会有明显的卡顿,这也成为广为诟病的一点。不过在新的架构中, RN 也做出了新的方案去解决这些痛点,下面会有介绍。

但我们知道 Flutter 也不是完美的,虽然什么事情都自己造自己来,但因为缺少成熟的生态,很多问题都需要官方或社区提供足够的轮子才能解决,否则开发者会在遇到特定问题时,只能自己想办法。另外,Dart 发布阶段用了静态编译,虽然效率得到了提升,但也缺少了在线动态更新的灵活性。

1.2 核心架构

1.3.1 Flutter

Flutter 的架构分为了三层,我们大多情况只与 Flutter Framework 层交互,更多平台无关的的底层能力已被封装好。这也使得 Flutter Framework 非常的轻,如果你需要更多的原生能力,通常使用各类 Flutter Plugin 比如 Camera

所以原生能力(轮子)依赖于官方和社区的产出速度

1.3.2 React

新旧架构对比

Old

三个线程各自负责运算,渲染,Native 交互,中间的交互使用 Bridge 与 JSON 信息格式进行传递。

New

新的架构主要有两点改变

  1. JS Bundle 不再依赖于 JSC(Javascript Core)。换句话说,它可以编译和应用在任何 JS 引擎 (V8等)。

  2. 引入 JSI 标准,基于 JSI 协议实现各自方法,使得 JS 可以直接引用 C++ 对象,反之亦然。与原生之间的交互不再用 Bridge 去做粘合。

渲染引擎仍是依赖原生的管道。猜测可能 FB 没有像 Google 那样,有这么多年的 Web 渲染引擎经验,轮子就不用再花时间再造了

RN Bridge 上的变化

Old

可以看到 Bridge 非常的重

将原先较重的 Bridge 分拆成两个模块,Farbric 处理 UI,TurboModules 处理与原生交互

两个模块均是遵循 JSI 协议的 C++ 模块

2. 核心流程

2.1 数据获取

2.1.1 网络请求

Flutter

React Native

http.dart 库C++ 实现

复用现有的 JS 库fetch, XMLHttpRequest, Axios

Flutter

import 'package:http/http.dart' as http;

// 它返回一个 Flutter 的 Future 对象,类似 JS 的 Promise.
http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
复制代码

RN

fetch('https://reactnative.dev/movies.json');
复制代码

其它 JS 生态里的网络库都是适用的

2.1.2 JSON 模型化

Flutter

官方提供了 json_serializable 库,让你可以先定义好模型与属性后,直接通过命令行生成对应的 JSON 转模型代码。

@JsonSerializable()
class User {
  User(this.name, this.email);

  String name;
  String email;

  /// A necessary factory constructor for creating a new User instance
  /// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
  /// The constructor is named after the source class, in this case, User.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` is the convention for a class to declare support for serialization
  /// to JSON. The implementation simply calls the private, generated
  /// helper method `_$UserToJson`.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
复制代码

运行脚本命令即可

flutter pub run build_runner build
复制代码

React Native

官方没有提供了 JSON Model 转化库,需要自己找轮子。

2.2 状态管理

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 变化去更新控件,但写法和 JS 有些不大一样,需要习惯

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'
  }
});
复制代码

2.3 页面渲染

2.3.1 共性

在渲染方式上,理论基本一样,先是构建一颗平台无关性的虚拟树 (Virtual Dom Tree),然后通过各自不同的实现自已渲染或交给原生进行渲染。

虚拟树的好处可以实现 UI 节点的局部更新,而不会全量刷新,具有平台无关性

  • 两个框架都是 UI 响应式框架(React Framework)

    UI = f(state) UI 仅依赖于它的父类与自身的状态

    Flutter 在设计之初,也借鉴了很多 React 的设计思想。

  • 所有组件都可被组合成一颗虚拟树虚拟树 (VDom),在真正渲染前各个框架会把它们转化为各自的渲染对象 (RenderObj / VDom)。

2.3.2 差异

2.3.2.1 布局

Flutter

在 Flutter 中,UI 组件称为 Widget,Flutter 将所有可能的控件都封装为 Widget ,而 RN 没有将所有控件封装,而是将样式与 Component 分离,进行自由组合。所以你不会在 RN 里看到长长的嵌套。

Flutter Widget 嵌套组合:

虽然看起来组合 UI 很合理,但对于处理复杂的 UI 场景,就拙荆见肘了,比如富文本

在 RN 中,UI 组件称为 Component,布局沿用了 Component (类似 Web UI 元素) + Style (类似 CSS) 进行布局,没有像 Flutter Widget 一样先封装好各种"样式+组件",而是把选择权交给你自己进行组合。

import {View, Text, StyleSheet} from ‘react-native';

class HelloThere extends React.Component {
  render() {
    return (
      <View style={styles.box}>
        <Text>Hello World!</Text>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  box: {
    borderColor: 'red',
    backgroundColor: '#fff',
    borderWidth: 1,
    padding: 10,
    width: 100,
    height: 100
  }
});
复制代码

2.3.2.2 绘制

Flutter

正如上面提到的架构所示,Flutter 不需要和原生渲染引擎打交道,直接通过 Skia (2D 渲染引擎)进行绘制 (GPU),所以它的渲染管道非常简洁高效。

React Native

RN 是在通过 Yoga (布局引擎)计算好后位置后,通过不同平台的渲染管道进行渲染,所以这里在 Layout 计算与投递结果的过程中多了 Bridge 环节,效率可想而知。\

Flutter UI 所见即所得,在所有平台上表现一致。RN 依赖平台的原生控件样式,表现更趋于原生。

2.3.3 渲染过程

Flutter

如前所说,Flutter 在更新完 UI Tree 后直接通过 GPU 渲染

React Native

和 React Render 很类似,先是更新 VDom ,然后再更新真正的组件,只是 RN 是 Native 组件

2.4 原生交互

2.4.1 混合开发 (Embed)

Flutter

Flutter 内嵌入 Native 页面

Fluttter 提供了 AndroidViewUiKitView 来支持原生页面的嵌入,不过这类 Widget 在使用中还要注意布局,事件的回调等诸多问题,从官方的文档来看其实不太推荐这类场景。虽然架构上没有限制,但目前桌面端的 Widget 还不支持。

if (defaultTargetPlatform == TargetPlatform.android) {
  return AndroidView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
  return UiKitView(
    viewType: 'plugins.flutter.io/google_maps',
    onPlatformViewCreated: onPlatformViewCreated,
    gestureRecognizers: gestureRecognizers,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}
return Text(
    '$defaultTargetPlatform is not yet supported by the maps plugin');
复制代码

Native 嵌入 Fluttter

如 Flutter Demo 所示一样,它可以被嵌入任何 Activity 或 ViewController 中。

官方建议最好是在应用初始化时将 Flutter 环境加载好,或者在向用户展示 Flutter 页面前加载好。因为 Flutter 初始化要做很多事情,如 加载 Flutter 库,初始化 Dart VM, 创建 Dart Isolate(内存与线程管理),UI 初始化等。预热的时间消耗大概是在 300ms 左右(参考官方数据

React Native

React Native 与 Native 原生的控件互嵌相对比较容易。

Native 内嵌入 RN 页面

iOS

RCTRootView 我们可以认为是 RN 的一个容器,可以像处理普通 View 一样进行添加。但要注意 RN 里的 layout 要设置为 flex 布局,以便按容器的 size 去适配。

- (void)viewDidLoad {
    ...
    RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                       moduleName:appName
                                                initialProperties:props];
    rootView.frame = CGRectMake(0, 0, self.view.width, 200);
    [self.view addSubview:rootView];
    ...
}
复制代码

RN 内嵌入 Native 页面

iOS

继承 RCTViewManager 。然后和事件通信一样,通过 RCT_EXPORT_MODULE 暴露 Native 对应的类,然后实现 view 方法,返回 native 的 view 实例。

// RNTMapManager.m
#import <MapKit/MapKit.h>

#import <React/RCTViewManager.h>

@interface RNTMapManager : RCTViewManager
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

- (UIView *)view
{
  return [[MKMapView alloc] init];
}

@end
复制代码

然后在 RN 里直接插入该 View 到对应的 UI 组件下。

import { requireNativeComponent } from 'react-native';

// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap');

// MyApp.js

import MapView from './MapView.js';

...

render() {
  return <MapView style={{ flex: 1 }} />;
}
复制代码

Android 的嵌入方式类似

2.4.2 事件通信

Flutter

Native <-> Shell (iOS /Android) <-> MethodChannel (Flutter Framework) <-> Dart Code

Message 会被不同的平台间进行类型转换,比如 Map -> HashMap/Dictionary

Dart

const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
复制代码

iOS

let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
  (call: FlutterMethodCall, result: FlutterResult) -> Void in
  switch (call.method) {
    case "bar": result("Hello, (call.arguments as! String)")
    default: result(FlutterMethodNotImplemented)
  }
}
复制代码

Android

val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    else -> result.notImplemented()
  }
}
复制代码

React Native

Native

在 Native 侧只需实现对应的协议,即可将类或方法暴露给 RN

React 通常将要它们称为 Module

iOS

//  RCTCalendarModule.h
#import <React/RCTBridgeModule.h>
  
// 在对应的 Native Class 声明上加上 RCTBridgeModule 协议
@interface RCTCalendarModule : NSObject <RCTBridgeModule>
@end
  
  
// RCTCalendarModule.m
#import "RCTCalendarModule.h"

@implementation RCTCalendarModule

// 将这个类暴露给 RN 掉用。如果不指定名称,默认以类的名字命名
RCT_EXPORT_MODULE();

// 暴露一个方法给 RN
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
}

@end
复制代码

Android

参照以下方式引入 RN 各个库,并继承 ReactContextBaseJavaModule ,加上 @ReactMethod 标识以暴露方法

package com.your-app-name; // replace com.your-app-name with your app’s name
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;

public class CalendarModule extends ReactContextBaseJavaModule {
   CalendarModule(ReactApplicationContext context) {
       super(context);
   }
}

// 暴露方法给 RN
@ReactMethod
public void createCalendarEvent(String name, String location) {
}
复制代码

RN

这个时候就可以通过下面方法访问到 Native 的类

const { CalendarModule } = ReactNative.NativeModules;
复制代码

3. 开发环境

3.1 编码

Flutter

声明式的语言结构

每个 Widget 可以继续嵌套 Widget ,有点类似俄罗斯套娃。

SwiftUI 也是声明式的,写法很类似

var container = Container( // grey box
  child: Center(
    child: Container( // red box
      child: Text(
        "Lorem ipsum",
        style: bold24Roboto,
      ),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: EdgeInsets.all(16),
      width: 240, //max-width is 240
    ),
  ),
  width: 320, 
  height: 240,
  color: Colors.grey[300],
);
复制代码

React Native

RN 可以支持函数式编程 Hook 与 Class 方式编写。样式与组件代码分离,不会有长长嵌套出现。

3.2 调试

在 UI 调试上,两者都有对应的工具。效果上来看,RN 更加像 JS 的调试工具一样,上手比较快。

react-devtools

Flutter Widget Inspector

但两个方案都有共同的一个问题,就是当需要 Native 与 RN/Flutter 联调时,比如在两侧都要打断点时,好像没有推荐的做法。

4. 维护成本

4.1 环境依赖

Flutter

  • Flutter SDK

  • XCode

  • Android toolchain

React Native

  • React Native SDK

  • XCode

  • Android toolchain

  • Node

4.2 工程化

Flutter

可使用线上代码管理,进行一站式代码提交,打包 Flutter 项目,不过目前还没有国内平台支持。

或者通过 fastlane 手动设置 pipeline.

React Native

官方没有提供最佳实践,不过因为 JS 在线打包很多平台都已支持,所以只要配置对应的 Native 工程环境即可。

4.3 产物

Flutter

通过 flutter 可以用命令行工具手动生成最终产物

iOS 生成的是两个 framework

flutter build ios-framework
复制代码
  • App.framework (你的 Dart 代码产物) ~ 100 KB (模板空项目)

  • Flutter.framework (依赖的 Flutter 库)~ 100mb

Android 可以生成 aar 或 apk

flutter build apk
复制代码
  • libapp.so (你的 Dart 代码产物) ~ 3.4 MB (模板空项目)

  • libflutter.so (flutter 工程产物) ~ 9 MB

React Native

Metro

RN 通过 Metro(专为 React Native 设计)打包工具将所有 RN 代码打包成对应的 js.bundle 产物,双端产物大小差不多。

你也可以自己通过命令行生成离线包:

react-native bundle --entry-file index.js --bundle-output ./bundle/ios.bundle --platform ios --dev false
复制代码

注意:--dev false 生成的非 dev 包和 dev 包,大小还是差很多的。

官方提供的一个初始化工程,生成的 bundle 大概是在 750 KB 左右

5. 性能

5.1 渲染性能

在大多数浏览器和手机设备上都是 60HZ 刷新频率,也就我们只能在每帧 16ms 的时间内处理完所有事情,包括渲染才能保证显示的平滑。

React Native

在渲染效率上,官方其实也提到了,我们的大部分业务逻辑和事件处理都是在 JS 线程上的,因为架构的原因,在 JS 线程处理完数据之后,要扔给 UI 线程进行 Native 原生控件渲染,如果这个时间等于 200ms 就会丢掉 12 帧。而出现卡顿。如果任何情况下超过 100ms 就会被用户所感知。这种情况通常发生在新进一个页面时,要计算所有控件和布局进行渲染。

Flutter

其实 Flutter 因为少了原生控件的转化,少了一步桥接上的时间消耗。但要注意的问题仍一样,业务逻辑的处理耗时,和 UI Tree 层级。

"configurations": [
  {
    "name": "Flutter",
    "request": "launch",
    "type": "dart",
    "flutterMode": "profile"
  }
]
复制代码

可通过 VS Code 安装 Dart Extension, 然后点击状态栏下方 status bar ,打开 DevTools 查看时时性能。

DevTools

6. 综合比较

Flutter

React-Native

备注

背后团队

Google

Facebook

发布日期

2017.5

2015.3

React-Native 是一个更成熟的框架

编程语言

Dart

Javascript

学习曲线

如果你已经了解 JS,将会更快上手 RN.

热加载

热更新

RN 可下发 JS 实现。 Flutter 产物已为二进制

开源

文档完整性

编程架构

State Manager

Flux

都基于状态管理

自动化集成发布

官方文档

无可用的官方文档

插件数量

~20k

~30k

如果算上 React 的话插件就有 200k 左右

仓库地址

Github Stars/Forks

132k/19k

99k/21k

产物

~10MB (Android) ~100MB (iOS)

~ 70M (Android) ~ 40M (iOS)

模板空工程,多架构产物

什么时候选择跨平台框架

  • 当你没有太多 UI 动效和复杂的交互界面时

  • 如果你已有原生项目,想在部分模块提升开发效率时

  • 当你新建一个项目,想快速试错时

什么时候推荐使用 RN?

  • 已有项目,有较多场景想混合开发时

  • 已有前端页面,想尽快移植时

  • 有大量前端开发者,Native 人员不足时

  • 有真正跨多端场景时, iOS/Native/Web/Desktop

什么时候推荐使用 Flutter?

  • 全新项目,无太多混合开发的场景

  • 现存项目,没有太多 Native 与 Flutter 页面互相嵌套的情况

  • 在移动设备上对于渲染性能及 UI 一致性有较高要求时

Last updated