Flutter原理与实践
Flutter是Google開發(fā)的一套全新的跨平臺(tái)、開源UI框架,支持iOS、Android系統(tǒng)開發(fā),并且是未來新操作系統(tǒng)Fuchsia的默認(rèn)開發(fā)套件。自從2017年5月發(fā)布第一個(gè)版本以來,目前Flutter已經(jīng)發(fā)布了近60個(gè)版本,并且在2018年5月發(fā)布了第一個(gè)“Ready for Production Apps”的Beta 3版本,6月20日發(fā)布了第一個(gè)“Release Preview”版本。
初識(shí)Flutter
Flutter的目標(biāo)是使同一套代碼同時(shí)運(yùn)行在Android和iOS系統(tǒng)上,并且擁有媲美原生應(yīng)用的性能,Flutter甚至提供了兩套控件來適配Android和iOS(滾動(dòng)效果、字體和控件圖標(biāo)等等)為了讓App在細(xì)節(jié)處看起來更像原生應(yīng)用。
在Flutter誕生之前,已經(jīng)有許多跨平臺(tái)UI框架的方案,比如基于WebView的Cordova、AppCan等,還有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。
基于WebView的框架優(yōu)點(diǎn)很明顯,它們幾乎可以完全繼承現(xiàn)代Web開發(fā)的所有成果(豐富得多的控件庫、滿足各種需求的頁面框架、完全的動(dòng)態(tài)化、自動(dòng)化測試工具等等),當(dāng)然也包括Web開發(fā)人員,不需要太多的學(xué)習(xí)和遷移成本就可以開發(fā)一個(gè)App。同時(shí)WebView框架也有一個(gè)致命(在對(duì)體驗(yàn)&性能有較高要求的情況下)的缺點(diǎn),那就是WebView的渲染效率和JavaScript執(zhí)行性能太差。再加上Android各個(gè)系統(tǒng)版本和設(shè)備廠商的定制,很難保證所在所有設(shè)備上都能提供一致的體驗(yàn)。
為了解決WebView性能差的問題,以React Native為代表的一類框架將最終渲染工作交還給了系統(tǒng),雖然同樣使用類HTML+JS的UI構(gòu)建邏輯,但是最終會(huì)生成對(duì)應(yīng)的自定義原生控件,以充分利用原生控件相對(duì)于WebView的較高的繪制效率。與此同時(shí)這種策略也將框架本身和App開發(fā)者綁在了系統(tǒng)的控件系統(tǒng)上,不僅框架本身需要處理大量平臺(tái)相關(guān)的邏輯,隨著系統(tǒng)版本變化和API的變化,開發(fā)者可能也需要處理不同平臺(tái)的差異,甚至有些特性只能在部分平臺(tái)上實(shí)現(xiàn),這樣框架的跨平臺(tái)特性就會(huì)大打折扣。
Flutter則開辟了一種全新的思路,從頭到尾重寫一套跨平臺(tái)的UI框架,包括UI控件、渲染邏輯甚至開發(fā)語言。渲染引擎依靠跨平臺(tái)的Skia圖形庫來實(shí)現(xiàn),依賴系統(tǒng)的只有圖形繪制相關(guān)的接口,可以在最大程度上保證不同平臺(tái)、不同設(shè)備的體驗(yàn)一致性,邏輯處理使用支持AOT的Dart語言,執(zhí)行效率也比JavaScript高得多。
Flutter同時(shí)支持Windows、Linux和macOS操作系統(tǒng)作為開發(fā)環(huán)境,并且在Android Studio和VS Code兩個(gè)IDE上都提供了全功能的支持。Flutter所使用的Dart語言同時(shí)支持AOT和JIT運(yùn)行方式,JIT模式下還有一個(gè)備受歡迎的開發(fā)利器“熱刷新”(Hot Reload),即在Android Studio中編輯Dart代碼后,只需要點(diǎn)擊保存或者“Hot Reload”按鈕,就可以立即更新到正在運(yùn)行的設(shè)備上,不需要重新編譯App,甚至不需要重啟App,立即就可以看到更新后的樣式。
在Flutter中,所有功能都可以通過組合多個(gè)Widget來實(shí)現(xiàn),包括對(duì)齊方式、按行排列、按列排列、網(wǎng)格排列甚至事件處理等等。Flutter控件主要分為兩大類,StatelessWidget和StatefulWidget,StatelessWidget用來展示靜態(tài)的文本或者圖片,如果控件需要根據(jù)外部數(shù)據(jù)或者用戶操作來改變的話,就需要使用StatefulWidget。State的概念也是來源于Facebook的流行Web框架React,React風(fēng)格的框架中使用控件樹和各自的狀態(tài)來構(gòu)建界面,當(dāng)某個(gè)控件的狀態(tài)發(fā)生變化時(shí)由框架負(fù)責(zé)對(duì)比前后狀態(tài)差異并且采取最小代價(jià)來更新渲染結(jié)果。
Hot Reload
在Dart代碼文件中修改字符串“Hello, World”,添加一個(gè)驚嘆號(hào),點(diǎn)擊保存或者熱刷新按鈕就可以立即更新到界面上,僅需幾百毫秒:
Flutter通過將新的代碼注入到正在運(yùn)行的DartVM中,來實(shí)現(xiàn)Hot Reload這種神奇的效果,在DartVM將程序中的類結(jié)構(gòu)更新完成后,Flutter會(huì)立即重建整個(gè)控件樹,從而更新界面。但是熱刷新也有一些限制,并不是所有的代碼改動(dòng)都可以通過熱刷新來更新:
熱刷新無法實(shí)現(xiàn)更新時(shí),執(zhí)行一次熱重啟(Hot Restart)就可以全量更新所有代碼,同樣不需要重啟App,區(qū)別是restart會(huì)將所有Dart代碼打包同步到設(shè)備上,并且所有狀態(tài)都會(huì)重置。
Flutter插件
Flutter使用的Dart語言無法直接調(diào)用Android系統(tǒng)提供的Java接口,這時(shí)就需要使用插件來實(shí)現(xiàn)中轉(zhuǎn)。Flutter官方提供了豐富的原生接口封裝:
- android_alarm_manager,訪問Android系統(tǒng)的AlertManager。
- android_intent,構(gòu)造Android的Intent對(duì)象。
- battery,獲取和監(jiān)聽系統(tǒng)電量變化。
- connectivity,獲取和監(jiān)聽系統(tǒng)網(wǎng)絡(luò)連接狀態(tài)。
- device info,獲取設(shè)備型號(hào)等信息。
- image_picker,從設(shè)備中選取或者拍攝照片。
- package_info,獲取App安裝包的版本等信息。
- path_provider,獲取常用文件路徑。
- quick_actions,App圖標(biāo)添加快捷方式,iOS的eponymous concept和Android的App Shortcuts。
- sensors,訪問設(shè)備的加速度和陀螺儀傳感器。
- shared_preferences,App KV存儲(chǔ)功能。
- url_launcher,啟動(dòng)URL,包括打電話、發(fā)短信和瀏覽網(wǎng)頁等功能。
- video_player,播放視頻文件或者網(wǎng)絡(luò)流的控件。
在Flutter中,依賴包由Pub倉庫管理,項(xiàng)目依賴配置在pubspec.yaml文件中聲明即可(類似于NPM的版本聲明 Pub Versioning Philosophy),對(duì)于未發(fā)布在Pub倉庫的插件可以使用git倉庫地址或文件路徑:
dependencies: url_launcher: ">=0.1.2 <0.2.0"collection: "^0.1.2"plugin1: git: url: "git://github.com/flutter/plugin1.git"plugin2: path: ../plugin2/以shared_preferences為例,在pubspec中添加代碼:
dependencies:flutter:sdk: fluttershared_preferences: "^0.4.1"脫字號(hào)“^”開頭的版本表示和當(dāng)前版本接口保持兼容的最新版,^1.2.3 等效于 >=1.2.3 <2.0.0 而 ^0.1.2 等效于 >=0.1.2 <0.2.0,添加依賴后點(diǎn)擊“Packages get”按鈕即可下載插件到本地,在代碼中添加import語句就可以使用插件提供的接口:
import 'package:shared_preferences/shared_preferences.Dart';class _MyAppState extends State<MyAppCounter> {int _count = 0;static const String COUNTER_KEY = 'counter';_MyAppState() {init();}init() async {var pref = await SharedPreferences.getInstance();_count = pref.getInt(COUNTER_KEY) ?? 0;setState(() {});}increaseCounter() async {SharedPreferences pref = await SharedPreferences.getInstance();pref.setInt(COUNTER_KEY, ++_count);setState(() {});} ...Dart
Dart是一種強(qiáng)類型、跨平臺(tái)的客戶端開發(fā)語言。具有專門為客戶端優(yōu)化、高生產(chǎn)力、快速高效、可移植(兼容ARM/x86)、易學(xué)的OO編程風(fēng)格和原生支持響應(yīng)式編程(Stream & Future)等優(yōu)秀特性。Dart主要由Google負(fù)責(zé)開發(fā)和維護(hù),在2011年10啟動(dòng)項(xiàng)目,2017年9月發(fā)布第一個(gè)2.0-dev版本。
Dart本身提供了三種運(yùn)行方式:
Flutter在篩選了20多種語言后,最終選擇Dart作為開發(fā)語言主要有幾個(gè)原因:
在Dart中,有一些重要的基本概念需要了解:
- 所有變量的值都是對(duì)象,也就是類的實(shí)例。甚至數(shù)字、函數(shù)和null也都是對(duì)象,都繼承自O(shè)bject類。
- 雖然Dart是強(qiáng)類型語言,但是顯式變量類型聲明是可選的,Dart支持類型推斷。如果不想使用類型推斷,可以用dynamic類型。
- Dart支持泛型,List<int>表示包含int類型的列表,List<dynamic>則表示包含任意類型的列表。
- Dart支持頂層(top-level)函數(shù)和類成員函數(shù),也支持嵌套函數(shù)和本地函數(shù)。
- Dart支持頂層變量和類成員變量。
- Dart沒有public、protected和private這些關(guān)鍵字,使用下劃線“_”開頭的變量或者函數(shù),表示只在庫內(nèi)可見。參考庫和可見性。
DartVM的內(nèi)存分配策略非常簡單,創(chuàng)建對(duì)象時(shí)只需要在現(xiàn)有堆上移動(dòng)指針,內(nèi)存增長始終是線形的,省去了查找可用內(nèi)存段的過程:
Dart中類似線程的概念叫做Isolate,每個(gè)Isolate之間是無法共享內(nèi)存的,所以這種分配策略可以讓Dart實(shí)現(xiàn)無鎖的快速分配。
Dart的垃圾回收也采用了多生代算法,新生代在回收內(nèi)存時(shí)采用了“半空間”算法,觸發(fā)垃圾回收時(shí)Dart會(huì)將當(dāng)前半空間中的“活躍”對(duì)象拷貝到備用空間,然后整體釋放當(dāng)前空間的所有內(nèi)存:
整個(gè)過程中Dart只需要操作少量的“活躍”對(duì)象,大量的沒有引用的“死亡”對(duì)象則被忽略,這種算法也非常適合Flutter框架中大量Widget重建的場景。
Flutter Framework
Flutter的框架部分完全使用Dart語言實(shí)現(xiàn),并且有著清晰的分層架構(gòu)。分層架構(gòu)使得我們可以在調(diào)用Flutter提供的便捷開發(fā)功能(預(yù)定義的一套高質(zhì)量Material控件)之外,還可以直接調(diào)用甚至修改每一層實(shí)現(xiàn)(因?yàn)檎麄€(gè)框架都屬于“用戶空間”的代碼),這給我們提供了最大程度的自定義能力。Framework底層是Flutter引擎,引擎主要負(fù)責(zé)圖形繪制(Skia)、文字排版(libtxt)和提供Dart運(yùn)行時(shí),引擎全部使用C++實(shí)現(xiàn),Framework層使我們可以用Dart語言調(diào)用引擎的強(qiáng)大能力。
分層架構(gòu)
Framework的最底層叫做Foundation,其中定義的大都是非常基礎(chǔ)的、提供給其他所有層使用的工具類和方法。繪制庫(Painting)封裝了Flutter Engine提供的繪制接口,主要是為了在繪制控件等固定樣式的圖形時(shí)提供更直觀、更方便的接口,比如繪制縮放后的位圖、繪制文本、插值生成陰影以及在盒子周圍繪制邊框等等。Animation是動(dòng)畫相關(guān)的類,提供了類似Android系統(tǒng)的ValueAnimator的功能,并且提供了豐富的內(nèi)置插值器。Gesture提供了手勢識(shí)別相關(guān)的功能,包括觸摸事件類定義和多種內(nèi)置的手勢識(shí)別器。GestureBinding類是Flutter中處理手勢的抽象服務(wù)類,繼承自BindingBase類。Binding系列的類在Flutter中充當(dāng)著類似于Android中的SystemService系列(ActivityManager、PackageManager)功能,每個(gè)Binding類都提供一個(gè)服務(wù)的單例對(duì)象,App最頂層的Binding會(huì)包含所有相關(guān)的Bingding抽象類。如果使用Flutter提供的控件進(jìn)行開發(fā),則需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接調(diào)用Render層,則需要使用RenderingFlutterBinding。
Flutter本身支持Android和iOS兩個(gè)平臺(tái),除了性能和開發(fā)語言上的“native”化之外,它還提供了兩套設(shè)計(jì)語言的控件實(shí)現(xiàn)Material & Cupertino,可以幫助App更好地在不同平臺(tái)上提供原生的用戶體驗(yàn)。
渲染庫(Rendering)
Flutter的控件樹在實(shí)際顯示時(shí)會(huì)轉(zhuǎn)換成對(duì)應(yīng)的渲染對(duì)象(RenderObject)樹來實(shí)現(xiàn)布局和繪制操作。一般情況下,我們只會(huì)在調(diào)試布局,或者需要使用自定義控件來實(shí)現(xiàn)某些特殊效果的時(shí)候,才需要考慮渲染對(duì)象樹的細(xì)節(jié)。渲染庫主要提供的功能類有:
abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { ... } abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget { abstract class RenderBox extends RenderObject { ... } class RenderParagraph extends RenderBox { ... } class RenderImage extends RenderBox { ... } class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>,RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>,DebugOverflowIndicatorMixin { ... }RendererBinding是渲染樹和Flutter引擎的膠水層,負(fù)責(zé)管理幀重繪、窗口尺寸和渲染相關(guān)參數(shù)變化的監(jiān)聽。RenderObject渲染樹中所有節(jié)點(diǎn)的基類,定義了布局、繪制和合成相關(guān)的接口。RenderBox和其三個(gè)常用的子類RenderParagraph、RenderImage、RenderFlex則是具體布局和繪制邏輯的實(shí)現(xiàn)類。
在Flutter界面渲染過程分為三個(gè)階段:布局、繪制、合成,布局和繪制在Flutter框架中完成,合成則交由引擎負(fù)責(zé)。
控件樹中的每個(gè)控件通過實(shí)現(xiàn)RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject方法來創(chuàng)建對(duì)應(yīng)的不同類型的RenderObject對(duì)象,組成渲染對(duì)象樹。因?yàn)镕lutter極大地簡化了布局的邏輯,所以整個(gè)布局過程中只需要深度遍歷一次:
渲染對(duì)象樹中的每個(gè)對(duì)象都會(huì)在布局過程中接受父對(duì)象的Constraints參數(shù),決定自己的大小,然后父對(duì)象就可以按照自己的邏輯決定各個(gè)子對(duì)象的位置,完成布局過程。子對(duì)象不存儲(chǔ)自己在容器中的位置,所以在它的位置發(fā)生改變時(shí)并不需要重新布局或者繪制。子對(duì)象的位置信息存儲(chǔ)在它自己的parentData字段中,但是該字段由它的父對(duì)象負(fù)責(zé)維護(hù),自身并不關(guān)心該字段的內(nèi)容。同時(shí)也因?yàn)檫@種簡單的布局邏輯,Flutter可以在某些節(jié)點(diǎn)設(shè)置布局邊界(Relayout boundary),即當(dāng)邊界內(nèi)的任何對(duì)象發(fā)生重新布局時(shí),不會(huì)影響邊界外的對(duì)象,反之亦然:
布局完成后,渲染對(duì)象樹中的每個(gè)節(jié)點(diǎn)都有了明確的尺寸和位置,Flutter會(huì)把所有對(duì)象繪制到不同的圖層上:
因?yàn)槔L制節(jié)點(diǎn)時(shí)也是深度遍歷,可以看到第二個(gè)節(jié)點(diǎn)在繪制它的背景和前景不得不繪制在不同的圖層上,因?yàn)榈谒膫€(gè)節(jié)點(diǎn)切換了圖層(因?yàn)椤?”節(jié)點(diǎn)是一個(gè)需要獨(dú)占一個(gè)圖層的內(nèi)容,比如視頻),而第六個(gè)節(jié)點(diǎn)也一起繪制到了紅色圖層。這樣會(huì)導(dǎo)致第二個(gè)節(jié)點(diǎn)的前景(也就是“5”)部分需要重繪時(shí),和它在邏輯上毫不相干但是處于同一圖層的第六個(gè)節(jié)點(diǎn)也必須重繪。為了避免這種情況,Flutter提供了另外一個(gè)“重繪邊界”的概念:
在進(jìn)入和走出重繪邊界時(shí),Flutter會(huì)強(qiáng)制切換新的圖層,這樣就可以避免邊界內(nèi)外的互相影響。典型的應(yīng)用場景就是ScrollView,當(dāng)滾動(dòng)內(nèi)容重繪時(shí),一般情況下其他內(nèi)容是不需要重繪的。雖然重繪邊界可以在任何節(jié)點(diǎn)手動(dòng)設(shè)置,但是一般不需要我們來實(shí)現(xiàn),Flutter提供的控件默認(rèn)會(huì)在需要設(shè)置的地方自動(dòng)設(shè)置。
控件庫(Widgets)
Flutter的控件庫提供了非常豐富的控件,包括最基本的文本、圖片、容器、輸入框和動(dòng)畫等等。在Flutter中“一切皆是控件”,通過組合、嵌套不同類型的控件,就可以構(gòu)建出任意功能、任意復(fù)雜度的界面。它包含的最主要的幾個(gè)類有:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding,PaintingBinding, RendererBinding, WidgetsBinding { ... } abstract class Widget extends DiagnosticableTree { ... } abstract class StatelessWidget extends Widget { ... } abstract class StatefulWidget extends Widget { ... } abstract class RenderObjectWidget extends Widget { ... } abstract class Element extends DiagnosticableTree implements BuildContext { ... } class StatelessElement extends ComponentElement { ... } class StatefulElement extends ComponentElement { ... } abstract class RenderObjectElement extends Element { ... } ...基于Flutter控件系統(tǒng)開發(fā)的程序都需要使用WidgetsFlutterBinding,它是Flutter的控件框架和Flutter引擎的膠水層。Widget就是所有控件的基類,它本身所有的屬性都是只讀的。RenderObjectWidget所有的實(shí)現(xiàn)類則負(fù)責(zé)提供配置信息并創(chuàng)建具體的RenderObjectElement。Element是Flutter用來分離控件樹和真正的渲染對(duì)象的中間層,控件用來描述對(duì)應(yīng)的element屬性,控件重建后可能會(huì)復(fù)用同一個(gè)element。RenderObjectElement持有真正負(fù)責(zé)布局、繪制和碰撞測試(hit test)的RenderObject對(duì)象。
StatelessWidget和StatefulWidget并不會(huì)直接影響RenderObject的創(chuàng)建,它們只負(fù)責(zé)創(chuàng)建對(duì)應(yīng)的RenderObjectWidget,StatelessElement和StatefulElement也是類似的功能。
它們之間的關(guān)系如下圖:
如果控件的屬性發(fā)生了變化(因?yàn)榭丶膶傩允侵蛔x的,所以變化也就意味著重新創(chuàng)建了新的控件樹),但是其樹上每個(gè)節(jié)點(diǎn)的類型沒有變化時(shí),element樹和render樹可以完全重用原來的對(duì)象(因?yàn)閑lement和render object的屬性都是可變的):
但是,如果控件樹種某個(gè)節(jié)點(diǎn)的類型發(fā)生了變化,則element樹和render樹中的對(duì)應(yīng)節(jié)點(diǎn)也需要重新創(chuàng)建:
外賣全品類頁面實(shí)踐
在調(diào)研了Flutter的各項(xiàng)特性和實(shí)現(xiàn)原理之后,外賣計(jì)劃灰度上線Flutter版的全品類頁面。對(duì)于將Flutter頁面作為App的一部分這種集成模式,官方并沒有提供完善的支持,所以我們首先需要了解Flutter是如何編譯、打包并且運(yùn)行起來的。
Flutter App構(gòu)建過程
最簡單的Flutter工程至少包含兩個(gè)文件:
運(yùn)行Flutter程序時(shí)需要對(duì)應(yīng)平臺(tái)的宿主工程,在Android上Flutter通過自動(dòng)創(chuàng)建一個(gè)Gradle項(xiàng)目來生成宿主,在項(xiàng)目目錄下執(zhí)行flutter create .,Flutter會(huì)創(chuàng)建ios和android兩個(gè)目錄,分別構(gòu)建對(duì)應(yīng)平臺(tái)的宿主項(xiàng)目,android目錄內(nèi)容如下:
此Gradle項(xiàng)目中只有一個(gè)app module,構(gòu)建產(chǎn)物即是宿主APK。Flutter在本地運(yùn)行時(shí)默認(rèn)采用Debug模式,在項(xiàng)目目錄執(zhí)行flutter run即可安裝到設(shè)備中并自動(dòng)運(yùn)行,Debug模式下Flutter使用JIT方式來執(zhí)行Dart代碼,所有的Dart代碼都會(huì)打包到APK文件中assets目錄下,由libflutter.so中提供的DartVM讀取并執(zhí)行:
kernel_blob.bin是Flutter引擎的底層接口和Dart語言基本功能部分代碼:
third_party/dart/runtime/bin/*.dart third_party/dart/runtime/lib/*.dart third_party/dart/sdk/lib/_http/*.dart third_party/dart/sdk/lib/async/*.dart third_party/dart/sdk/lib/collection/*.dart third_party/dart/sdk/lib/convert/*.dart third_party/dart/sdk/lib/core/*.dart third_party/dart/sdk/lib/developer/*.dart third_party/dart/sdk/lib/html/*.dart third_party/dart/sdk/lib/internal/*.dart third_party/dart/sdk/lib/io/*.dart third_party/dart/sdk/lib/isolate/*.dart third_party/dart/sdk/lib/math/*.dart third_party/dart/sdk/lib/mirrors/*.dart third_party/dart/sdk/lib/profiler/*.dart third_party/dart/sdk/lib/typed_data/*.dart third_party/dart/sdk/lib/vmservice/*.dart flutter/lib/ui/*.dartplatform.dill則是實(shí)現(xiàn)了頁面邏輯的代碼,也包括Flutter Framework和其他由pub依賴的庫代碼:
flutter_tutorial_2/lib/main.dart flutter/packages/flutter/lib/src/widgets/*.dart flutter/packages/flutter/lib/src/services/*.dart flutter/packages/flutter/lib/src/semantics/*.dart flutter/packages/flutter/lib/src/scheduler/*.dart flutter/packages/flutter/lib/src/rendering/*.dart flutter/packages/flutter/lib/src/physics/*.dart flutter/packages/flutter/lib/src/painting/*.dart flutter/packages/flutter/lib/src/gestures/*.dart flutter/packages/flutter/lib/src/foundation/*.dart flutter/packages/flutter/lib/src/animation/*.dart .pub-cache/hosted/pub.flutter-io.cn/collection-1.14.6/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/meta-1.1.5/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.4.2/*.dartkernel_blob.bin和platform.dill都是由flutter_tools中的bundle.dart中調(diào)用KernelCompiler生成。
在Release模式(flutter run --release)下,Flutter會(huì)使用Dart的AOT運(yùn)行模式,編譯時(shí)將Dart代碼轉(zhuǎn)換成ARM指令:
kernel_blob.bin和platform.dill都不在打包后的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四個(gè)文件。snapshot文件由Flutter SDK中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot命令生成,vm_snapshot_*是Dart虛擬機(jī)運(yùn)行所需要的數(shù)據(jù)和代碼指令,isolate_snapshot_*則是每個(gè)isolate運(yùn)行所需要的數(shù)據(jù)和代碼指令。
Flutter App運(yùn)行機(jī)制
Flutter構(gòu)建出的APK在運(yùn)行時(shí)會(huì)將所有assets目錄下的資源文件解壓到App私有文件目錄中的flutter目錄下,主要包括處理字符編碼的icudtl.dat,還有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4個(gè)snapshot文件。默認(rèn)情況下Flutter在Application#onCreate時(shí)調(diào)用FlutterMain#startInitialization來啟動(dòng)解壓任務(wù),然后在FlutterActivityDelegate#onCreate中調(diào)用FlutterMain#ensureInitializationComplete來等待解壓任務(wù)結(jié)束。
Flutter在Debug模式下使用JIT執(zhí)行方式,主要是為了支持廣受歡迎的熱刷新功能:
觸發(fā)熱刷新時(shí)Flutter會(huì)檢測發(fā)生改變的Dart文件,將其同步到App私有緩存目錄下,DartVM加載并且修改對(duì)應(yīng)的類或者方法,重建控件樹后立即可以在設(shè)備上看到效果。
在Release模式下Flutter會(huì)直接將snapshot文件映射到內(nèi)存中執(zhí)行其中的指令:
在Release模式下,FlutterActivityDelegate#onCreate中調(diào)用FlutterMain#ensureInitializationComplete方法中會(huì)將AndroidManifest中設(shè)置的snapshot(沒有設(shè)置則使用上面提到的默認(rèn)值)文件名等運(yùn)行參數(shù)設(shè)置到對(duì)應(yīng)的C++同名類對(duì)象中,構(gòu)造FlutterNativeView實(shí)例時(shí)調(diào)用nativeAttach來初始化DartVM,運(yùn)行編譯好的Dart代碼。
打包Android Library
了解Flutter項(xiàng)目的構(gòu)建和運(yùn)行機(jī)制后,我們就可以按照其需求打包成AAR然后集成到現(xiàn)有原生App中了。首先在andorid/app/build.gradle中修改:
| 修改android插件類型 | apply plugin: ‘com.android.application’ | apply plugin: ‘com.android.library’ |
| 刪除applicationId字段 | applicationId “com.example.fluttertutorial” | applicationId “com.example.fluttertutorial” |
| 建議添加發(fā)布所有配置功能,方便調(diào)試 | - | defaultPublishConfig ‘release’ publishNonDefault true |
簡單修改后我們就可以使用Android Studio或者Gradle命令行工具將Flutter代碼打包到aar中了。Flutter運(yùn)行時(shí)所需要的資源都會(huì)包含在aar中,將其發(fā)布到maven服務(wù)器或者本地maven倉庫后,就可以在原生App項(xiàng)目中引用。
但這只是集成的第一步,為了讓Flutter頁面無縫銜接到外賣App中,我們需要做的還有很多。
圖片資源復(fù)用
Flutter默認(rèn)將所有的圖片資源文件打包到assets目錄下,但是我們并不是用Flutter開發(fā)全新的頁面,圖片資源原來都會(huì)按照Android的規(guī)范放在各個(gè)drawable目錄,即使是全新的頁面也會(huì)有很多圖片資源復(fù)用的場景,所以在assets目錄下新增圖片資源并不合適。
Flutter官方并沒有提供直接調(diào)用drawable目錄下的圖片資源的途徑,畢竟drawable這類文件的處理會(huì)涉及大量的Android平臺(tái)相關(guān)的邏輯(屏幕密度、系統(tǒng)版本、語言等等),assets目錄文件的讀取操作也在引擎內(nèi)部使用C++實(shí)現(xiàn),在Dart層面實(shí)現(xiàn)讀取drawable文件的功能比較困難。Flutter在處理assets目錄中的文件時(shí)也支持添加多倍率的圖片資源,并能夠在使用時(shí)自動(dòng)選擇,但是Flutter要求每個(gè)圖片必須提供1x圖,然后才會(huì)識(shí)別到對(duì)應(yīng)的其他倍率目錄下的圖片:
flutter:assets:- images/cat.png- images/2x/cat.png- images/3.5x/cat.png new Image.asset('images/cat.png');這樣配置后,才能正確地在不同分辨率的設(shè)備上使用對(duì)應(yīng)密度的圖片。但是為了減小APK包體積我們的位圖資源一般只提供常用的2x分辨率,其他分辨率的設(shè)備會(huì)在運(yùn)行時(shí)自動(dòng)縮放到對(duì)應(yīng)大小。針對(duì)這種特殊的情況,我們在不增加包體積的前提下,同樣提供了和原生App一樣的能力:
這樣就可以同時(shí)解決APK包大小和圖片資源缺失1x圖的問題。
Flutter和原生代碼的通信
我們只用Flutter實(shí)現(xiàn)了一個(gè)頁面,現(xiàn)有的大量邏輯都是用Java實(shí)現(xiàn),在運(yùn)行時(shí)會(huì)有許多場景必須使用原生應(yīng)用中的邏輯和功能,例如網(wǎng)絡(luò)請(qǐng)求,我們統(tǒng)一的網(wǎng)絡(luò)庫會(huì)在每個(gè)網(wǎng)絡(luò)請(qǐng)求中添加許多通用參數(shù),也會(huì)負(fù)責(zé)成功率等指標(biāo)的監(jiān)控,還有異常上報(bào),我們需要在捕獲到關(guān)鍵異常時(shí)將其堆棧和環(huán)境信息上報(bào)到服務(wù)器。這些功能不太可能立即使用Dart實(shí)現(xiàn)一套出來,所以我們需要使用Dart提供的Platform Channel功能來實(shí)現(xiàn)Dart→Java之間的互相調(diào)用。
以網(wǎng)絡(luò)請(qǐng)求為例,我們在Dart中定義一個(gè)MethodChannel對(duì)象:
import 'dart:async'; import 'package:flutter/services.dart'; const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network'); Future<Map<String, dynamic>> post(String path, [Map<String, dynamic> form]) async {return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) {return new Map<String, dynamic>.from(result);}).catchError((_) => null); }然后在Java端實(shí)現(xiàn)相同名稱的MethodChannel:
public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {private static final String CHANNEL_NAME = "com.sankuai.waimai/network";@Overridepublic void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {switch (methodCall.method) {case "post":RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")),new DefaultSubscriber<Map>() {@Overridepublic void onError(Throwable e) {result.error(e.getClass().getCanonicalName(), e.getMessage(), null);}@Overridepublic void onNext(Map stringBaseResponse) {result.success(stringBaseResponse);}}, tag);break;default:result.notImplemented();break;}} }在Flutter頁面中注冊后,調(diào)用post方法就可以調(diào)用對(duì)應(yīng)的Java實(shí)現(xiàn):
loadData: (callback) async {Map<String, dynamic> data = await post("home/groups");if (data == null) {callback(false);return;}_data = AllCategoryResponse.fromJson(data);if (_data == null || _data.code != 0) {callback(false);return;}callback(true);}),SO庫兼容性
Flutter官方只提供了四種CPU架構(gòu)的SO庫:armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模式,但是外賣使用的大量SDK都只提供了armeabi架構(gòu)的庫。雖然我們可以通過修改引擎src根目錄和third_party/dart目錄下build/config/arm.gni,third_party/skia目錄下的BUILD.gn等配置文件來編譯出armeabi版本的Flutter引擎,但是實(shí)際上市面上絕大部分設(shè)備都已經(jīng)支持armeabi-v7a,其提供的硬件加速浮點(diǎn)運(yùn)算指令可以大大提高Flutter的運(yùn)行速度,在灰度階段我們可以主動(dòng)屏蔽掉不支持armeabi-v7a的設(shè)備,直接使用armeabi-v7a版本的引擎。做到這點(diǎn)我們首先需要修改Flutter提供的引擎,在Flutter安裝目錄下的bin/cache/artifacts/engine下有Flutter下載的所有平臺(tái)的引擎:
我們只需要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,將其中的lib/armeabi-v7a/libflutter.so移動(dòng)到lib/armeabi/libflutter.so即可:
cd $FLUTTER_ROOT/bin/cache/artifacts/engine for arch in android-arm android-arm-profile android-arm-release; dopushd $archcp flutter.jar flutter-armeabi-v7a.jar # 備份unzip flutter.jar lib/armeabi-v7a/libflutter.somv lib/armeabi-v7a lib/armeabizip -d flutter.jar lib/armeabi-v7a/libflutter.sozip flutter.jar lib/armeabi/libflutter.sopopd done這樣在打包后Flutter的SO庫就會(huì)打到APK的lib/armeabi目錄中。在運(yùn)行時(shí)如果設(shè)備不支持armeabi-v7a可能會(huì)崩潰,所以我們需要主動(dòng)識(shí)別并屏蔽掉這類設(shè)備,在Android上判斷設(shè)備是否支持armeabi-v7a也很簡單:
public static boolean isARMv7Compatible() {try {if (SDK_INT >= LOLLIPOP) {for (String abi : Build.SUPPORTED_32_BIT_ABIS) {if (abi.equals("armeabi-v7a")) {return true;}}} else {if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) {return true;}}} catch (Throwable e) {L.wtf(e);}return false; }灰度和自動(dòng)降級(jí)策略
Horn是一個(gè)美團(tuán)內(nèi)部的跨平臺(tái)配置下發(fā)SDK,使用Horn可以很方便地指定灰度開關(guān):
在條件配置頁面定義一系列條件,然后在參數(shù)配置頁面添加新的字段flutter即可:
因?yàn)樵诳蛻舳俗隽薃BI兜底策略,所以這里定義的ABI規(guī)則并沒有啟用。
Flutter目前仍然處于Beta階段,灰度過程中難免發(fā)生崩潰現(xiàn)象,觀察到崩潰后再針對(duì)機(jī)型或者設(shè)備ID來做降級(jí)雖然可以盡量降低影響,但是我們可以做到更迅速。外賣的Crash采集SDK同時(shí)也支持JNI Crash的收集,我們專門為Flutter注冊了崩潰監(jiān)聽器,一旦采集到Flutter相關(guān)的JNI Crash就立即停止該設(shè)備的Flutter功能,啟動(dòng)Flutter之前會(huì)先判斷FLUTTER_NATIVE_CRASH_FLAG文件是否存在,如果存在則表示該設(shè)備發(fā)生過Flutter相關(guān)的崩潰,很有可能是不兼容導(dǎo)致的問題,當(dāng)前版本周期內(nèi)在該設(shè)備上就不再使用Flutter功能。
除了崩潰以外,Flutter頁面中的Dart代碼也可能發(fā)生異常,例如服務(wù)器下發(fā)數(shù)據(jù)格式錯(cuò)誤導(dǎo)致解析失敗等等,Dart也提供了全局的異常捕獲功能:
import 'package:wm_app/plugins/wm_metrics.dart';void main() {runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) {uploadException("$obj\n$stack");}); }這樣我們就可以實(shí)現(xiàn)全方位的異常監(jiān)控和完善的降級(jí)策略,最大程度減少灰度時(shí)可能對(duì)用戶帶來的影響。
分析崩潰堆棧和異常數(shù)據(jù)
Flutter的引擎部分全部使用C/C++實(shí)現(xiàn),為了減少包大小,所有的SO庫在發(fā)布時(shí)都會(huì)去除符號(hào)表信息。和其他的JNI崩潰堆棧一樣,我們上報(bào)的堆棧信息中只能看到內(nèi)存地址偏移量等信息:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' Revision: '0' Author: collect by 'libunwind' ABI: 'arm64-v8a' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<< signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0backtrace:r0 00000000 r1 ffffffff r2 c0e7cb2c r3 c15affccr4 c15aff88 r5 c0e7cb2c r6 c15aff90 r7 bf567800r8 c0e7cc58 r9 00000000 sl c15aff0c fp 00000001ip 80000000 sp c0e7cb28 lr c11a03f9 pc c1254088 cpsr 200c0030#00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so#09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr單純這些信息很難定位問題,所以我們需要使用NDK提供的ndk-stack來解析出具體的代碼位置:
ndk-stack -sym PATH [-dump PATH] Symbolizes the stack trace from an Android native crash.-sym PATH sets the root directory for symbols-dump PATH sets the file containing the crash dump (default stdin)如果使用了定制過的引擎,必須使用engine/src/out/android-release下編譯出的libflutter.so文件。一般情況下我們使用的是官方版本的引擎,可以在flutter_infra頁面直接下載帶有符號(hào)表的SO文件,根據(jù)打包時(shí)使用的Flutter工具版本下載對(duì)應(yīng)的文件即可。比如0.4.4 beta版本:
$ flutter --version # version命令可以看到Engine對(duì)應(yīng)的版本 06afdfe54e Flutter 0.4.4 ? channel beta ? https://github.com/flutter/flutter.git Framework ? revision f9bb4289e9 (5 weeks ago) ? 2018-05-11 21:44:54 -0700 Engine ? revision 06afdfe54e Tools ? Dart 2.0.0-dev.54.0.flutter-46ab040e58 $ cat flutter/bin/internal/engine.version # flutter安裝目錄下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa 06afdfe54ebef9168a90ca00a6721c2d36e6aafa拿到引擎版本號(hào)后在 https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到該版本對(duì)應(yīng)的所有構(gòu)建產(chǎn)物,下載android-arm-release、android-arm64-release和android-x86目錄下的symbols.zip,并存放到對(duì)應(yīng)目錄:
執(zhí)行ndk-stack即可看到實(shí)際發(fā)生崩潰的代碼和具體行數(shù)信息:
ndk-stack -sym flutter-production-syms/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/armeabi-v7a -dump flutter_jni_crash.txt ********** Crash dump: ********** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<< signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 Stack frame #00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::WordBreaker::setText(unsigned short const*, unsigned int) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/WordBreaker.cpp:55 Stack frame #01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::LineBreaker::setText() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/LineBreaker.cpp:74 Stack frame #02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::ComputeLineBreaks() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:273 Stack frame #03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::Layout(double, bool) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:428 Stack frame #04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine blink::ParagraphImplTxt::layout(double) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/lib/ui/text/paragraph_impl_txt.cc:54 Stack frame #05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine tonic::DartDispatcher<tonic::IndicesHolder<0u>, void (blink::Paragraph::*)(double)>::Dispatch(void (blink::Paragraph::*)(double)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:150 Stack frame #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall<void (blink::Paragraph::*)(double)>(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:198 Stack frame #07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::AutoScopeNativeCallWrapperNoStackCheck(_Dart_NativeArguments*, void (*)(_Dart_NativeArguments*)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:198 Stack frame #08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::LinkNativeCall(_Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:348 Stack frame #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instrDart異常則比較簡單,默認(rèn)情況下Dart代碼在編譯成機(jī)器碼時(shí)并沒有去除符號(hào)表信息,所以Dart的異常堆棧本身就可以標(biāo)識(shí)真實(shí)發(fā)生異常的代碼文件和行數(shù)信息:
FlutterException: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'num' in type cast #0 _$CategoryGroupFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:29) #1 new CategoryGroup.fromJson (package:wm_app/all_category/model/category_model.dart:51) #2 _$CategoryListDataFromJson.<anonymous closure> (package:wm_app/lib/all_category/model/category_model.g.dart:5) #3 MappedListIterable.elementAt (dart:_internal/iterable.dart:414) #4 ListIterable.toList (dart:_internal/iterable.dart:219) #5 _$CategoryListDataFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:6) #6 new CategoryListData.fromJson (package:wm_app/all_category/model/category_model.dart:19) #7 _$AllCategoryResponseFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:19) #8 new AllCategoryResponse.fromJson (package:wm_app/all_category/model/category_model.dart:29) #9 AllCategoryPage.build.<anonymous closure> (package:wm_app/all_category/category_page.dart:46) <asynchronous suspension> #10 _WaimaiLoadingState.build (package:wm_app/all_category/widgets/progressive_loading_page.dart:51) #11 StatefulElement.build (package:flutter/src/widgets/framework.dart:3730) #12 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3642) #13 Element.rebuild (package:flutter/src/widgets/framework.dart:3495) #14 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2242) #15 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:626) #16 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:208) #17 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:990) #18 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:930) #19 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:842) #20 _rootRun (dart:async/zone.dart:1126) #21 _CustomZone.run (dart:async/zone.dart:1023) #22 _CustomZone.runGuarded (dart:async/zone.dart:925) #23 _invoke (dart:ui/hooks.dart:122) #24 _drawFrame (dart:ui/hooks.dart:109)Flutter和原生性能對(duì)比
雖然使用原生實(shí)現(xiàn)(左)和Flutter實(shí)現(xiàn)(右)的全品類頁面在實(shí)際使用過程中幾乎分辨不出來:
但是我們還需要在性能方面有一個(gè)比較明確的數(shù)據(jù)對(duì)比。
我們最關(guān)心的兩個(gè)頁面性能指標(biāo)就是頁面加載時(shí)間和頁面渲染速度。測試頁面加載速度可以直接使用美團(tuán)內(nèi)部的Metrics性能測試工具,我們將頁面Activity對(duì)象創(chuàng)建作為頁面加載的開始時(shí)間,頁面API數(shù)據(jù)返回作為頁面加載結(jié)束時(shí)間。從兩個(gè)實(shí)現(xiàn)的頁面分別啟動(dòng)400多次的數(shù)據(jù)中可以看到,原生實(shí)現(xiàn)(AllCategoryActivity)的加載時(shí)間中位數(shù)為210ms,Flutter實(shí)現(xiàn)(FlutterCategoryActivity)的加載時(shí)間中位數(shù)為231ms。考慮到目前我們還沒有針對(duì)FlutterView做緩存和重用,FlutterView每次創(chuàng)建都需要初始化整個(gè)Flutter環(huán)境并加載相關(guān)代碼,多出的20ms還在預(yù)期范圍內(nèi):
因?yàn)镕lutter的UI邏輯和繪制代碼都不在主線程執(zhí)行,Metrics原有的FPS功能無法統(tǒng)計(jì)到Flutter頁面的真實(shí)情況,我們需要用特殊方法來對(duì)比兩種實(shí)現(xiàn)的渲染效率。Android原生實(shí)現(xiàn)的界面渲染耗時(shí)使用系統(tǒng)提供的FrameMetrics接口進(jìn)行監(jiān)控:
public class AllCategoryActivity extends WmBaseActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {List<Integer> frameDurations = new ArrayList<>(100);@Overridepublic void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {frameDurations.add((int) (frameMetrics.getMetric(TOTAL_DURATION) / 1000000));if (frameDurations.size() == 100) {getWindow().removeOnFrameMetricsAvailableListener(this);L.w("AllCategory", Arrays.toString(frameDurations.toArray()));}}}, new Handler(Looper.getMainLooper()));}super.onCreate(savedInstanceState);// ...} }Flutter在Framework層只能取到每幀中UI操作的CPU耗時(shí),GPU操作都在Flutter引擎內(nèi)部實(shí)現(xiàn),所以需要修改引擎來監(jiān)控完整的渲染耗時(shí),在Flutter引擎目錄下的src/flutter/shell/common/rasterizer.cc文件中添加:
void Rasterizer::DoDraw(std::unique_ptr<flow::LayerTree> layer_tree) {if (!layer_tree || !surface_) {return;}if (DrawToSurface(*layer_tree)) {last_layer_tree_ = std::move(layer_tree); #if defined(OS_ANDROID)if (compositor_context_->frame_count().count() == 101) {std::ostringstream os;os << "[";const std::vector<TimeDelta> &engine_laps = compositor_context_->engine_time().Laps();const std::vector<TimeDelta> &frame_laps = compositor_context_->frame_time().Laps();size_t i = 1;for (auto engine_iter = engine_laps.begin() + 1, frame_iter = frame_laps.begin() + 1;i < 101 && engine_iter != engine_laps.end(); i++, engine_iter++, frame_iter++) {os << (*engine_iter + *frame_iter).ToMilliseconds() << ",";}os << "]";__android_log_write(ANDROID_LOG_WARN, "AllCategory", os.str().c_str());} #endif} }即可得到每幀繪制時(shí)真正消耗的時(shí)間。測試時(shí)我們將兩種實(shí)現(xiàn)的頁面分別打開100次,每次打開后執(zhí)行兩次滾動(dòng)操作,使其繪制100幀,將這100幀的每幀耗時(shí)記錄下來:
for (( i = 0; i < 100; i++ )); doopenWMPage allcategorysleep 1adb shell input swipe 500 1000 500 300 900adb shell input swipe 500 1000 500 300 900adb shell input keyevent 4 done將測試結(jié)果的100次啟動(dòng)中每幀耗時(shí)取平均値,得到每幀平均耗時(shí)情況(橫坐標(biāo)軸為幀序列,縱坐標(biāo)軸為每幀耗時(shí),單位為毫秒):
Android原生實(shí)現(xiàn)和Flutter版本都會(huì)在頁面打開的前5幀超過16ms,剛打開頁面時(shí)原生實(shí)現(xiàn)需要?jiǎng)?chuàng)建大量View,Flutter也需要?jiǎng)?chuàng)建大量Widget,后續(xù)幀中可以重用大部分控件和渲染節(jié)點(diǎn)(原生的RenderNode和Flutter的RenderObject),所以啟動(dòng)時(shí)的布局和渲染操作都是最耗時(shí)的。
10000幀(100次×100幀每次)中Android原生總平均値為10.21ms,Flutter總平均値為12.28ms,Android原生實(shí)現(xiàn)總丟幀數(shù)851幀8.51%,Flutter總丟幀987幀9.87%。在原生實(shí)現(xiàn)的觸摸事件處理和過度繪制充分優(yōu)化的前提下,Flutter完全可以媲美原生的性能。
總結(jié)
Flutter目前仍處于早期階段,也還沒有發(fā)布正式的Release版本,不過我們看到Flutter團(tuán)隊(duì)一直在為這一目標(biāo)而努力。雖然Flutter的開發(fā)生態(tài)不如Android和iOS原生應(yīng)用那么成熟,許多常用的復(fù)雜控件還需要自己實(shí)現(xiàn),有的甚至?xí)容^困難(比如官方尚未提供的ListView.scrollTo(index)功能),但是在高性能和跨平臺(tái)方面Flutter在眾多UI框架中還是有很大優(yōu)勢的。
開發(fā)Flutter應(yīng)用只能使用Dart語言,Dart本身既有靜態(tài)語言的特性,也支持動(dòng)態(tài)語言的部分特性,對(duì)于Java和JavaScript開發(fā)者來說門檻都不高,3-5天可以快速上手,大約1-2周可以熟練掌握。在開發(fā)全品類頁面的Flutter版本時(shí)我們也深刻體會(huì)到了Dart語言的魅力,Dart的語言特性使得Flutter的界面構(gòu)建過程也比Android原生的XML+JAVA更直觀,代碼量也從原來的900多行減少到500多行(排除掉引用的公共組件)。Flutter頁面集成到App后APK體積至少會(huì)增加5.5MB,其中包括3.3MB的SO庫文件和2.2MB的ICU數(shù)據(jù)文件,此外業(yè)務(wù)代碼1300行編譯產(chǎn)物的大小有2MB左右。
Flutter本身的特性適合追求iOS和Android跨平臺(tái)的一致體驗(yàn),追求高性能的UI交互效果的場景,不適合追求動(dòng)態(tài)化部署的場景。Flutter在Android上已經(jīng)可以實(shí)現(xiàn)動(dòng)態(tài)化部署,但是由于Apple的限制,在iOS上實(shí)現(xiàn)動(dòng)態(tài)化部署非常困難,Flutter團(tuán)隊(duì)也正在和Apple積極溝通。
美團(tuán)外賣大前端團(tuán)隊(duì)將來也會(huì)繼續(xù)在更多場景下使用Flutter實(shí)現(xiàn),并且將實(shí)踐過程中發(fā)現(xiàn)和修復(fù)的問題積極反饋到開源社區(qū),幫助Flutter更好地發(fā)展。如果你也對(duì)Flutter感興趣,歡迎加入。
參考資料
作者簡介
- 少杰,美團(tuán)高級(jí)工程師,2017年加入美團(tuán),目前主要負(fù)責(zé)外賣App監(jiān)控等基礎(chǔ)設(shè)施建設(shè)工作。
招聘信息
美團(tuán)外賣誠招Android、iOS、FE高級(jí)/資深工程師和技術(shù)專家,Base北京、上海、成都,歡迎有興趣的同學(xué)投遞簡歷到wukai05#meituan.com。
總結(jié)
以上是生活随笔為你收集整理的Flutter原理与实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 飞行器真的是计算机!
- 下一篇: Redis系列教程(五):Redis哨兵