- 原文地址:How fast is Flutter? I built a stopwatch app to find out.
- 原文做者:Andrea Bizzotto
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:ALVINYEH
- 校对者:swants、talisk
图片来源: Petar Petkovskihtml
这个周末,我花了点时间去用由谷歌新开发的 UI 框架 Flutter。前端
从理论上讲,它听起来很是棒!android
根据文档,高性能是预料之中的:ios
Flutter 旨在帮助开发者轻松地实现恒定的 60 fps。git
可是 CPU 利用率如何?github
太长了读不下去,直接看评论:不如原生好。你必须正确地作到:编程
setState()
方法,请确保尽量少地从新绘制用户界面。我用 Flutter 框架开发了一个简单的秒表应用程序,并分析了 CPU 和内存的使用状况。后端
图左:iOS 秒表应用。 图右:用 Flutter 的版本。很漂亮吧?bash
UI 界面是由两个对象驱动的: 秒表和定时器。markdown
主界面是这样创建的:
class TimerPage extends StatefulWidget { TimerPage({Key key}) : super(key: key); TimerPageState createState() => new TimerPageState(); } class TimerPageState extends State<TimerPage> { Stopwatch stopwatch = new Stopwatch(); void leftButtonPressed() { setState(() { if (stopwatch.isRunning) { print("${stopwatch.elapsedMilliseconds}"); } else { stopwatch.reset(); } }); } void rightButtonPressed() { setState(() { if (stopwatch.isRunning) { stopwatch.stop(); } else { stopwatch.start(); } }); } Widget buildFloatingButton(String text, VoidCallback callback) { TextStyle roundTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white); return new FloatingActionButton( child: new Text(text, style: roundTextStyle), onPressed: callback); } @override Widget build(BuildContext context) { return new Column( children: <Widget>[ new Container(height: 200.0, child: new Center( child: new TimerText(stopwatch: stopwatch), )), new Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ buildFloatingButton(stopwatch.isRunning ? "lap" : "reset", leftButtonPressed), buildFloatingButton(stopwatch.isRunning ? "stop" : "start", rightButtonPressed), ]), ], ); } } 复制代码
这是如何运做的呢?
setState()
会被调用,而后触发 build()
方法。build()
方法的一部分, 一个新的 TimerText
会被建立。TimerText
类看起来是这样的:
class TimerText extends StatefulWidget { TimerText({this.stopwatch}); final Stopwatch stopwatch; TimerTextState createState() => new TimerTextState(stopwatch: stopwatch); } class TimerTextState extends State<TimerText> { Timer timer; final Stopwatch stopwatch; TimerTextState({this.stopwatch}) { timer = new Timer.periodic(new Duration(milliseconds: 30), callback); } void callback(Timer timer) { if (stopwatch.isRunning) { setState(() { }); } } @override Widget build(BuildContext context) { final TextStyle timerTextStyle = const TextStyle(fontSize: 60.0, fontFamily: "Open Sans"); String formattedTime = TimerTextFormatter.format(stopwatch.elapsedMilliseconds); return new Text(formattedTime, style: timerTextStyle); } } 复制代码
一些注意事项:
TimerTextState
对象所建立。每次触发回调后,若是秒表在运行,就会调用 setState()
方法。build()
方法,并在更新的时候绘制一个新的 Text
对象。当我一开始开发这个 App 时,我管理了 TimerPage
类中对所有状态以及 UI 界面,其中包括了秒表和定时器。
这就意味着每次触发定时器的回调时,会从新构建整个 UI 界面。这是没必要要且低效的:只有包含了过去时间的 Text
对象须要从新绘制 —— 特别是当每 30 毫秒计时器触发一次时。
若是咱们考虑到未优化和已优化的部件树层次结构,这一点就变得更显而易见了:
建立一个独立的的 TimerText
类来封装定时器的逻辑,能够下降 CPU 负担。
换句话说:
setState()
方法,确保尽量少地从新绘制 UI 用户界面。Flutter 官方文档指出该平台对快速分配进行了优化:
Flutter 框架使用了一种功能式流程,这种流程很大程度上取决于内存分配器是否有效地处理了小型,短时间的分配工做。
也许重建一棵部件树不能算做“小型,短时间的分配”。实际上,个人代码优化了致使较低的 CPU 和内存使用率的问题(见下文)。
自从这篇文章发表以来,一些谷歌工程师注意到了这一点,并作出了进一步的优化。
更新后的代码经过将 TimerText
分为了两个 MinutesAndSeconds
和 Hundredths
控件,进一步减小了用户界面的重绘:
进一步的 UI 界面优化(来源:谷歌)。
它们将本身注册为定时器回调的监听器,而且只有状态发生改变时才会从新绘制。这进一步优化了性能,由于如今每 30 毫秒只有 Hundredths
控件会渲染。
我在发布模式下运行了这个应用程序(flutter run --release
):
我在 Xcode 中监控了三分钟的 CPU 和内存使用状况,并测试了三种不一样模式下的性能表现。
在最后一个测试中,CPU 使用状况图密切地追踪了 GPU 线程,而 UI 线程保持地至关稳定。
注意:在低速模式下以相同的基准运行,CPU 的使用率超过了 50%。随着时间的推移,内存使用量也在不断增加。
这可能意味着内存在开发模式下没有被释放。
关键要点:确保你的应用处于发布模式。
请注意,当 CPU 使用率超过 20% 时,Xcode 会报告出一个很是高的电力消耗警告。
我在不断思考这些结果。每秒触发 30 次而且从新渲染一个文本标签的定时器不该该占用 25 %的双核 1.4GHz 的 CPU。
Flutter 应用中的控件树是由声明式范型所构建的,而不是在 iOS 和安卓上的命令式编程模型。
可是,命令模式下性能是否更加好呢?
为了找到答案,我在 iOS 上开发了相同的秒表应用。
这是用 Swift 代码设置了一个定时器,而且每 30 毫秒更新一次文本标签:
startDate = Date() Timer.scheduledTimer(withTimeInterval: 0.03, repeats: true) { timer in let elapsed = Date().timeIntervalSince(self.startDate) let hundreds = Int((elapsed - trunc(elapsed)) * 100.0) let seconds = Int(trunc(elapsed)) % 60 let minutes = seconds / 60 let hundredsStr = String(format: "%02d", hundreds) let secondsStr = String(format: "%02d", seconds) let minutesStr = String(format: "%02d", minutes) self.timerLabel.text = "\(minutesStr):\(secondsStr).\(hundredsStr)" } 复制代码
为了完整性,这是我在 Dart 中使用的时间格式代码(优化方案 1):
class TimerTextFormatter { static String format(int milliseconds) { int hundreds = (milliseconds / 10).truncate(); int seconds = (hundreds / 100).truncate(); int minutes = (seconds / 60).truncate(); String minutesStr = (minutes % 60).toString().padLeft(2, '0'); String secondsStr = (seconds % 60).toString().padLeft(2, '0'); String hundredsStr = (hundreds % 100).toString().padLeft(2, '0'); return "$minutesStr:$secondsStr.$hundredsStr"; } } 复制代码
最后结果如何?
Flutter. CPU:25%,内存:22 MB
iOS. CPU:7%,内存:8 MB
Flutter 实现方式在 CPU 的使用状况超过了 3 倍以上,内存上也一样是 3 倍之多。
当定时器中止运行时,CPU 的使用率回到了 1%。这就证明了所有 CPU 的工做都用于处理定时器的回调和从新绘制 UI 界面。
这并不足以让人惊讶。
Text
控件。UILabel
的文本。“嘿!” —— 我听到你说的。“可是时间格式的代码是不一样的!你怎么知道 CPU 使用率的差别不是由于这个?”
那么,咱们不进行格式去修改这两个例子:
Swift:
startDate = Date() Timer.scheduledTimer(withTimeInterval: 0.03, repeats: true) { timer in let elapsed = Date().timeIntervalSince(self.startDate) self.timerLabel.text = "\(elapsed)" } 复制代码
Dart:
class TimerTextFormatter { static String format(int milliseconds) { return "$milliseconds"; } } 复制代码
最新结果:
Flutter. CPU:15%,内存:22 MB
iOS. CPU:8%,内存:8 MB
Flutter 的实现仍然是 CPU-intensive 的两倍。此外,它彷佛在多线程(GPU,I/O 工做)上作了至关多的事情。但在 iOS 上,只有一个线程是处于活动状态的。
我用一个具体的案例来对比了 Flutter/Dart 和 iOS/Swift 的性能表现。
数字是不会说谎的。当涉及到频繁的 UI 界面更新时候,鱼和熊掌不可兼得。 🎂
Flutter 框架让开发者用一样的代码库为 iOS 和安卓开发应用程序,像热加载等功能进一步提升了开发效率。但 Flutter 仍然处于初期阶段。我但愿谷歌和社区能够改进运行时配置文件,更好地将好处带给终端用户。
至于你的应用程序,请务必考虑对代码进行微调,以减小用户界面的重绘。这份努力是值得。
我将这个项目的全部代码托管在这个 GitHub 仓库,你能够本身来运行一下。
不用客气!😊
这个样品项目是我第一次使用 Flutter 框架的实验。若是你知道如何编写更优雅的代码,我很乐意收到你的评论。
关于我: 我是一个自由职业的 iOS 开发者,同时兼顾在职工做,开源,写小项目和博客。
这是个人推特:@biz84。GiHub 主页:GitHub。欢迎一切的反馈,推文,有趣的资讯!想知道我最喜欢什么?许多的掌声 👏👏👏。噢,还有香蕉和面包。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。