天天看點

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

由于疫情的原因,今年的Google 開發者大會 (Google Developer Summit) 線上上舉行,本次大會以“代碼不止”為主題,全面介紹了産品更新以及一系列面向本地開發者的技術支援内容。我比較關注的是移動開發,在本次大會上,關于Flutter 主題的演講主要從 Flutter 性能方面優化和新功能進行展開。

作為全球增長速度第二的開源項目,越來越多國内開發者使用 Flutter 實作跨平台開發,包括騰訊英語君團隊、阿裡閑魚團隊等等。其在 開放性上的進步,得益于開源社群、生态建設、對 Web 的支援。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

有興趣的讀者可以通過Google Developer官網進行學習:Google Developer官網

下面我們就來看一下這些新功能和性能上的優化。

Flutter 性能優化

首先為我們帶來演講的是Google 軟體工程師李宇骞,他是Flutter 團隊的一位軟體工程師,主要專注于提升其性能。下面是具體的演講内容:

2019 下半年,Flutter 團隊共收到 23 個量化的性能提升;2020 上半年,Flutter 團隊共收到 27 個量化的性能提升。2020 上半年 Flutter 團隊共收到來自 78 位開發者的 49 個性能改進。

工具的性能十分重要,性能測試也同樣至關重要,擁有良好的性能測試可以:

  • 快速重制問題;
  • 疊代和驗證解決方案;
  • 提供資料,激勵進一步的工作并防止倒退。

通常,能耗與渲染速度相關,每一幀渲染時間越長則能耗就越高,但能耗并不能衡量渲染速度,因為在某些情況下渲染速度快也可能會導緻能耗升高,渲染速度慢也可能不耗能。

CPU 上運作時間雖然短,但由于新的算法利用了更多的 GPU 核心,是以 GPU 能耗反而增加;有些 CPU 上的任務被别的 I/O 或 GPU 任務阻塞,進行了長時間的等待,而等待的時間内并無過多能耗。

是以,在速度之外增加能耗測試是十分必要的。因為 Flutter 團隊在 GitHub 上收到的大部分能耗問題都和 iOS 相關,是以此次 Flutter 首先加入了 iOS 的能耗測試,Android 的能耗測試工具會于後續加入。

開發者可以使用 Flutter Gallery App 在 Timeline 中檢視 CPU/GPU 的使用率,也可以用內建測試自動檢測 CPU/GPU 的使用率。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

Flutter 還新加入了 SkSL 着色器編譯預熱功能,來幫助開發者消除着色器編譯卡頓。如果一個 Flutter 程式第一次渲染某類動畫時出現明顯的卡頓,但是之後渲染這些動畫時,卡頓完全消失,那麼這就很可能是着色器編譯卡頓。開發者可以使用 --trace-skia,然後檢查 Timeline 來确認是否為着色器卡頓。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

值得一提的是,SkSL 可以實作自動化生成與測試,這對于需要持續更新的 Flutter App 來說,可以節省很多的人力。

記憶體和包體積的測試工具

接下來,是由Flutter 使用者體驗研究員侯悠揚帶來的測試工具專題。侯悠揚于 2017 年加入 Google,并于 2019 年加入 Flutter 團隊。她是 Flutter 團隊一名使用者體驗研究員,關注提升 Flutter 産品和開發工具的程式員體驗。

此次,Flutter 團隊更新了Dart開發工具。Dart 開發工具是面向 Flutter 和 Dart 開發人員的工具套件,包括如下一些小工具:

  • 布局檢查(Inspector)
  • 性能調試(Performance)
  • 記憶體調試(Memory)
  • 網絡調試(Network)
  • 包體積調試(App Size)
  • 調試器(Debugger)
  • 日志(Logging)

連接配接上裝置然後運作Flutter應用,點選Android Studio底部工具欄中的【Open DevTools】按鈕即可開啟調試功能。

記憶體調試器功能

Flutter的記憶體調試器提供如下功能:

  • 事件窗格(Dart 和 Android 記憶體)
  • 手動和自動快照(snapshot)和垃圾回收(GC)
  • 記憶體分析
  • 記憶體堆配置設定累加器(Heap Allocation Accumulators)
  • 通過指令行界面将記憶體統計資訊到處到 JSON 檔案

記憶體測試

記憶體測試提供如下功能:

  • 通過 ADB 互動直接進行記憶體測試
  • Dart 開發工具記憶體測試
  • iOS 記憶體測試

更多資訊可以通過這篇由 Flutter 工程師撰寫的文章進行了解:怎麼進行Flutter記憶體測試

包體積調試器功能

包體積調試器提供如下功能:

  • 可視化了應用程式的總大小,包括功能級别的 Dart AOT 快照;
  • 分析快照和應用包(APK,IPA 等);
  • 分析快照或應用程式包(APK,IPA 等)的差異;
  • 檢視軟體包級别的應用大小歸因資料。

Pigeon與Flutter混合開發

什麼是Pigeon

在早期的hybird開發模式中,前端和Native互動時需要native雙端為JS提供接口。這種情況下如何規範命名,參數等就成了一個問題,如果單獨維護一份協定檔案,三端依照協定檔案進行開發,很容易出現協定更改後,沒有及時同步,又或者在實際開發過程沒有按照規範,可能導緻各種意外情況。

同樣,在Flutter插件包的開發中,因為涉及到Native雙端代碼開發能力,Dart側暴露統一的接口給使用者,也會出現同樣的問題,此時Pigeon應運而生,Pigeon是Flutter官方推薦插件管理工具,可以使用來解決和優化 Native 插件開發上 platform channel 相關的問題。

Flutter官方提供的Pigeon插件,通過dart入口,生成雙端通用的模闆代碼,Native部分隻需通過重寫模闆内的接口,無需關心methodChannel部分的具體實作,入參,出參也均通過生成的模闆代碼進行限制。接口新增,或者參數修改,隻需要在dart側更新協定檔案,生成雙端模闆,即可達到同步更新,有效的避免了參數修改,參數新增帶來的雙端代碼不同步的問題,下面是Pigeon工作原理示意圖。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

下面是Pigeon給出的示例:

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

可以看到接入Pigeon後整體代碼簡潔了不少,而且規範了類型定義。

Pigeon接入

接下來我們看一下如何從零接入Pigeon。截止目前,Pigeon已經釋出了0.1.15版本,如下圖所示。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

首先,建立一個名為testpigeon的Flutter項目,打開項目的pubspec.yaml檔案,并添加如下依賴代碼。

dependencies:
  pigeon: ^0.1.15           

複制

然後,按照官方的要求在項目目錄下建立一個pigeons目錄,作為存放dart側的入口檔案,内容為接口、參數、傳回值的定義等,以及後面通過pigeon的指令,生産native端代碼。接下來,建立一個message.dart 檔案,并添加如下。

import 'package:pigeon/pigeon.dart';

class SearchRequest {
  String query;
}

class SearchReply {
  String result;
}

@HostApi()
abstract class Api {
  SearchReply search(SearchRequest request);
}           

複制

在上面的message.dart 檔案中,通過 @HostApi() 注解标示了通信對象和接口,之後我們隻需要執行如下指令,就可以生成對應代碼到工程中。

flutter pub run pigeon  --input pigeons/message.dart           

複制

其實上面的指令是下面指令的簡寫方式:

flutter pub run pigeon  --input pigeons/message.dart  --dart_out lib/pigeon.dart  --objc_header_out ios/Runner/pigeon.h --objc_source_out ios/Runner/pigeon.m --java_out android/app/src/main/java/Pigeon.java --java_package "com.xzh.testpigeon"           

複制

指令的參數的含義如下:

  • --input:引入了我們建立的 message.dart 檔案;
  • --dart_out:輸出了 dart 模闆檔案;
  • --objc_header_out 和 --objc_source_out 輸出了 object-c 檔案;
  • --java_out 輸出了 java 檔案;

指令執行後 dart 檔案輸出到 lib 目錄下, object-c 檔案輸出到了 ios/Runner 目錄下,java 檔案輸出到指定的 com.xzh.testpigeon" 包名路徑下,之後就可以開始正式接入。然後我們分别使用Android Studio和Xcode打開原生工程代碼。

Android 工程代碼

使用Android Studio打開Flutter項目的原生Android工程,生成的代碼如下:

// Autogenerated from Pigeon (v0.1.15), do not edit directly.
// See also: https://pub.dev/packages/pigeon

package com.xzh.testpigeon;

import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import java.util.ArrayList;
import java.util.HashMap;

/** Generated class from Pigeon. */
@SuppressWarnings("unused")
public class Pigeon {

  /** Generated class from Pigeon that represents data sent in messages. */
  public static class SearchReply {
    private String result;
    public String getResult() { return result; }
    public void setResult(String setterArg) { this.result = setterArg; }

    HashMap toMap() {
      HashMap<String, Object> toMapResult = new HashMap<>();
      toMapResult.put("result", result);
      return toMapResult;
    }
    static SearchReply fromMap(HashMap map) {
      SearchReply fromMapResult = new SearchReply();
      Object result = map.get("result");
      fromMapResult.result = (String)result;
      return fromMapResult;
    }
  }

  /** Generated class from Pigeon that represents data sent in messages. */
  public static class SearchRequest {
    private String query;
    public String getQuery() { return query; }
    public void setQuery(String setterArg) { this.query = setterArg; }

    HashMap toMap() {
      HashMap<String, Object> toMapResult = new HashMap<>();
      toMapResult.put("query", query);
      return toMapResult;
    }
    static SearchRequest fromMap(HashMap map) {
      SearchRequest fromMapResult = new SearchRequest();
      Object query = map.get("query");
      fromMapResult.query = (String)query;
      return fromMapResult;
    }
  }

  /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
  public interface Api {
    SearchReply search(SearchRequest arg);

    /** Sets up an instance of `Api` to handle messages through the `binaryMessenger` */
    static void setup(BinaryMessenger binaryMessenger, Api api) {
      {
        BasicMessageChannel<Object> channel =
            new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.Api.search", new StandardMessageCodec());
        if (api != null) {
          channel.setMessageHandler((message, reply) -> {
            HashMap<String, HashMap> wrapped = new HashMap<>();
            try {
              @SuppressWarnings("ConstantConditions")
              SearchRequest input = SearchRequest.fromMap((HashMap)message);
              SearchReply output = api.search(input);
              wrapped.put("result", output.toMap());
            }
            catch (Exception exception) {
              wrapped.put("error", wrapError(exception));
            }
            reply.reply(wrapped);
          });
        } else {
          channel.setMessageHandler(null);
        }
      }
    }
  }
  private static HashMap wrapError(Exception exception) {
    HashMap<String, Object> errorMap = new HashMap<>();
    errorMap.put("message", exception.toString());
    errorMap.put("code", exception.getClass().getSimpleName());
    errorMap.put("details", null);
    return errorMap;
  }
}           

複制

上面生成的 Pigeon.java 代碼中包含了 Api 接口用于開發者實作互動邏輯,同時開發者可以通過 SearchRequest 擷取 dart 發送過來的請求,通過 SearchReply 傳回資料給 dart 。然後,還需要在Android的入口檔案MainActivity 中實作 Api 接口來完成資料互動,代碼如下。

public class MainActivity extends FlutterActivity {

  private class MyApi implements Pigeon.Api {
    @Override
    public Pigeon.SearchReply search(Pigeon.SearchRequest request) {
      Pigeon.SearchReply reply = new Pigeon.SearchReply();
      reply.setResult(String.format("Hi %s!", request.getQuery()));
      return reply;
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    Pigeon.Api.setup(getFlutterView(), new MyApi());
  }
}           

複制

首先,我們繼承 Pigeon.Api 實作了 MyApi 對象,然後在 search() 方法中通過 request.getQuery() 擷取 dart 的請求資料,并且通過 Pigeon.SearchReply 的 setResult 傳回 資料給dart 端,最後通過

Pigeon.Api.setup(getFlutterView(), new MyApi())

啟動。

iOS

使用Xcode打開Flutter項目的iOS工程,把生成的 pigeon.h 和 pigeon.m 檔案 link 到 Xcode 工程裡,之後如下代碼所示在 AppDelegate.h 引入 Api 協定。

#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "pigeon.h"

@interface AppDelegate : FlutterAppDelegate<Api>

@end           

複制

接下來,在 AppDelegate.m 中實作 search 接口,并在收到的 dart 消息後基于回複,最後調用 ApiSetup()方法将完成注冊。

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
  FlutterViewController* controller =
      (FlutterViewController*)self.window.rootViewController;
  ApiSetup(controller.binaryMessenger, self);
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}


-(SearchReply *)search:(SearchRequest*)input error:(FlutterError **)error {
    SearchReply* result = [[SearchReply alloc] init];
    result.result  = [NSString stringWithFormat:@"%s%@","Hi ",input.query];
    return result;
}


@end           

複制

Dart測試

最後我們在 Dart 代碼中建立一個測試的代碼,如下所示。

import 'pigeon.dart';

void main() {
  testWidgets("test pigeon", (WidgetTester tester) async {
    SearchRequest request = SearchRequest()..query = "Aaron";
    Api api = Api();
    SearchReply reply = await api.search(request);
    expect(reply.result, equals("Hi Aaron!"));
  });
}           

複制

Flutter 在阿裡巴巴的應用

首先,主持人為我們介紹了Flutter的曆史,介紹圍繞美觀、高效、流程和開放等幾個方面來介紹Flutter。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

接下來,阿裡巴巴的無線技術專家門柳介紹Flutter在阿裡巴巴的應用,閑魚是阿裡巴巴Flutter技術實踐的先驅,也是國内最早嘗試Flutter技術的大型網際網路公司,而阿裡巴巴旗下的淘寶也不甘示弱,也在某些子產品結成Flutter,不過大多是業務級别的子產品,而沒有像閑魚那樣大規模使用。我們可以從下圖看到Flutter在阿裡巴巴的使用情況。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

那為什麼,這麼多的移動應用開始使用Flutter來進行開發呢?首先,讓我們來了解下跨平台技術的發展曆程。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

可以發現,移動跨平台開發經曆了大約四個階段:

  • 早期的WebView加載方案
  • 原生API橋接的Hybrid方案
  • 原生渲染方案(Web文法+原生UI)
  • 自繪渲染(獨立布局/渲染)

而Flutter就是采用的自繪渲染方案,有興趣的童鞋可以研究以下Flutter的架構。為什麼選擇Flutter進行跨平台應用開發呢,下面是Flutter所具有的一些優勢:

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

不過,Flutter也不是萬能的,Flutter目前處于快速疊代的階段,是以保險起見,我們隻在一些正常的業務開發和子產品化的UI界面開發和部分遊戲中使用Flutter。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

總結起來,就是在一些富互動類應用和新型的應用中使用Flutter,對于視訊、直播等渲染要求高的則繼續使用原生進行開發。

那使用Flutter進行應用開發時,有哪些經驗和問題需要注意呢?下圖顯示了阿裡巴巴在使用Flutter進行應用開發時遇到的一些問題,大家使用時需要規避。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

首先遇到的問題是,由于Flutter使用的是Dart進行開發,無疑增加了開發者的學習成本。其次,對于大型應用來說,如何保證代碼品質,如何在多個平台運作自動化測試腳本也是一個問題;并且由于Flutter作為一門新的技術,如何快速的将老得業務遷移過來也是大家需要考慮的問題。總結一下,就是調試、測試、狀态管理、緩和導航棧管理、跨平台相容以及如何尋找解決方案的問題。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

盡管Flutter已經提供了很多的工具,但是如何将它融入到阿裡巴巴的用戶端開發工作流中,是大家需要考慮的問題。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

首先,為了提升開發效率,降低初期的接入成本,我們将Flutter Toolkit融入到Alibab DevOps工作流中,并自研了一些工具、打包和釋出平台以及搭建調試環境。接下來,我們基于現存的技術積累,研發了一些中間件。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

下面來看一個執行個體,即如何解決多圖清單頁面的記憶體占用問題。這類問題的特征如下:

  • 頁面很長,圖檔很多,首次加載時間很長
  • 大量圖檔同時加載并生成紋理,記憶體飙升
  • Sliver中每項Cell拆分粒度很大,單個Cell占用多屏,難以回收
    Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

    在這裡插入圖檔描述

    對于清單Flutter清單記憶體回收的問題,大家可以閱讀 細化 Flutter List 記憶體回收,解決大 Cell 問題這篇文章。

對于上面的多圖長清單的記憶體問題,我們可以從以下幾個方面着手進行優化:

  • 拆分Cell,使每一項變得更小
  • 根據坐标判斷圖檔是否在螢幕内,進而進行圖檔的懶加載和回收
  • 提前擷取圖檔的寬高大小,減少布局和重繪
  • 以圖檔為機關進行紋理回收,而不是Sliver中的每項Cell為機關
  • 外接原生圖檔庫,實作共享本地緩存
Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

最後,我們來看一下Flutter在阿裡巴巴的體系化建設。首先,Flutter的體系化建設主要從基礎能力建設、研發平台和可持續疊代等幾個方面着手。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

下面是Flutter在阿裡巴巴平台建設的具體的一些方案。

Google 2020開發者大會Flutter專題Flutter 性能優化記憶體和包體積的測試工具Pigeon與Flutter混合開發Flutter 在阿裡巴巴的應用

在這裡插入圖檔描述

目前,Flutter在阿裡巴巴已經經過了大規模的應用,并且我們自己的技術體系建設也在穩步推薦中,後面會将建設的一些成果通過社群分享出來。

附: Google 開發者大會