- 原文地址:How fast is Flutter? I built a stopwatch app to find out.
- 原文作者:Andrea Bizzotto
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:ALVINYEH
- 校對者:swants、talisk
Flutter 到底有多快?我開發了秒錶應用來弄清楚。
圖片來源: Petar Petkovski
這個週末,我花了點時間去用由谷歌新開發的 UI 框架 Flutter。
從理論上講,它聽起來非常棒!
根據文件,高效能是預料之中的:
Flutter 旨在幫助開發者輕鬆地實現恆定的 60 fps。
但是 CPU 利用率如何?
太長了讀不下去,直接看評論:不如原生好。你必須正確地做到:
- 頻繁地重繪使用者介面代價是很高的。
- 如果你經常呼叫
setState()
方法,請確保儘可能少地重新繪製使用者介面。
我用 Flutter 框架開發了一個簡單的秒錶應用程式,並分析了 CPU 和記憶體的使用情況。
圖左:iOS 秒錶應用。 圖右:用 Flutter 的版本。很漂亮吧?
實現
- 使用者可以通過點選這兩個按鈕來啟動、停止和重置秒錶。
- 每當秒錶開始計時時,都會建立一個週期性定時器,每 30 毫秒回撥一次,並更新 UI 介面。
主介面是這樣建立的:
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 負擔。
換句話說:
- 頻繁地重繪 UI 使用者介面代價很高。
- 如果經常呼叫
setState()
方法,確保儘可能少地重新繪製 UI 使用者介面。
Flutter 官方文件指出該平臺對快速分配進行了優化:
Flutter 框架使用了一種功能式流程,這種流程很大程度上取決於記憶體分配器是否有效地處理了小型,短期的分配工作。
也許重建一棵部件樹不能算作“小型,短期的分配”。實際上,我的程式碼優化了導致較低的 CPU 和記憶體使用率的問題(見下文)。
更新至 19–03–2018
自從這篇文章發表以來,一些谷歌工程師注意到了這一點,並做出了進一步的優化。
更新後的程式碼通過將 TimerText
分為了兩個 MinutesAndSeconds
和 Hundredths
控制元件,進一步減少了使用者介面的重繪:
進一步的 UI 介面優化(來源:谷歌)。
它們將自己註冊為定時器回撥的監聽器,並且只有狀態發生改變時才會重新繪製。這進一步優化了效能,因為現在每 30 毫秒只有 Hundredths
控制元件會渲染。
基準測試結果
我在釋出模式下執行了這個應用程式(flutter run --release
):
- 裝置: iPhone 6執行於iOS 11.2
- Flutter 版本:0.1.5 (2018年2月22日)。
- Xcode 9.2
我在 Xcode 中監控了三分鐘的 CPU 和記憶體使用情況,並測試了三種不同模式下的效能表現。
未優化的程式碼
- CPU 使用率:28%
- 記憶體使用率:32 MB (App啟動後的基準線為 17 MB)
優化方案 1(獨立的定時文字控制元件)
- CPU 使用率:25%
- 記憶體使用率:25 MB (App啟動後的基準線為 17 MB)
優化方案 2(獨立的分鐘、秒、分秒控制元件)
- CPU Usage: 15% to 25%
- 記憶體使用率:26 MB (App啟動後的基準線為 17 MB)
在最後一個測試中,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 介面。
這並不足以讓人驚訝。
- 在 Flutter 應用中,我每次都建立和渲染了一個新的
Text
控制元件。 - 在 iOS 中,我只是更新了
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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。