选型分析:深入分析App主流各大跨平台框架
共 10826字,需浏览 22分钟
·
2024-05-06 08:05
前言
App 跨平台框架历史悠久,从 cordova、react native、flutter,到最近的 uni-app x,以及鸿蒙的 ArkUI-X 等。各种框架的渲染方式不同,大概可以分为以下几类:
上表中 uni-app x 和原生应用渲染方式类似,逻辑层和渲染层都是原生,都是强类型;他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。
本文盘点一下各种渲染方式的技术方案和性能表现的差异,看一看当今跨平台渲染与原生渲染还有多大差距
1. JS+原生渲染
React Native 等跨平台方案虽然采用了原生渲染来替代 WebView,但性能上仍然无法完全达到原生应用的标准。这主要是由于JS与原生渲染之间的差异和通信开销导致的。
JS 启动和运行速度相对较慢
尽管 React Native 的 Hermes 引擎以及华为 ArkTS 都通过 JS 编译成字节码,对 JS 性能进行了优化,但 JS 这种弱类型在编译期的优化效果有限,仍然无法像强类型语言那样直接深入底层。因此运行时性能还是不如原生。例如 js 的 number 运算确实比强烈性的 int 慢,内存开销也更大。
因此,许多开发者会选择使用原生代码来编写应用的首页等体验关键的场景,以获得更好的性能。
JS 与原生之间的通信存在卡顿
由于 JS 和原生语言各自拥有独立的内存空间,跨语言通信会产生一定的开销。这种通信开销可能导致应用在频繁通信时出现卡顿现象。特别是在处理UI更新和调用原生API时,通信成本尤为显著。
-
JS 与原生UI的通信:
在跨平台开发中,实现 JS 实时调整 UI 元素是一个具有挑战性的任务。例如在监听列表滚动事件并根据滚动位置动态调整界面元素高度这类场景中 JS 与 原生UI之间的通信会导致卡顿。
需要注意,如果所有 UI 操作都在 WebView 内部完成,JS 闭环逻辑和渲染性能也还是不错的。但是有的场景,例如小程序和 uni-app 虽然逻辑层都是 JS,但其实在一个独立的 JS 引擎里,仍然通过平台的bridge控制WebView 渲染 UI,成本很高
-
JS 操作原生 API:
即使不依赖原生渲染,但是当JS需要调用操作系统的原生API或第三方SDK时,同样需要跨语言通信。这也会导致在处理大量数据时性能下降。
虽然有一些框架对 API 调用进行了优化,例如微信小程序为 storage 提供的 wx.batchGetStorageSync 这种批量读取 API 减少调用次数,但仍然无法完全消除通信开销,而且降低了API 的灵活性。
2. Flutter
Flutter,自2018年发布以来,成功统一了逻辑层和渲染层,并采用了强类型语言 Dart。其独特的优势在于使用 Dart驱动的渲染引擎,从而消除了逻辑层与UI层之间的通信延迟。这意味着,使用 Dart 编写的代码在操作UI时,无需再担心性能损耗。
因此,像 bindingx 或 wxs 这样的补丁方案在 Flutter 中变得不再必要。此外,由于 Dart 是强类型语言,其编译优化效果出色,使得Flutter的启动和运行速度都超过了JS。
为了证明 Flutter 在处理大量UI操作时的高性能,我们提供了一个开源项目示例 https://gitcode.net/dcloud/test-cross/-/tree/master/test_flutter_slider_100
其中包含了 100个slider 同时滑动的示例。你可以看到即使在同时操作 100个slider ,Flutter 依然能够保持流畅的滑动体验,这得益于 Dart 与 UI 层之间的无缝连接。
尽管 Flutter 在逻辑和渲染层面展现出了卓越的性能,但它为何没有成为主流选择呢?许多大型企业在初步尝试后,为何选择不再扩大其使用范围?
Dart与原生API的通信
尽管 Flutter 在逻辑层和渲染层都使用了 Dart,但在调用原生 API 时,仍然需要进行跨语言通信,会存在一定的性能损耗。为了测试这种通信耗时。
https://gitcode.net/dcloud/test_flutter_message_channe
该项目主要测试原生数据与Dart之间的通信性能。
在这个项目中,我们首先在 Kotlin 中创大量数据类,然后将这些数据传递到 Dart 层进行渲染,并再将结果写回到原生层。为了更直观地了解通信耗时,我们提供了 0.1k 和 1k 两种数据量,并设计了读和读并写两个按钮,每个按钮都会进行 1000次 循环测试。
在测试过程中,我们将手机上的所有进程关闭,以确保测试结果的准确性。
从测试结果来看,跨语言通信的损耗确实存在,并且数据量的大小对通信时间的影响并不如预期那样显著。数据量从 1k 降到 0.1k 时,通信时间并没有减少 10 倍之多
测试Case | 测试结果 |
---|---|
1k数据从原生读到dart并渲染 | |
1k数据从原生读到dart并渲染再写回 | |
0.1k数据从原生读到dart并渲染 | |
0.1k数据从原生读到dart并渲染再写回 |
这是因为 在Dart与Kotlin之间的通信过程中,数据需要先被序列化成字符串,然后再从原生层传递到Dart层。在Dart层,这些数据需要被重新构造才能使用。这种序列化和反序列化的过程会导致一定的性能损耗,而且这个过程是无法避免的。
相比之下,uni-app x在处理跨语言通信时采用了不同的策略,它使用的编程语言 uts 在 Android 平台上直接编译为 Kotlin,从而避免了跨语言通信的开销。这使得 uni-app x 在调用原生API时能够保持更高的性能。
<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 写的,系统 API及渲染层是 ObjectC,js 调用原生时,可以进行共享内存优化,但某些复杂对象也还是无法直接传递指针来共享内存。而 Android,不管 java 还是 kotlin,和 v8、dart 通信仍然需要跨语言通信。
Flutter 自渲染和原生渲染的并存问题
Flutter的 自渲染引擎在技术上确实是一个亮点,它为开发者提供了统一的逻辑层和渲染层,并通过强类型语言Dart实现了高效的性能。然而,在实际应用中,这个自渲染引擎也带来了一些生态兼容上的问题。
许多第三方软件和 SDK 都是基于原生渲染的,当它们与Flutter的自渲染共存时,就会遇到各种问题。一个典型的例子就是输入法。
输入法通常是由原生UI实现的,当它与 Flutter 的自绘UI并存时,就会出现各种兼容性问题,如输入框被遮挡、窗体 resize 适应等。由于输入法种类繁多,适配起来非常困难。
除了输入法,混合渲染还涉及到信息流广告、地图、图表、动画等众多第三方SDK。这些SDK在与Flutter的自渲染结合时,可能会导致内存占用高、渲染帧率下降、不同渲染方式下的字体不一致、暗黑主题不一致、国际化支持问题、无障碍访问问题以及UI自动化测试的不一致等问题。
Flutter官方也承认这些问题,并提供了两种解决方案:混合集成模式和虚拟显示模式。然而,这两种模式在渲染速度、内存占用、版本兼容以及键盘交互等方面都各有其局限性。开发者可以在Flutter的官方文档中找到更详细的信息:https://docs.flutter.dev/platform-integration/android/platform-views#performance
。
在各大厂 App 中,微信的小程序首页是为数不多的使用 flutter UI 的界面,已经上线1年以上。下面是微信8.0.44(此刻最新版)的入小程序首页。
在视频演示中,我们可以看到当手机切换到暗黑主题时,Flutter应用的UI仍然保持白色,而Flutter的父容器原生View已经变为黑色。这意味着Flutter在黑色背景上重新绘制了一个白色界面,这种体验显然是不佳的。
即使是一个相对简单的小程序首页界面,如没有输入框以避免混合渲染问题,当点击搜索图标跳转到原生渲染的黑色界面时,问题依然存在。如果此界面再内嵌一个原生的信息流SDK,用户将看到白色UI中的信息流广告呈现为黑底,这种视觉冲突更是难以接受。
这并不意味着Flutter无法实现暗黑主题。事实上,重新启动应用后,界面会变为黑色。但这里的关键问题是Flutter自渲染引擎与原生渲染之间的不一致性,它导致了各种渲染和兼容性问题。
注:Android 收集可以通过开发者选项 “GPU呈现模式分析”来识别页面是否是 Flutter,使用 Flutter 开发的 UI 无法显示布局边界。
值得注意的是,Flutter所面临的混合渲染问题在其他使用原生渲染的跨平台开发框架中并不存在,如 React Native、Weex或uni-app x。
综上,虽然 Flutter 在逻辑层和UI层的交互中实现了无通信折损的高效性能,但在逻辑层的Dart与原生API通信以及自绘UI与原生UI的混合渲染方面,仍然面临一些挑战和限制。
3. JS+Flutter 渲染
除了之前提到的原生通信和混合渲染问题,Flutter 还面临着 Dart 生态、热更新以及嵌套写法的挑战。针对这些问题,一些厂商选择将 Flutter 的 Dart 引擎替换为 JS 引擎,如 微信 Skyline、鸿蒙ArkUI-x。尽管这种做法似乎回到了 React Native 和 Weex 的老路,但它确实解决了Dart生态、热更新和嵌套写法等问题。
然而,这种做法也带来了一些新的挑战。首先,使用 JS 引擎意味着操作 UI时需要再次进行通信,这与 Flutter 的初衷相悖。其次,引入JS运行时引擎可能会增加应用的复杂性和资源消耗。
为了缓解这些问题,一些厂商如微信在 Skyline 中推出了补丁技术,如 Worklet 动画。这种技术允许将部分代码运行在UI层,从而减少通信开销。此外,微信的通信策略还包括跨进程通信,这进一步增强了其解决方案的灵活性。
这个项目 https://gitcode.net/dcloud/test-cross/-/tree/master/test_arkuix_slider_100
, 使用ArkUI-x做了100个slider
在上述视频中,我们可以看到当手指按下其中一个slider时,其他99个通过数据通讯指挥跟随行动的slider并没有实现同步,且界面出现了掉帧现象。尽管 Flutter 的自渲染引擎在某些方面表现出色,但这里展现的问题却不容忽视。
很多人对Flutter的自渲染寄予厚望,但实际上当Flutter的UI与原生组件混合渲染时,可能会出现不一致和性能下降的情况。
对比 js+Flutter渲染 与 js+原生渲染 两种方案,我们会发现它们都存在 js 的弱类型、逻辑层和渲染层的通信问题、以及原生API通信问题。
有观点认为,原生渲染在iOS和Android双端实现一致性很困难,而自渲染则没有这个问题。但实际上,只要使用合适的原生渲染框架,并在双端进行充分的测试和适配,完全可以实现一致性的渲染效果。
只是某些跨平台开发框架在跨端组件方面的投入可能不足,导致无法在不同平台使用效果一致的组件,甚至像 React Native 连 slider 组件都没有,所以本次评测中也没有提供 RN 的 slider 示例。
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个巨大的风险:
-
从来没有人走通过 -
即便能走通,工作量巨大
没有人确定这个产品可以做出来,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应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:
包体积数据说明
-
包含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 都不存在,它只是换了一种写法的原生应用。
当然,作为一个客观的分析,这里需要强调 uni-app x 刚刚面世,还有很多不成熟的地方。比如前文diss微信的暗黑模式,其实截止到目前 uni-app x 还不支持暗黑模式。甚至 iOS 版现在只能开发 uts 插件,还不能做完整 iOS 应用。
另外,原生 Android中一个界面不能有太多元素,否则性能会拉胯。flutter 的自渲染和 compose ui 解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在 uni-app x 里对应的是 draw 自绘 API。
uni-app x 这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。
让跨平台开发不如原生,成为历史。
❝转载自:DCloud
「点击关注,Carson每天带你学习一个Android知识点。」
最后福利:学习资料赠送
-
福利:本人亲自整理的「Android学习资料」 -
数量:10名 -
参与方式:「点击右下角”在看“并回复截图到公众号,随机抽取」
点击就能升职、加薪水!