由于疫情的原因,今年的Google 開發者大會 (Google Developer Summit) 線上上舉行,本次大會以“代碼不止”為主題,全面介紹了産品更新以及一系列面向本地開發者的技術支援内容。我比較關注的是移動開發,在本次大會上,關于Flutter 主題的演講主要從 Flutter 性能方面優化和新功能進行展開。
作為全球增長速度第二的開源項目,越來越多國内開發者使用 Flutter 實作跨平台開發,包括騰訊英語君團隊、阿裡閑魚團隊等等。其在 開放性上的進步,得益于開源社群、生态建設、對 Web 的支援。
在這裡插入圖檔描述
有興趣的讀者可以通過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 的使用率。
在這裡插入圖檔描述
Flutter 還新加入了 SkSL 着色器編譯預熱功能,來幫助開發者消除着色器編譯卡頓。如果一個 Flutter 程式第一次渲染某類動畫時出現明顯的卡頓,但是之後渲染這些動畫時,卡頓完全消失,那麼這就很可能是着色器編譯卡頓。開發者可以使用 --trace-skia,然後檢查 Timeline 來确認是否為着色器卡頓。
在這裡插入圖檔描述
值得一提的是,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工作原理示意圖。
在這裡插入圖檔描述
下面是Pigeon給出的示例:
在這裡插入圖檔描述
可以看到接入Pigeon後整體代碼簡潔了不少,而且規範了類型定義。
Pigeon接入
接下來我們看一下如何從零接入Pigeon。截止目前,Pigeon已經釋出了0.1.15版本,如下圖所示。
在這裡插入圖檔描述
首先,建立一個名為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。
在這裡插入圖檔描述
接下來,阿裡巴巴的無線技術專家門柳介紹Flutter在阿裡巴巴的應用,閑魚是阿裡巴巴Flutter技術實踐的先驅,也是國内最早嘗試Flutter技術的大型網際網路公司,而阿裡巴巴旗下的淘寶也不甘示弱,也在某些子產品結成Flutter,不過大多是業務級别的子產品,而沒有像閑魚那樣大規模使用。我們可以從下圖看到Flutter在阿裡巴巴的使用情況。
在這裡插入圖檔描述
那為什麼,這麼多的移動應用開始使用Flutter來進行開發呢?首先,讓我們來了解下跨平台技術的發展曆程。
在這裡插入圖檔描述
可以發現,移動跨平台開發經曆了大約四個階段:
- 早期的WebView加載方案
- 原生API橋接的Hybrid方案
- 原生渲染方案(Web文法+原生UI)
- 自繪渲染(獨立布局/渲染)
而Flutter就是采用的自繪渲染方案,有興趣的童鞋可以研究以下Flutter的架構。為什麼選擇Flutter進行跨平台應用開發呢,下面是Flutter所具有的一些優勢:
在這裡插入圖檔描述
不過,Flutter也不是萬能的,Flutter目前處于快速疊代的階段,是以保險起見,我們隻在一些正常的業務開發和子產品化的UI界面開發和部分遊戲中使用Flutter。
在這裡插入圖檔描述
總結起來,就是在一些富互動類應用和新型的應用中使用Flutter,對于視訊、直播等渲染要求高的則繼續使用原生進行開發。
那使用Flutter進行應用開發時,有哪些經驗和問題需要注意呢?下圖顯示了阿裡巴巴在使用Flutter進行應用開發時遇到的一些問題,大家使用時需要規避。
在這裡插入圖檔描述
首先遇到的問題是,由于Flutter使用的是Dart進行開發,無疑增加了開發者的學習成本。其次,對于大型應用來說,如何保證代碼品質,如何在多個平台運作自動化測試腳本也是一個問題;并且由于Flutter作為一門新的技術,如何快速的将老得業務遷移過來也是大家需要考慮的問題。總結一下,就是調試、測試、狀态管理、緩和導航棧管理、跨平台相容以及如何尋找解決方案的問題。
在這裡插入圖檔描述
盡管Flutter已經提供了很多的工具,但是如何将它融入到阿裡巴巴的用戶端開發工作流中,是大家需要考慮的問題。
在這裡插入圖檔描述
首先,為了提升開發效率,降低初期的接入成本,我們将Flutter Toolkit融入到Alibab DevOps工作流中,并自研了一些工具、打包和釋出平台以及搭建調試環境。接下來,我們基于現存的技術積累,研發了一些中間件。
在這裡插入圖檔描述
下面來看一個執行個體,即如何解決多圖清單頁面的記憶體占用問題。這類問題的特征如下:
- 頁面很長,圖檔很多,首次加載時間很長
- 大量圖檔同時加載并生成紋理,記憶體飙升
- Sliver中每項Cell拆分粒度很大,單個Cell占用多屏,難以回收
在這裡插入圖檔描述
對于清單Flutter清單記憶體回收的問題,大家可以閱讀 細化 Flutter List 記憶體回收,解決大 Cell 問題這篇文章。
對于上面的多圖長清單的記憶體問題,我們可以從以下幾個方面着手進行優化:
- 拆分Cell,使每一項變得更小
- 根據坐标判斷圖檔是否在螢幕内,進而進行圖檔的懶加載和回收
- 提前擷取圖檔的寬高大小,減少布局和重繪
- 以圖檔為機關進行紋理回收,而不是Sliver中的每項Cell為機關
- 外接原生圖檔庫,實作共享本地緩存
在這裡插入圖檔描述
最後,我們來看一下Flutter在阿裡巴巴的體系化建設。首先,Flutter的體系化建設主要從基礎能力建設、研發平台和可持續疊代等幾個方面着手。
在這裡插入圖檔描述
下面是Flutter在阿裡巴巴平台建設的具體的一些方案。
在這裡插入圖檔描述
目前,Flutter在阿裡巴巴已經經過了大規模的應用,并且我們自己的技術體系建設也在穩步推薦中,後面會将建設的一些成果通過社群分享出來。
附: Google 開發者大會