Products
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 的框架到底哪里不如原生?
react native、weex 等抛弃 webview,改由原生渲染的跨平台方案,2014 年就推出了。 如今手机硬件也越来越好了,为什么性能还达不到原生?
js + 原生渲染的方案主要有 2 点缺陷:
JS 引擎自身的性能问题
JS 和原生之间的通信延迟
所以很多开发者即便使用这类方案,首页也还是原生来写。
React Native 的 Hermes 引擎和华为的 arkUI,提供了 js 编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。
弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。
以数字运算为例,js 的 number 运算确实比强类型的 int 慢,内存开销也更大。
每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。
逻辑层的 js,即要和原生渲染层通信,还要和原生 API 通信:
举个简单的场景例子,在 js 里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。
如果全部在 webview 里,js 操作 ui 还好一些,所以 uni-app 的 app-vue 里的 renderjs 操作 UI 性能高,就是这个道理。同理还有微信小程序的 wsx。
虽然小程序和 uni-app 都是 js,但实际上逻辑层在独立 js 引擎里,通过原生桥来控制 web-view,通信成本很高。
weex 提供了 bindingx 技术,这是一种弱编程,渲染层预先定义了一些操作 UI 的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在 js 里高性能、自由的操作所有 UI。
操作系统和三方 SDK 的 API 都是原生的,js 调用这些能力也需要跨语言通信。比如 js 调用原生的 Storage 或 IO,数据较多时遍历的性能非常差。
当然在 js API 的封装上可以做些优化,比如微信的 storage 提供了 wx.batchGetStorageSync 这种批量读取的 API,既然遍历性能差,那干脆一次性从原生读出来再传给 js。
这也只能是无奈的方案,如果在遍历时想用 js 做什么判断就实现不了了,而且一次性读出很大的数据后传给 js 这一下,也需要通信时间。
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 看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?
别忘了上面 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 并渲染
1k 数据从原生读到 dart 并渲染再写回
0.1k 数据从原生读到 dart 并渲染
0.1k 数据从原生读到 dart 并渲染再写回
通信损耗非常明显。并且数据量从 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 通信仍然需要跨语言通信。
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 的混合渲染问题很多。
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 的示例和视频。
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 个巨大的风险:
从来没有人走通过
即便能走通,工作量巨大
没有人确定这个产品可以做出来,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 次获取的值取平均值
自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高
跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。
这些问题在 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