[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

ALVIN君發表於2018-04-19

Flutter 到底有多快?我開發了秒錶應用來弄清楚。

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

圖片來源: Petar Petkovski

這個週末,我花了點時間去用由谷歌新開發的 UI 框架 Flutter

從理論上講,它聽起來非常棒!

根據文件,高效能是預料之中的:

Flutter 旨在幫助開發者輕鬆地實現恆定的 60 fps。

但是 CPU 利用率如何?

太長了讀不下去,直接看評論:不如原生好。你必須正確地做到:

  • 頻繁地重繪使用者介面代價是很高的。
  • 如果你經常呼叫 setState() 方法,請確保儘可能少地重新繪製使用者介面。

我用 Flutter 框架開發了一個簡單的秒錶應用程式,並分析了 CPU 和記憶體的使用情況。

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

圖左:iOS 秒錶應用。 圖右:用 Flutter 的版本。很漂亮吧?

實現

UI 介面是由兩個物件驅動的: 秒錶定時器

  • 使用者可以通過點選這兩個按鈕來啟動、停止和重置秒錶。
  • 每當秒錶開始計時時,都會建立一個週期性定時器,每 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 毫秒計時器觸發一次時。

如果我們考慮到未優化和已優化的部件樹層次結構,這一點就變得更顯而易見了:

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

建立一個獨立的的 TimerText 類來封裝定時器的邏輯,可以降低 CPU 負擔。

換句話說:

  • 頻繁地重繪 UI 使用者介面代價很高。
  • 如果經常呼叫 setState() 方法,確保儘可能少地重新繪製 UI 使用者介面。

Flutter 官方文件指出該平臺對快速分配進行了優化:

Flutter 框架使用了一種功能式流程,這種流程很大程度上取決於記憶體分配器是否有效地處理了小型,短期的分配工作。

也許重建一棵部件樹不能算作“小型,短期的分配”。實際上,我的程式碼優化了導致較低的 CPU 和記憶體使用率的問題(見下文)。

更新至 19–03–2018

自從這篇文章發表以來,一些谷歌工程師注意到了這一點,並做出了進一步的優化。

更新後的程式碼通過將 TimerText 分為了兩個 MinutesAndSecondsHundredths 控制元件,進一步減少了使用者介面的重繪:

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

進一步的 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)

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

優化方案 1(獨立的定時文字控制元件)

  • CPU 使用率:25%
  • 記憶體使用率:25 MB (App啟動後的基準線為 17 MB)

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

優化方案 2(獨立的分鐘、秒、分秒控制元件)

  • CPU Usage: 15% to 25%
  • 記憶體使用率:26 MB (App啟動後的基準線為 17 MB)

[譯] Flutter 到底有多快?我開發了秒錶應用來弄清楚。

在最後一個測試中,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。歡迎一切的反饋,推文,有趣的資訊!想知道我最喜歡什麼?許多的掌聲 ???。噢,還有香蕉和麵包。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章