其他教程

其他教程

Products

当前位置:首页 > 其他教程 >

App 跨平台框架 VS 原生开发深度评测之 2023 版

GG网络技术分享 2025-03-18 16:16 0


App 跨平台框架历史悠久,从 cordova、react native、flutter,直到最近的 uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。

过去的问题到底在哪里?

我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差别具体在哪里。

逻辑层

渲染层

类型

代表作

webview

webview

弱类型

5+App、cordova

js 引擎

webview

弱类型

uni-app 之 app-vue 、小程序(dount)

js 引擎

原生渲染

弱类型

react native、uni-app 之 app-nvue、weex

dart 引擎

flutter 渲染引擎

强类型

flutter

js 引擎

flutter 渲染引擎

弱类型

微信 skyline、webF、ArkUI-x

kotlin

原生渲染

强类型

uni-app x

kotlin

原生渲染

强类型

原生应用

上面的表格,除了行尾的原生应用外,各个跨平台框架按出现时间排序,可以看到跨平台框架是如何演进的。

上表中,uni-app x 和原生应用是一样的,逻辑层和渲染层都是原生,都是强类型;而其他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。

webview 不行已经是业内常识了,启动慢、渲染慢、内存占用高。这块本文不再详述。

但那些非 web-view 的框架到底哪里不如原生?

1. js 逻辑 + 原生渲染

react native、weex 等抛弃 webview,改由原生渲染的跨平台方案,2014 年就推出了。 如今手机硬件也越来越好了,为什么性能还达不到原生?

js + 原生渲染的方案主要有 2 点缺陷:

  • JS 引擎自身的性能问题

  • JS 和原生之间的通信延迟

1.1 js 引擎慢,启动速度和运行速度都弱于原生

所以很多开发者即便使用这类方案,首页也还是原生来写。

React Native 的 Hermes 引擎和华为的 arkUI,提供了 js 编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。

弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。

以数字运算为例,js 的 number 运算确实比强类型的 int 慢,内存开销也更大。

1.2 js 语言与原生之间通信卡顿

每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。

逻辑层的 js,即要和原生渲染层通信,还要和原生 API 通信:

1.2.1 js 与原生 ui 通信

举个简单的场景例子,在 js 里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。

如果全部在 webview 里,js 操作 ui 还好一些,所以 uni-app 的 app-vue 里的 renderjs 操作 UI 性能高,就是这个道理。同理还有微信小程序的 wsx。

虽然小程序和 uni-app 都是 js,但实际上逻辑层在独立 js 引擎里,通过原生桥来控制 web-view,通信成本很高。

weex 提供了 bindingx 技术,这是一种弱编程,渲染层预先定义了一些操作 UI 的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在 js 里高性能、自由的操作所有 UI。

1.2.2 js 操作原生 api

操作系统和三方 SDK 的 API 都是原生的,js 调用这些能力也需要跨语言通信。比如 js 调用原生的 Storage 或 IO,数据较多时遍历的性能非常差。

当然在 js API 的封装上可以做些优化,比如微信的 storage 提供了 wx.batchGetStorageSync 这种批量读取的 API,既然遍历性能差,那干脆一次性从原生读出来再传给 js。

这也只能是无奈的方案,如果在遍历时想用 js 做什么判断就实现不了了,而且一次性读出很大的数据后传给 js 这一下,也需要通信时间。

2. flutter 方案

flutter 在 2018 年发布,第一次统一了逻辑层和渲染层,而且使用了强类型。

它没有使用原生渲染,而是使用由 dart 驱动的渲染引擎,这样逻辑层的 dart 代码操作 UI 时,再也没有延时了!bindingx、wxs 这种补丁方案再也不需要了。

并且 dart 作为强类型,编译优化很好做,启动速度和运行速度都胜过 js。

在这个开源项目下 https://gitcode.net/dcloud/test-cross/-/tree/master/test_flutter_slider_100,提供了一个 flutter 编写的 100 个 slider 同时滑动的示例, 项目下有源码也有打包好 apk,可以直接安装体验。

100 个 slider 同时滑动,非常考验逻辑和 UI 的通信。如果在 webview 内部,html 和 js 写 100 个这样的 slider,在新的手机上表现也还 ok。但在小程序和 react native 这种逻辑和 UI 分离的模式下,100 个 slider 是灾难。

下载安装 apk 后可以看到 dart 操作 flutter 的 UI 真的没有通信折损,100 个 slider 的拖动非常流畅。

点击查看视频

flutter 看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?

2.1 dart 与原生 API 的通信

别忘了上面 1.2.2 提到的原生 API 通信。flutter 虽然在逻辑层和渲染层都是 dart,但要调用原生 API 时,还是要通信。

操作系统和三方 SDK 的 API 是原生的,让 dart 调用需要做一层封装,又落到了跨语言通信的坑里。

https://gitcode.net/dcloud/test_flutter_message_channel 这是一个开源测试项目,来测试原生的 claas 数据与 dart 的通信耗时。

项目里面有源码,大家可自行编译;根目录有打包好的 apk,也可以直接安装体验。

这个项目首先在 kotlin 中构建了包含不同数据量的 class,传递到 dart 然后渲染在界面上,并且再写回到原生层。

有 0.1k 和 1k 两种数据量(点击界面上的 1k 数字可切换),有读和读并写 2 个按钮,各自循环 1000 次。

以下截图的测试环境是华为 mate 30 5G,麒麟 990。手机上所有进程杀掉。如下图:

  • 1k 数据从原生读到 dart 并渲染

  • Image

  • 1k 数据从原生读到 dart 并渲染再写回

  • Image

  • 0.1k 数据从原生读到 dart 并渲染

  • Image

  • 0.1k 数据从原生读到 dart 并渲染再写回

  • Image

通信损耗非常明显。并且数据量从 1k 降低到 0.1k 时,通信时间并没有减少 10 倍,这是因为通信耗时有一个基础线,数据再小也降不下去。

为什么会这样?因为 dart 和 kotlin 不是一种编程语言,不能直接调用 kotlin 的 class,只能先序列化成字符串,把字符串数据从原生传到 dart,然后在 dart 层再重新构造。

当然也可以在原生层为 dart 封装 API 时提供 wx.batchGetStorageSync 这类批处理 API,把数据一次读好再给 dart,但这种又会遇到灵活性问题。

而在 uni-app x 中,这种跨语言通信是不存在的,不需要序列化,因为 uni-app x 使用的编程语言 uts,在 android 上就编译为了 kotlin,它可以直接调用 kotlin 的 class 而无需通信和封装。示例如下,具体 uni-app x 的原理后续章节会专题介绍。

<template>

</template>

<script lang=\"uts\">

import Build from \'android.os.Build\';

export default {

onLoad() {

console.log(Build.MODEL); //uts可以直接导入并使用原生对象,不需要封装,没有跨语言通信折损

}

}

</script>

再分享一个知识:

很多人都知道 iPhone 上跨平台框架的应用,表现比 android 好。但大多数人只知道是因为 iPhone 的硬件好。

其实还有一个重要原因,iOS 的 jscore 是 c 写的,OS 的 API 及渲染层也都是 ObjectC,js 调用原生时,某些类型可以做共享内存的优化。但复杂对象也还是无法直接丢一个指针过去共享使用内存。

而 android,不管 java 还是 kotlin,他们和 v8、dart 通信仍然需要跨语言通信。

2.2 flutter 渲染和原生渲染的并存问题

flutter 的自渲染引擎,在技术上是不错的。但在生态兼容上有问题。

很多三方软件和 SDK 是原生的,原生渲染和 flutter 自渲染并存时,问题很多。

flutter 开发者都知道的一个常见坑是输入法,因为输入法是典型的原生 UI,它和 flutter 自绘 UI 并存时各种兼容问题,输入框被遮挡、窗体 resize 适应,输入法有很多种,很难适配。

混合渲染,还有信息流广告、map、图表、动画等很多三方 sdk 涉及。这个时候内存占用高、渲染帧率下降、不同渲染方式字体不一致、暗黑主题不一致、国际化、无障碍、UI 自动化测试,各种不一致。。。

这里没有提供开源示例,因为 flutter 官方是承认这个问题的,它提供了 2 种方式:混合集成模式和虚拟显示模式模式。

但在渲染速度、内存占用、版本兼容、键盘交互上都各自有各自的问题。详见 flutter 官网:https://docs.flutter.dev/platform-integration/android/platform-views#performance。这个是中文翻译:https://flutter.cn/docs/platform-integration/android/platform-views#performance

在各大 App 中,微信的小程序首页是为数不多的使用 flutter UI 的界面,已经上线 1 年以上。

下面是微信 8.0.44(此刻最新版),从微信的发现页面进入小程序首页。 点击查看视频

视频中手机切换暗黑主题后,这个 UI 却还是白的,而且 flutter 的父容器原生 view 已经变黑了,它又在黑底上绘制了一个白色界面,体验非常差。

这个小程序首页界面很简单,没有输入框,规避了混合渲染,点击搜索图标后又跳转到了黑色的原生渲染的界面里。

假使这个界面再内嵌一个原生的信息流 SDK,那会看到白色 UI 中的信息流广告是黑底的,更无法接受。

当然这不是说 flutter 没法做暗黑主题,重启微信后这个界面会变黑。这里只是说明渲染引擎不一致带来的各种问题。

注:如何识别一个界面是不是用 flutter 开发的?在手机设置的开发者选项里,有一个 GPU 呈现模式分析,flutter 的 UI 不触发这个分析。且无法审查布局边界。

flutter 的混合渲染的问题,在所有使用原生渲染的跨平台开发框架中都不存在,比如 react native、weex、uni-app x。

总结下 flutter:逻辑层和 UI 层交互没有通信折损,但逻辑层 dart 和原生 api 有通信成本,自绘 UI 和原生 ui 的混合渲染问题很多。

3. js+flutter 渲染

flutter 除了上述提到的原生通信和混合渲染,还有 3 个问题:dart 生态、热更新、以及比较难用的嵌套写法。

一些厂商把 flutter 的 dart 引擎换成了 js 引擎,来解决上述 3 个问题。比如微信 skyline、webF、ArkUI-x。

其实这是让人困惑的行为。因为这又回到了 react native 和 weex 的老路了,只是把原生渲染换成了 flutter 渲染。

flutter 最大的优势是 dart 操作 UI 不需要通信,以及强类型,而改成 js,操作 UI 再次需要通信,又需要 js 运行时引擎。

为了解决 js 和 flutter 渲染层的通信问题,微信的 skyline 又推出了补丁技术 worklet 动画,让这部分代码运行在 UI 层。(当然微信的通信,除了跨语言,还有跨进程通信,会更明显)

这个项目 https://gitcode.net/dcloud/test-cross/-/tree/master/test_arkuix_slider_100, 使用 ArkUI-x 做了 100 个 slider,大家可以看源码,下载 apk 体验,明显能看到由于逻辑层和 UI 层通信导致的卡顿。

点击查看视频

上述视频中,注意看手指按下的那 1 个 slider,和其他 99 个通过数据通讯指挥跟随一起行动的 slider,无法同步,并且界面掉帧。

不过自渲染由于无法通过 Android 的开发者工具查看 GPU 呈现模式,所以无法从条状图直观反映出掉帧。

注意 ArkUI-x 不支持 Android8.0 以下的手机,不要找太老的手机测试。

很多人以为自渲染是王道,但其实自渲染是坑。因为 flutter 的 UI 还会带来混合渲染问题。

也就是说,js+flutter 渲染,和 js + 原生渲染,这 2 个方案相比,都是 js 弱类型、都有逻辑层和渲染层的通信问题、都有原生 API 通信问题,而 js+flutter 还多了一个混合渲染问题。

可能有的同学会说,原生渲染很难在 iOS、Android 双端一致,自渲染没有这个问题。

但其实完全可以双端一致,如果你使用某个原生渲染框架遇到不一致问题,那只是这个框架厂商做的不好而已。

是的,很遗憾 react native 在跨端组件方面投入不足,官方连 slider 组件都没有,导致本次评测中未提供 react native 下 slider-100 的示例和视频。

4. uni-app x

2022 年,uts 语言发布。2023 年,uni-app x 发布。

uts 语言是基于 typescript 修改而来的强类型语言,编译到不同平台时有不同的输出:

  • 编译到 web,输出 js

  • 编译到 Android,输出 kotlin

  • 编译到 iOS,输出 swift

而 uni-app x,是基于 uts 语言重新开发了一遍 uni-app 的组件、API 以及 vue 框架。

如下这段示例,前端的同学都很熟悉,但它在编译为 Android App 时,变成了一个纯的 kotlin app,里面没有 js 引擎、没有 flutter、没有 webview,从逻辑层到 UI 层都是原生的。

<template>

<view class=\"content\">

<button @click=\"buttonClick\">{{title}}</button>

</view>

</template>

<script> //这里只能写uts

export default {

data() {

return {

title: \"Hello world\"

}

},

onLoad() {

console.log(\'onLoad\')

},

methods: {

buttonClick: function () {

uni.showModal({

\"showCancel\": false,

\"content\": \"点了按钮\"

})

}

}

}

</script>

<style>

.content {

width: 750rpx;

background-color: white;

}

</style>

这听起来有点天方夜谭,很多人不信。DCloud 不得不反复告诉大家,可以使用如下方式验证:

  • 在编译 uni-app x 项目时,在项目的 unpackage 目录下看看编译后生成的 kt 文件

  • 解压打包后的 apk,看看里面有没有 js 引擎或 flutter 引擎

  • 手机端审查布局边界,看看渲染是不是原生的(flutter 和 webview 都无法审查布局边界)

但是开发者也不要误解之前的 uni-app 代码可以无缝迁移。

  • 之前的 js 要改成 uts。uts 是强类型语言,上面的示例恰好类型都可以自动推导,不能推导的时候,需要用: 和 as 声明和转换类型。

  • uni-app x 支持 css,但是 css 的子集,不影响开发者排版出所需的界面,但并非 web 的 css 全都兼容。

了解了 uni-app x 的基本原理,我们来看下 uni-app x 下的 100 个 slider 效果怎么样。

项目 https://gitcode.net/dcloud/test-cross/-/tree/master/test_uniappx_slider_100 下有源码工程和编译好的 apk。

如下视频,打开了 GPU 呈现模式,可以看到没有一条竖线突破那条红色的掉帧安全横线,也就是没有一帧掉帧。

点击查看视频

uni-app x 在 app 端,不管逻辑层、渲染层,都是 kotlin,没有通信问题、没有混合渲染问题。不是达到了原生的性能,而是它本身就是原生应用,它和原生应用的性能没差别。

这也是其他跨平台开发框架做不到的。

uni-app x 是一次大胆的技术突破,分享下 DCloud 选择这条技术路线的思路:

DCloud 做了很多年跨平台开发,uni-app 在 web 和小程序平台取得了很大的成功,不管规模大小的开发者都在使用;但在 app 平台,大开发者只使用 uni 小程序 sdk,中小开发者的 app 会整体使用。

究其原因,uni-app 在 web 和小程序上,没有性能问题,直接编译为了 js 或 wxml,uni-app 只是换了一种跨平台的写法,不存在用 uni-app 开发比原生 js 或原生 wxml 性能差的说法。

但过去基于小程序架构的 app 端,性能确实不及原生开发。

那么 App 平台,为什么不能像 web 和小程序那样,直接编译为 App 平台的原生语言呢?

uni-app x,目标不是改进跨平台框架的性能,而是给原生应用提供一个跨平台的写法。

这个思路的转换使得 uni-app x 超越了其他跨平台开发框架。

在 web 端编译为 js,在小程序端编译为 wxml 等,在 app 端编译为 kotlin。每个平台都只是帮开发者换种一致的写法而已,运行的代码都是该平台原生的代码。

然而在 2 年前,这条路线有 2 个巨大的风险:

  1. 从来没有人走通过

  2. 即便能走通,工作量巨大

没有人确定这个产品可以做出来,DCloud 内部争议也很多。

还好,经历了无数的困难和挑战,这个产品终于面世了。

换个写法写原生应用,还带来另一个好处。

同样业务功能的 app,使用 vue 的写法,比手写纯原生快多了。也就是 uni-app x 对开发效率的提升不只是因为跨平台,单平台它的开发效率也更高。

其实 google 自己也知道原生开发写法太复杂,关于换种更高效的写法来写原生应用,他们的做法是推出了 compose UI。

不过遗憾的是这个方案引入了性能问题。我们专门测试使用 compose UI 做 100 个 slider 滑动的例子,流畅度也掉帧。

源码见:https://gitcode.net/dcloud/test-cross/-/tree/master/test_compose_ui_slider_100, 项目下有打包后的 apk 可以直接安装体验。

打开 GPU 呈现模式,可以看到 compose ui 的 100 个 slider 拖动时,大多数竖线都突破那条红色的掉帧安全横线,也就是掉帧严重。 点击查看视频

既然已经把不同开发框架的 slider-100 应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:

包体积(单位:M)

内存占用(单位:Kb)

flutter

18

141324.8

ArtUI-x

45.7

133091.2

uni-app x

8.5

105451.2

compose ui

4.5

97683.2

包体积数据说明:

  • 包含 3 个 CPU 架构:arm64、arm32、x86_64。

  • flutter 的代码都是编译为 so 文件,支持的 cpu 类型和包体积是等比关系,1 个 cpu 最小需要 6M 体积,业务代码越多,cpu 翻倍起来越多。

  • ArtUI-x 的业务代码虽然写在 js 里,但除了引用了 flutter 外还引用了 js 引擎,这些 so 库体积都不小且按 cpu 分类型翻倍。

  • uni-app x 里主业务都在 kotlin 里,kotlin 和 Android x 的兼容库占据了不少体积。局部如图片引用了 so 库,1 个 cpu 最小需要 7M 体积。但由于 so 库小,增加了 2 个 cpu 类型只增加了不到 1M。

  • compose ui 没有使用 so 库,体积裁剪也更彻底。

  • uni-app x 的常用模块并没有裁剪出去,比如 slider100 的例子其实没有用到图片,但图片使用的 fesco 的 so 库还是被打进去了。实际业务中不可能不用图片,所以实际业务中 uni-app x 并不会比 compose ui 体积大多少。

内存占用数据说明:

  • 在页面中操作 slider 数次后停止,获取应用内存使用信息 VmRSS: 进程当前占用物理内存的大小

  • 表格中的内存数据是运行 5 次获取的值取平均值

  • 自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高

5. 后记

跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。

这些问题在 uni-app x 都不存在,它只是换了一种写法的原生应用。

各种框架

类型

逻辑层与 UI 通信折损

逻辑层与 OS API 通信折损

混合渲染

react native、nvue、weex

flutter

微信 skyline、webF、ArkUI-x

uni-app x

原生应用

当然,作为一个客观的分析,这里需要强调 uni-app x 刚刚面世,还有很多不成熟的地方。比如前文 diss 微信的暗黑模式,其实截止到目前 uni-app x 还不支持暗黑模式。甚至 iOS 版现在只能开发 uts 插件,还不能做完整 iOS 应用。

需求墙里都是 uni-app x 该做还未做的。也欢迎大家投票。

另外,原生 Android 中一个界面不能有太多元素,否则性能会拉胯。flutter 的自渲染和 compose ui 解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在 uni-app x 里对应的是 draw 自绘 API。

uni-app x 这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。

让跨平台开发不如原生,成为历史。

欢迎体验 uni-app x 的示例应用,感受它的启动速度,渲染流畅度。

源码在:https://gitcode.net/dcloud/hello-uni-app-x/; 或者扫描下方二维码下载打包后的 apk 文件:

这个示例里有几个例子非常考验通信性能,除了也内置了 slider-100 外,另一个是 “模版 - scroll-view 自定义滚动吸顶”,在滚动时实时修改元素 top 值始终为一个固定值,一点都不抖动。

我们不游说您使用任何开发技术,但您应该知道它们的原理和差别。

欢迎指正和讨论。

标签: 通信 都是

提交需求或反馈

Demand feedback