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

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

圖片來源: Petar Petkovskihtml

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

從理論上講,它聽起來很是棒!android

根據文檔,高性能是預料之中的:ios

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

可是 CPU 利用率如何?github

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

  • 頻繁地重繪用戶界面代價是很高的。
  • 若是你常常調用 setState() 方法,請確保儘量少地從新繪製用戶界面。

我用 Flutter 框架開發了一個簡單的秒錶應用程序,並分析了 CPU 和內存的使用狀況。後端

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

實現

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 毫秒計時器觸發一次時。

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

建立一個獨立的的 TimerText 類來封裝定時器的邏輯,能夠下降 CPU 負擔。

換句話說:

  • 頻繁地重繪 UI 用戶界面代價很高。
  • 若是常常調用 setState() 方法,確保儘量少地從新繪製 UI 用戶界面。

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

Flutter 框架使用了一種功能式流程,這種流程很大程度上取決於內存分配器是否有效地處理了小型,短時間的分配工做。

也許重建一棵部件樹不能算做「小型,短時間的分配」。實際上,個人代碼優化了致使較低的 CPU 和內存使用率的問題(見下文)。

更新至 19–03–2018

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

更新後的代碼經過將 TimerText 分爲了兩個 MinutesAndSecondsHundredths 控件,進一步減小了用戶界面的重繪:

進一步的 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。歡迎一切的反饋,推文,有趣的資訊!想知道我最喜歡什麼?許多的掌聲 👏👏👏。噢,還有香蕉和麪包。


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

相關文章
相關標籤/搜索