
9 月 26 日,我们开始用 Flutter 构建 Cliqer Remote,这是我们为演讲者打造的手机应用。五天后,它已上线 TestFlight 和 Google Play 内部测试轨道,具备网页版演讲者端的全部功能:演示遥控器、实时屏幕、墨迹标注、聊天、备注和通话。它能用。随后我们在真实手机上进行测量,眼看其中一部仅仅显示共享屏幕,机身表面温度就达到了 42 °C,于是我们把它扔掉了。
团队以外没有任何人见过那个应用。您现在得到的是在每个平台上都原生的应用:iPhone、iPad 和 Apple Watch 上用 SwiftUI,Android 和 Wear OS 上用 Kotlin 与 Jetpack Compose,底层则是同一个引擎,用 Rust 只写一次。本文讲述背后的原因,并附上我们一路测得的数据。
- 演示遥控器作为手机应用,承担的任务很特别:在温热的手中握上一小时,显示屏幕的实时视频,而且不能漏掉任何一次点按。帧时间、发热和体积,比对大多数应用都更重要。
- 我们的 Flutter 构建写起来快,看起来也漂亮,但其玻璃效果和视频路径每一帧都要消耗 GPU。在 Galaxy S24+ 上,我们把一个界面每帧的 GPU 工作量从 19.8 ms 降到 5.7 ms,却仍在与引擎较劲,而不是在打造产品。
- 随后我们把三个构建装在同一部手机上,在同一个房间里,各自观看同一个动态屏幕十分钟。第一版 Flutter 构建每秒绘制 47.6 帧,让手机升温 5.9 °C,耗电 168 mAh。原生应用完全没有绘制任何 UI 帧,升温 0.2 °C,耗电 86 mAh:电量减半。
- 原生应用更小:内含 Apple Watch 应用的 iPhone 应用为 10.8 MB(Flutter 版为 27.6 MB),Android 下载大小为 5.8 MB(Flutter:15.6 MB),测量方法相同。
- 同一轮优化中,我们还让桌面应用把点按转化为翻页的速度提升了十倍:在 Mac 上,PowerPoint 中为 48 ms,Keynote 中为 42 ms,而此前约需半秒。

遥控器是一种特别的应用
大多数应用都是短时间使用的。演示遥控器则不然。它在整场演讲中都握在一只手里,常常处于舞台灯光下,屏幕常亮,无线电全程繁忙。在 Cliqer 上,它还会显示演讲者屏幕的实时画面、在幻灯片上绘制墨迹、承载聊天和备注,并在主持人邀请时让演讲者出镜。
这样的任务描述定下了三条规则:
- 每次点按都必须立即生效。迟到的点按意味着演讲者在观众面前沉默地干站着。
- **手机必须保持凉爽。**手机发热会降频,降频的手机会掉帧、视频解码延迟、耗电更快。而且握上一小时也很不舒服。
- **应用必须小巧、启动迅速。**人们往往是在会场里、在上台前几分钟、通过会议 Wi-Fi 安装它。
对大多数应用来说,Flutter 三点都能做到。而我们的应用恰好属于代价大于收益的那一类。
Flutter 做对了什么
该肯定的要肯定。Flutter 让我们在五天内从一个空文件夹做出了在两大应用商店上架的全功能应用:一套代码库、热重载、出色的测试体系,界面在各处都一模一样。我们为它写了 266 个测试。如果您要做的是表单与列表类应用、仪表盘或商城,我们依然会毫不犹豫地推荐它。
我们也从中学到了很多。本文几乎每一项发现都来自在真实手机上追踪那个 Flutter 构建,其中一些修复如今已应用在每个 Cliqer 客户端中,包括网页版。
痛点:玻璃效果每帧都要复制整个屏幕
Cliqer 的外观是磨砂玻璃:半透明面板会模糊其后方的内容。在 iPhone 和 iPad 上这很省资源。但在我们的 Android 测试机,一部配备 120 Hz 屏幕的 Galaxy S24+ 上,情况并非如此。在 120 Hz 下,每一帧从头到尾只有 8.3 ms 的绘制时间。
我们在设备上追踪 GPU,发现问题出在渲染引擎读取背景的方式:每个玻璃面板每一帧都会复制整个屏幕,还叠加了多重采样。加入界面有九个玻璃面板,也就是每帧九次全屏复制。这部手机的 GPU 是桌面级设计,不是分块渲染架构:它没有低成本的方式回读刚刚绘制的内容,而每出现一种新的纹理尺寸,就意味着一次新的内存分配,且内核要先将其清零。在一次追踪中,栅格线程 71% 的时间都花在了分配图形内存上。

我们逐个界面地修复。我们换用了引擎的预发布版本,使模糊效果可以限定在其面板范围内(从 41 ms 降到 17 ms)。我们把加入界面上的九个面板合并为一次共享读取(从 19.8 ms 降到 5.7 ms)。我们不再为任何玻璃表面的尺寸添加动画,因为每个新尺寸都意味着一次新的分配,改用固定尺寸的图层来模拟动画。二维码扫描器最差的一帧从 285 ms 降到了 18 ms。
这些修复每一项都是在绕开引擎的问题,而不是为演讲者打造的功能。而且在房间界面中,玻璃效果占了 80% 至 90% 的 GPU 时间,我们始终离再次掉帧只差一个新面板。
真正的痛点:视频及其带来的发热
共享屏幕是应用显示的最重的内容。在 Flutter 构建中,每一帧解码后的视频都以纹理形式交给 UI,这意味着 UI 会为每一帧视频重绘自身:房间、玻璃、控件,全部重绘,每秒 45 次,只为了移动一个矩形里的像素。
10 月 1 日,我们首次在 Galaxy 上用系统追踪测量了这一点。视频播放时,GPU 维持在 440 MHz 左右。切换幻灯片时,帧耗时 25 至 33 ms,GPU 在一半以上的过渡时间里都顶在 800 MHz 上限。手机处于系统的「中度」热状态,机身表面温度达到 42 °C,并且在自行限制 GPU 来应对。发热还让我们的测量失真:温热的手机测得的速度比凉爽时慢约 20%,所以每次对比都必须在相同温度下交替测试各个构建。
然后我们做了那个显而易见的实验:把视频放到由操作系统自己合成的原生图层上,置于 UI 之外。同一部手机、同一个应用、同一个房间:UI 不再为视频重绘,GPU 频率从 440 MHz 降到 121 MHz,切换幻灯片时的那一帧从 25 至 33 ms 降到 3 至 7 ms。我们在 10 月 1 日就把这个修复发布到了 Flutter 应用中。它也让我们看清了真正的答案在哪里:在原生应用中,无论 iPhone 还是 Android,视频始终位于系统图层上,所以这一整类问题从架构上就不存在,而不是靠变通方法解决。
同一部手机,同一个房间,十分钟
关于发热的说法很容易张口就来,所以我们做了测量。10 月 5 日,我们在同一部 Galaxy S24+ 上依次运行三个构建,每个十分钟,不接电源,在同一个房间里观看同一个动态共享屏幕。每次运行前都让手机重新冷却下来。
- Flutter 第一版构建:我们最初构建的应用,视频由 UI 绘制。
- Flutter 调优版:经过上述玻璃和视频修复后的同一应用。
- 原生版:全新的 Cliqer Remote。

第一版 Flutter 构建仅仅为了显示视频,就每秒重绘整个界面 47.6 次,GPU 平均频率保持在 366 MHz,每秒消耗 1.47 秒 CPU 时间。原生应用在十分钟内没有绘制任何一帧界面:只有视频图层在动。它的 GPU 平均频率为 129 MHz,CPU 为每秒 0.69 秒,耗电 86 mAh,而前者为 168 mAh。同样的任务,电量减半。

再看发热。十分钟内,第一版 Flutter 构建让电池温度从 36.2 °C 升至 42.1 °C,手机开始自行降频。调优后的 Flutter 构建升温 1.9 °C。原生应用升温 0.2 °C。一场演讲远不止十分钟,而这种差异您握在手里就能感觉到。
公平地说,调优后的 Flutter 构建已经相当接近。我们在 Flutter 内部所做的修复消除了大部分开销,原生版的领先幅度较小:CPU 少 12%,耗电少 25%。关键在于各自的起点。第一版构建代表了一款优秀 Flutter 应用开箱即用的样子;原生版无需任何变通方法就能达到凉爽的效果。
我们的测量方法如下,方便您复现:设备端 Perfetto 追踪(帧时间线、GPU 频率、电池计数器)、来自内核的进程 CPU 时间,以及手机自身的电池温度,每次运行时共享屏幕上都播放相同的 60 fps 动画。每个构建只用一部手机、运行一次,所以请把这些数据看作一次严谨的实地测试,而非实验室基准测试。原生版运行开始时手机温度还低了约 2 °C,这也是我们比较升温幅度的原因。
我们改用了什么
全新的 Cliqer Remote 在每个平台上都是原生应用,构建在同一个共享引擎之上。
**界面在每个平台上都是原生的。**在 iPhone 和 iPad 上采用 SwiftUI,配合系统自带的 Liquid Glass。在 Android 上采用 Jetpack Compose,在平台允许的情况下使用真正的背景模糊。Apple Watch 和 Wear OS 应用同样是原生的,小巧到足以把「下一页」和「上一页」戴在您的手腕上。
**引擎只写一次,用 Rust。**加入房间、在网络变化后重新连接并恢复、接收屏幕视频、发送点按、激光笔和墨迹、聊天、备注、计时器、通话,以及将手机与电脑配对,全部位于一个由两个应用共享的 Rust 库中。每个应用只负责绘制界面、用系统硬件解码器解码视频,以及调用摄像头和麦克风。一次协议修复只需落地一次,就能同时发布到两个平台,两个应用也不会各行其是。
Rust 是这个引擎的不二之选:没有垃圾回收器让帧暂停,无需运行时即可保证内存安全,而且能为 iOS 和 Android 编译成同一个小巧的库。在关键之处它非常快。在 Mac 上,我们的引擎打包一个 20 KB 的 H.264 帧只需 31 µs,每一端完成一次安全握手只需 198 µs;在我们的测试中,把一个 100 KB 的视频帧交给 Android 界面只需 46 µs(在我们优化这一边界之前需要 440 µs)。在我们的测试中,经由中继的一次往返中位数为 13 至 16 ms。
而且更小

我们用相同的方法测量了两种构建。上传至 Apple 的 iPhone 应用从 27.6 MB 降到 10.8 MB,而且这还包含了其中的 Apple Watch 应用(1.5 MB)。Android 手机的下载大小,使用 Google 官方的 bundle 工具针对同一部 64 位手机测量,从 15.6 MB 降到 5.8 MB。当有人在开场前五分钟用会场 Wi-Fi 安装应用时,只有原来三分之一左右的体积就显得尤为重要。
对毫秒的追求不止于手机
更快的手机应用只是一次点按的一半。另一半是运行演示文稿的电脑,在重建应用的同时,我们也着手优化了这一端。
点按:从半秒到十分之一秒
当点按到达 Cliqer 桌面应用时,它必须先翻动演示文稿,再告诉房间里的每个人当前显示的是哪一张幻灯片。过去,第二步需要在每次点按后向演示软件询问幻灯片编号,而这个询问很慢。

Cliqer 2.3 与 PowerPoint 或 Keynote 保持一个常开连接,并直接从刚执行的操作中读取当前位置。在 Mac 上运行 20 次,从点按到翻页的中位数时间在 PowerPoint 中从 509 ms 降到 48 ms,在 Keynote 中从 408 ms 降到 42 ms。连续快速按十次,也就是演讲者跳过某一部分时的操作,过去需要 5.3 秒才能稳定下来。现在它们会合并为一次移动,在 254 ms 内完成。
在糟糕的 Wi-Fi 下也能跟上的视频
今年早些时候,我们找到了共享屏幕在 Wi-Fi 下可能冻结数秒的原因。Cliqer 中的屏幕视频通过可靠数据通道传输,而该通道所基于的标准在重传丢失的数据包前至少要等待整整一秒,且每次重试等待时间翻倍。Chrome 使用的是 400 ms。丢失帧之后的每一帧都在排队等待,主机随后又把一次全量刷新塞进同一个队列,在慢速链路上这种情况一再发生。

我们把重传计时器调整为 Chrome 的数值,并阻止刷新排在积压数据之后。在采用 Wi-Fi 丢包模型的网络模拟中,一路 1080p 共享每两分钟内持续一秒或更长的冻结从 15 至 22 次降到 0 至 2 次,相对直播画面的中位延迟从最高 2.3 秒降到 12 ms。我们还将全量刷新限制为每路流每半秒最多一次,因为每一次刷新都会发送给每一位观看者:在设置这一上限之前,一位观看者每秒可能收到 19 次全量刷新,约 40 Mbit/s。
在手机端,Galaxy 的硬件视频解码器大约每秒一次会把一帧普通画面滞留约 60 ms。让解码器以全速运行后,50 秒内迟到帧降为零。这一修复已包含在原生 Android 应用中。
给其他团队的建议
如果您在手机上构建任何实时应用,下面这些经验我们花了好几天才学到,而您只需几分钟就能用上:
- **在最差的真实设备上测量,而不是在模拟器上。**我们的模拟器以 60 Hz 运行在不同的图形后端上,一个玻璃问题都显现不出来。
- **在相同温度下交替测试各个构建。**温热的手机测得的速度慢约 20%。拿冷机运行和热机运行作比较,结果会骗人。
- **统计您的模糊读取次数。**每个背景模糊都可能让每帧多一次全屏复制。把它们合并、限定范围,并且永远不要为模糊表面的尺寸添加动画。
- **永远不要为视频重绘 UI。**把视频放到由系统合成的图层上。这是我们测得的在 GPU、电量和发热方面最大的一项改进。
- **要求硬件解码器以实时速度运行。**在 Android 上,设置解码器的 operating rate。一行代码就消除了每秒一次的 60 ms 卡顿。
- **检查传输层的计时器。**可靠数据通道沿用了旧标准中的重传计时器。对实时视频来说,最少一秒的等待简直是永恒。
- **把全量刷新当作昂贵的帧来对待。**按每路流限制其频率,因为每一次都会发送给每一位观看者。
- **合并突发输入。**按十次应该是一次移动十步,而不是十次往返。
- **查看您的工具往构建里写了什么。**某个预发布工具链未经询问就在我们的 Android 清单中开启了一个渲染标志。
这对您意味着什么
10x
在 Mac 上的 PowerPoint 和 Keynote 中,点按到翻页的速度提升
5.8 MB
Android 下载大小,约为原来的三分之一
10.8 MB
内含 Apple Watch 应用的 iPhone 应用
Half
相比第一版 Flutter 构建的耗电量,同一部手机,同样 10 分钟
Cliqer Remote 已在 iPhone、iPad、Android、Apple Watch 和 Wear OS 上原生运行。点按更快,手机保持凉爽,耗电只有我们第一版构建的一半,视频以手机设计时的方式绘制,应用体积约为原来的三分之一。网页版演讲者端依然可以在任何浏览器中使用,完全无需安装,因此您房间里的任何人都不会被落下。
常见问题
Cliqer Remote 要在一部被演讲者握在手里一小时的手机上,显示实时视频、磨砂玻璃效果和演示遥控器。在 Flutter 构建中,UI 会为每一帧视频重绘,每个玻璃面板每帧都要复制整个屏幕,这在 Android 上消耗了 GPU 时间并带来发热。原生应用从架构上就在系统图层上绘制视频,而且下载大小只有原来的三分之一左右。
在我们于 Galaxy S24+ 上进行的测试中,播放十分钟实时屏幕视频,原生应用让手机升温 0.2 °C,而我们第一版 Flutter 构建为 5.9 °C。原生应用在视频播放期间不绘制任何界面帧,耗电 86 mAh,而后者为 168 mAh。
不是。Flutter 让我们在五天内就做出了在两大应用商店上架的完整应用,而且经过我们的调优后,原生应用的耗电只比它少 25%。对大多数应用来说,它都是绝佳的选择。我们的应用结合了持续的实时视频、大量模糊玻璃表面,以及在温热手机上的长时间使用,正是这种情况下它的渲染模型让我们付出了最大的代价。
它负责绘制之外的一切:加入和重新连接房间、接收共享屏幕、发送点按、激光笔和墨迹、聊天、备注、计时器、通话,以及将手机与电脑配对。iPhone 和 Android 应用共享这一核心,因此一次修复会同时作用于两者。
在装有 Cliqer 2.3 的 Mac 上,桌面应用将一次点按转化为翻页的中位时间在 PowerPoint 中为 48 ms,在 Keynote 中为 42 ms,基于 20 次运行测得。网络还会额外增加自身的往返时间,在我们的中继测试中其中位数为 13 至 16 ms。
不需要。网页版演讲者端可在任何现代浏览器中使用,无需安装。应用则额外提供原生二维码扫描器、用于控制主机的 Face ID 或生物识别、保持常亮的屏幕,以及 Apple Watch 和 Wear OS 上的演示遥控器。
继续阅读
更多产品动态,助力您的下一场活动
赶在您的下一场活动之前,抢先获取下一篇指南
为演讲者、AV 团队和活动团队提供实用指南,以及真正重要的 Cliqer 版本更新。绝无垃圾邮件,一键即可退订。


