浅谈 2018 移动端跨平台开发方案这篇文章基本把目前跟客户端“跨平台”搭边的技术梳理了一遍,多达数十种。不过从苹果官方支持度、实用性、使用范围来挑选,主要是这三大类:Hybrid系列、RN系列和 Flutter 系列。本文将对这三类方案作简要总结和对比。

Hybrid 系列


这种是目前使用最简单也最普遍的,主要依靠 WebView 容器展示H5页面,增加了H5可以通过 Javascript 调用的Js Api。通过这些 Js Api 可以调用 Native 的能力。iOS 已经基本由WKWebView替代了UIWebView,通过WebKit开放的能力可以得知网页加载情况并执行响应动作。如图为腾讯新闻小程序,正文通过 webView 展示网页,底部 TabBar 则是 Native 实现的控件。

优点

  1. 几乎可以完全继承现代Web开发的所有成果;
  2. 完全动态化、跨平台;
  3. web开发者不需要太多的学习成本。

缺点

  1. WebView的渲染效率和JS执行性能不佳,跟原生体验相差太远。
  2. Android 手机系统版本和设备厂商的定制,很难保证所在所有设备上都能提供一致的体验。

RN 系列


React NativeWeex 都归为这一类。为了解决 WebView 性能差的问题,以 RN 为代表的这一类框架将最终渲染工作交还给了系统,虽然同样使用类 HTML+JS 的 UI 构建逻辑,但最终会生成对应的自定义原生控件,以充分利用原生控件相对于 WebView 更优的绘制效率。与此同时这种策略也将框架本身和App开发者绑在了系统的控件系统上,不仅框架本身需要处理大量平台相关的逻辑,随着系统版本和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现(比如 RN 的ToastAndroid 顾名思义只能在 Android 使用),这样框架的跨平台特性就会大打折扣。下面对比一下这两个框架的原理和优缺点。

RN

站在 React 肩膀上

React 是一套可以用简洁的语法高效绘制 DOM 的框架,它将网页上真实的 DOM 节点,抽象为 Virtual DOM —— 一个存在于内存中的 JavaScript 对象,与 DOM 一一对应。当界面发生变化时,得益于 DOM Diff 算法能够知道 Virtual DOM 的变化,从而高效改动 DOM,避免重新绘制 DOM。

RN 其实就是Native 版本的 React, 目的是让不熟悉原生平台的前端同学利用 React 也能开发 iOS/Android App.

原理

关于RN的架构、启动流程、渲染过程,以及最核心问题 —— JS 与 Native 如何交互,可参考网上的一系列优秀文章:

缺点

  1. Learn once, write anywhere. 一致性程度有限,不少组件和API都区分了 Android 和 iOS 版本。即使是共用组件,也会有平台独享的函数。平台间的差异性导致维护成本高。
  2. 对移动端开发学习成本高,对前端开发者来说也需要对原生平台有所了解。
  3. 滑动列表性能表现不佳:
    • 不支持视图重用,随着 Cell 数量的增加,占用的内存也线性增加。
    • UITableView 是主线程同步的,为了保证 UI 流畅度,UI 的渲染需要达到60帧/秒,每帧的大致消耗时间保持在16ms之内。而RN 运行在单独线程,和UI主线程不同步,也就是从 RN Render 到真正调用 native 代码这个过程是异步的,导致从js运行到最后系统渲染的总时间很难做到<16ms.

Weex

Weex 使用的前端框架是Vue, 相比 React 更轻量一些。


更详细的原理解析可参考Weex 是如何在 iOS 客户端上跑起来的

优点

  1. 相比 RN 滑动列表性能好。充分利用了UITableView或者RecycleView的重用机制实现了性能的优化。
  2. Write Once, Run Everywhere. 保持Web、Android 和 iOS 三端高度一致性。
  3. 功能扩展更灵活,一些三方库如图片、网络等可由开发者自己适配。


Flutter 系列

Flutter 通过跨平台的 Skia 图形库实现渲染引擎,所有控件都是使用 Dart 语言重新开发(Material是安卓风格的控件库,Cupertino是iOS风格的控件库),然后通过自己的渲染引擎绘制出来,不使用系统原生控件。这样可以在最大程度上保证不同平台、不同设备的体验一致性。支持AOT的Dart语言,执行效率也非常高,使得Flutter完全可以媲美原生应用的性能。
Flutter框架的整个架构如下:

特性

Hot Reload


程序运行到设备上后,有任何代码修改,Save 之后立即就可以更新到界面上,只需要几百毫秒。这一功能得益于Dart 在Debug模式下使用JIT(Just-in-time, 动态编译)执行方式,触发热刷新时Flutter会检测发生改变的Dart文件,将其同步到App私有缓存目录下,DartVM加载并且修改对应的类或者方法,重建控件树后立即可以在设备上看到效果。

响应式

这个就是学React的,API名字也是setState.

class CounterState extends State<Counter> {
  int counter = 0;

  void increment() {
    // 告诉Flutter state已经改变, Flutter会调用build(),更新显示
    setState(() {
      counter++;
    });
  }

  Widget build(BuildContext context) {
    // 当 setState 被调用时,这个方法都会重新执行.
    // Flutter 对此方法做了优化,使重新执行变的很快
    // 所以你可以重新构建任何需要更新的东西,而无需分别去修改各个widget
    return new Row(
      children: <Widget>[
        new RaisedButton(
          onPressed: increment,
          child: new Text('Increment'),
        ),
        new Text('Count: $counter'),
      ],
    );
  }
}

支持AOT和JIT

Dart语言可以有两种编译模式:JIT和AOT。

  • JIT模式运行效率低,但是可以支持Hot Reload,只在Debug模式下使用
  • AOT模式运行效率高,在Release模式下使用。通过区分编译模式,提高正式版本中代码的运行速度。

Platform Channels

同样是去调用 Native Service 的 API, RN 和 Hybrid APP 都是使用 Bridge 的技术来调用。而 Flutter 提供了 Platform Channels 来让 Dart 代码和 Native Service API进行互操作,分为以下三类:

  • Message channel:用于传递字符串和半结构化的信息。
  • Method channel:用于传递方法调用(method invocation)。
  • Event channel:用于数据流(event streams)的通信。

Flutter和平台端通信的原理:通过消息信使(BinaryMessenger)来异步的收发二进制消息,每个消息都有对应的消息渠道(channel)来区分不同的消息用途,然后使用不同的消息编解码器(Codec)对二进制数据进行序列化与反序列化,最后通过注册的消息处理器(MessageHandler)来处理并回复对应的消息。

// 获取系统电量信息的异步方法
Future<Null> getBatteryLevel() async {
  var batteryLevel = 'unknown';
  try {
    int result = await methodChannel.invokeMethod('getBatteryLevel');
    batteryLevel = 'Battery level: $result%';
  } on PlatformException {
    batteryLevel = 'Failed to get battery level.';
  }
  setState(() {
    _batteryLevel = batteryLevel;
  });
}

这种方式用起来比 Bridge 简单许多,据说性能也更好。

性能优化

通过网上各种 Flutter 和 RN 的对比测试数据来看,Flutter在高低端机CPU上的流畅度、内存等表现都优于RN。除了 Flutter 团队做的各种性能优化措施外,很大程度还是二者架构方案的不同导致的。RN 最大的性能瓶颈就在于 JAVA/OC 和 JS 的 Bridge 通信上
RN的实现方式都是在JS层完成DOM的对比和更新,然后跨层通信到平台,创建平台组件,然后渲染。流程较长,而且存在线程跳跃:

Flutter 渲染引擎和控件都是自己实现,没有使用系统控件,整个渲染流程大大缩短。而且渲染引擎是C++实现,直接调用系统Canvas绘制,没有跨层通信和线程跳跃的问题:

总结

性能 跨端性 动态化
Hybrid系列
RN系列
Flutter系列
原生SDK

综合来讲没有十全十美的跨平台方案,每种方案在性能、跨端、动态上都有得有失。

  • 对性能要求不高的场景,如简单的app内嵌活动H5场景,基于 WebView 的 Hybrid 方案是个不错的选择,对项目无侵入,且门槛很低。
  • 如果对动态化需求强烈,不能接受 WebView 的性能问题,RN系列是不错的方案,跨端性的问题其实Weex已经解决了。RN和Weex都有大DAU的产品在使用,社区相对成熟。
  • Flutter 性能和跨端都是做的最好的,缺陷在于暂不支持动态化,可能是考虑到苹果官方不允许的问题。另外语言和框架都是全新的,需要从头学。