導讀html
Flutter是Google開發的一套全新的跨平臺、開源UI框架,支持iOS、Android系統開發,而且是將來新操做系統Fuchsia的默認開發套件。自從2017年5月發佈第一個版本以來,目前Flutter已經發布了近60個版本,而且在2018年5月發佈了第一個「Ready for Production Apps」的Beta 3版本,6月20日發佈了第一個「Release Preview」版本。前端
初識Flutterreact
Flutter的目標是使同一套代碼同時運行在Android和iOS系統上,而且擁有媲美原生應用的性能,Flutter甚至提供了兩套控件來適配Android和iOS(滾動效果、字體和控件圖標等等),爲了讓App在細節處看起來更像原生應用。android
在Flutter誕生以前,已經有許多跨平臺UI框架的方案,好比基於WebView的Cordova、AppCan等,還有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。ios
基於WebView的框架優勢很明顯,它們幾乎能夠徹底繼承現代Web開發的全部成果(豐富得多的控件庫、知足各類需求的頁面框架、徹底的動態化、自動化測試工具等等),固然也包括Web開發人員,不須要太多的學習和遷移成本就能夠開發一個App。同時WebView框架也有一個致命(在對體驗&性能有較高要求的狀況下)的缺點,那就是WebView的渲染效率和JavaScript執行性能太差。再加上Android各個系統版本和設備廠商的定製,很難保證所在全部設備上都能提供一致的體驗。git
爲了解決WebView性能差的問題,以React Native爲表明的一類框架將最終渲染工做交還給了系統,雖然一樣使用類HTML+JS的UI構建邏輯,可是最終會生成對應的自定義原生控件,以充分利用原生控件相對於WebView的較高的繪製效率。與此同時這種策略也將框架自己和App開發者綁在了系統的控件系統上,不只框架自己須要處理大量平臺相關的邏輯,隨着系統版本變化和API的變化,開發者可能也須要處理不一樣平臺的差別,甚至有些特性只能在部分平臺上實現,這樣框架的跨平臺特性就會大打折扣。github
Flutter則開闢了一種全新的思路,從頭至尾重寫一套跨平臺的UI框架,包括UI控件、渲染邏輯甚至開發語言。渲染引擎依靠跨平臺的Skia圖形庫來實現,依賴系統的只有圖形繪製相關的接口,能夠在最大程度上保證不一樣平臺、不一樣設備的體驗一致性,邏輯處理使用支持AOT的Dart語言,執行效率也比JavaScript高得多。web
Flutter同時支持Windows、Linux和macOS操做系統做爲開發環境,而且在Android Studio和VS Code兩個IDE上都提供了全功能的支持。Flutter所使用的Dart語言同時支持AOT和JIT運行方式,JIT模式下還有一個備受歡迎的開發利器「熱刷新」(Hot Reload),即在Android Studio中編輯Dart代碼後,只須要點擊保存或者「Hot Reload」按鈕,就能夠當即更新到正在運行的設備上,不須要從新編譯App,甚至不須要重啓App,當即就能夠看到更新後的樣式。算法
在Flutter中,全部功能均可以經過組合多個Widget來實現,包括對齊方式、按行排列、按列排列、網格排列甚至事件處理等等。Flutter控件主要分爲兩大類,StatelessWidget和StatefulWidget,StatelessWidget用來展現靜態的文本或者圖片,若是控件須要根據外部數據或者用戶操做來改變的話,就須要使用StatefulWidget。State的概念也是來源於Facebook的流行Web框架React,React風格的框架中使用控件樹和各自的狀態來構建界面,當某個控件的狀態發生變化時由框架負責對比先後狀態差別而且採起最小代價來更新渲染結果。shell
Hot Reload
在Dart代碼文件中修改字符串「Hello, World」,添加一個驚歎號,點擊保存或者熱刷新按鈕就能夠當即更新到界面上,僅需幾百毫秒:
Flutter經過將新的代碼注入到正在運行的DartVM中,來實現Hot Reload這種神奇的效果,在DartVM將程序中的類結構更新完成後,Flutter會當即重建整個控件樹,從而更新界面。可是熱刷新也有一些限制,並非全部的代碼改動均可以經過熱刷新來更新:
編譯錯誤,若是修改後的Dart代碼沒法經過編譯,Flutter會在控制檯報錯,這時須要修改對應的代碼。
控件類型從StatelessWidget
到StatefulWidget
的轉換,由於Flutter在執行熱刷新時會保留程序原來的state,而某個控件從stageless→stateful後會致使Flutter從新建立控件時報錯「myWidget is not a subtype of StatelessWidget」,而從stateful→stateless會報錯「type 'myWidget' is not a subtype of type 'StatefulWidget' of 'newWidget'」。
全局變量和靜態成員變量,這些變量不會在熱刷新時更新。
修改了main函數中建立的根控件節點,Flutter在熱刷新後只會根據原來的根節點從新建立控件樹,不會修改根節點。
某個類從普通類型轉換成枚舉類型,或者類型的泛型參數列表變化,都會使熱刷新失敗。
熱刷新沒法實現更新時,執行一次熱重啓(Hot Restart)就能夠全量更新全部代碼,一樣不須要重啓App,區別是restart會將全部Dart代碼打包同步到設備上,而且全部狀態都會重置。
Flutter插件
Flutter使用的Dart語言沒法直接調用Android系統提供的Java接口,這時就須要使用插件來實現中轉。Flutter官方提供了豐富的原生接口封裝:
android_alarm_manager,訪問Android系統的AlertManager
。
android_intent,構造Android的Intent對象。
battery,獲取和監聽系統電量變化。
connectivity,獲取和監聽系統網絡鏈接狀態。
device info,獲取設備型號等信息。
image_picker,從設備中選取或者拍攝照片。
package_info,獲取App安裝包的版本等信息。
path_provider,獲取經常使用文件路徑。
quick_actions,App圖標添加快捷方式,iOS的eponymous concept和Android的App Shortcuts。
sensors,訪問設備的加速度和陀螺儀傳感器。
shared_preferences,App KV存儲功能。
url_launcher,啓動URL,包括打電話、發短信和瀏覽網頁等功能。
video_player,播放視頻文件或者網絡流的控件。
在Flutter中,依賴包由Pub倉庫管理,項目依賴配置在pubspec.yaml文件中聲明便可(相似於NPM的版本聲明Pub Versioning Philosophy),對於未發佈在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: flutter
shared_preferences: "^0.4.1"
脫字號「^」開頭的版本表示和當前版本接口保持兼容的最新版,^1.2.3
等效於 >=1.2.3 <2.0.0
而 ^0.1.2
等效於 >=0.1.2 <0.2.0
,添加依賴後點擊「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是一種強類型、跨平臺的客戶端開發語言。具備專門爲客戶端優化、高生產力、快速高效、可移植(兼容ARM/x86)、易學的OO編程風格和原生支持響應式編程(Stream & Future)等優秀特性。Dart主要由Google負責開發和維護,在2011年10啓動項目,2017年9月發佈第一個2.0-dev版本。
Dart自己提供了三種運行方式:
使用Dart2js編譯成JavaScript代碼,運行在常規瀏覽器中(Dart Web)。
使用DartVM直接在命令行中運行Dart代碼(DartVM)。
AOT方式編譯成機器碼,例如Flutter App框架(Flutter)。
Flutter在篩選了20多種語言後,最終選擇Dart做爲開發語言主要有幾個緣由:
健全的類型系統,同時支持靜態類型檢查和運行時類型檢查。
代碼體積優化(Tree Shaking),編譯時只保留運行時須要調用的代碼(不容許反射這樣的隱式引用),因此龐大的Widgets庫不會形成發佈體積過大。
豐富的底層庫,Dart自身提供了很是多的庫。
多生代無鎖垃圾回收器,專門爲UI框架中常見的大量Widgets對象建立和銷燬優化。
跨平臺,iOS和Android共用一套代碼。
JIT & AOT運行模式,支持開發時的快速迭代和正式發佈後最大程度發揮硬件性能。
在Dart中,有一些重要的基本概念須要瞭解:
全部變量的值都是對象,也就是類的實例。甚至數字、函數和null
也都是對象,都繼承自Object類。
雖然Dart是強類型語言,可是顯式變量類型聲明是可選的,Dart支持類型推斷。若是不想使用類型推斷,能夠用dynamic類型。
Dart支持泛型,List
表示包含int類型的列表,List
則表示包含任意類型的列表。
Dart支持頂層(top-level)函數和類成員函數,也支持嵌套函數和本地函數。
Dart支持頂層變量和類成員變量。
Dart沒有public、protected和private這些關鍵字,使用下劃線「_」開頭的變量或者函數,表示只在庫內可見。參考庫和可見性。
DartVM的內存分配策略很是簡單,建立對象時只須要在現有堆上移動指針,內存增加始終是線形的,省去了查找可用內存段的過程:
Dart中相似線程的概念叫作Isolate,每一個Isolate之間是沒法共享內存的,因此這種分配策略可讓Dart實現無鎖的快速分配。
Dart的垃圾回收也採用了多生代算法,新生代在回收內存時採用了「半空間」算法,觸發垃圾回收時Dart會將當前半空間中的「活躍」對象拷貝到備用空間,而後總體釋放當前空間的全部內存:
整個過程當中Dart只須要操做少許的「活躍」對象,大量的沒有引用的「死亡」對象則被忽略,這種算法也很是適合Flutter框架中大量Widget重建的場景。
Flutter Framework
Flutter的框架部分徹底使用Dart語言實現,而且有着清晰的分層架構。分層架構使得咱們能夠在調用Flutter提供的便捷開發功能(預約義的一套高質量Material控件)以外,還能夠直接調用甚至修改每一層實現(由於整個框架都屬於「用戶空間」的代碼),這給咱們提供了最大程度的自定義能力。Framework底層是Flutter引擎,引擎主要負責圖形繪製(Skia)、文字排版(libtxt)和提供Dart運行時,引擎所有使用C++實現,Framework層使咱們能夠用Dart語言調用引擎的強大能力。
分層架構
Framework的最底層叫作Foundation,其中定義的大都是很是基礎的、提供給其餘全部層使用的工具類和方法。繪製庫(Painting)封裝了Flutter Engine提供的繪製接口,主要是爲了在繪製控件等固定樣式的圖形時提供更直觀、更方便的接口,好比繪製縮放後的位圖、繪製文本、插值生成陰影以及在盒子周圍繪製邊框等等。
Animation是動畫相關的類,提供了相似Android系統的ValueAnimator的功能,而且提供了豐富的內置插值器。Gesture提供了手勢識別相關的功能,包括觸摸事件類定義和多種內置的手勢識別器。GestureBinding類是Flutter中處理手勢的抽象服務類,繼承自BindingBase類。
Binding系列的類在Flutter中充當着相似於Android中的SystemService系列(ActivityManager、PackageManager)功能,每一個Binding類都提供一個服務的單例對象,App最頂層的Binding會包含全部相關的Bingding抽象類。若是使用Flutter提供的控件進行開發,則須要使用WidgetsFlutterBinding,若是不使用Flutter提供的任何控件,而直接調用Render層,則須要使用RenderingFlutterBinding。
Flutter自己支持Android和iOS兩個平臺,除了性能和開發語言上的「native」化以外,它還提供了兩套設計語言的控件實現Material & Cupertino,能夠幫助App更好地在不一樣平臺上提供原生的用戶體驗。
渲染庫(Rendering)
Flutter的控件樹在實際顯示時會轉換成對應的渲染對象(RenderObject
)樹來實現佈局和繪製操做。通常狀況下,咱們只會在調試佈局,或者須要使用自定義控件來實現某些特殊效果的時候,才須要考慮渲染對象樹的細節。渲染庫主要提供的功能類有:
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引擎的膠水層,負責管理幀重繪、窗口尺寸和渲染相關參數變化的監聽。RenderObject
渲染樹中全部節點的基類,定義了佈局、繪製和合成相關的接口。RenderBox
和其三個經常使用的子類RenderParagraph
、RenderImage
、RenderFlex
則是具體佈局和繪製邏輯的實現類。
在Flutter界面渲染過程分爲三個階段:佈局、繪製、合成,佈局和繪製在Flutter框架中完成,合成則交由引擎負責:
控件樹中的每一個控件經過實現RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject
方法來建立對應的不一樣類型的RenderObject
對象,組成渲染對象樹。由於Flutter極大地簡化了佈局的邏輯,因此整個佈局過程當中只須要深度遍歷一次:
渲染對象樹中的每一個對象都會在佈局過程當中接受父對象的Constraints
參數,決定本身的大小,而後父對象就能夠按照本身的邏輯決定各個子對象的位置,完成佈局過程。
子對象不存儲本身在容器中的位置,因此在它的位置發生改變時並不須要從新佈局或者繪製。子對象的位置信息存儲在它本身的parentData
字段中,可是該字段由它的父對象負責維護,自身並不關心該字段的內容。同時也由於這種簡單的佈局邏輯,Flutter能夠在某些節點設置佈局邊界(Relayout boundary),即當邊界內的任何對象發生從新佈局時,不會影響邊界外的對象,反之亦然:
佈局完成後,渲染對象樹中的每一個節點都有了明確的尺寸和位置,Flutter會把全部對象繪製到不一樣的圖層上:
由於繪製節點時也是深度遍歷,能夠看到第二個節點在繪製它的背景和前景不得不繪製在不一樣的圖層上,由於第四個節點切換了圖層(由於「4」節點是一個須要獨佔一個圖層的內容,好比視頻),而第六個節點也一塊兒繪製到了紅色圖層。這樣會致使第二個節點的前景(也就是「5」)部分須要重繪時,和它在邏輯上絕不相干可是處於同一圖層的第六個節點也必須重繪。爲了不這種狀況,Flutter提供了另一個「重繪邊界」的概念:
在進入和走出重繪邊界時,Flutter會強制切換新的圖層,這樣就能夠避免邊界內外的互相影響。典型的應用場景就是ScrollView,當滾動內容重繪時,通常狀況下其餘內容是不須要重繪的。雖然重繪邊界能夠在任何節點手動設置,可是通常不須要咱們來實現,Flutter提供的控件默認會在須要設置的地方自動設置。
控件庫(Widgets)
Flutter的控件庫提供了很是豐富的控件,包括最基本的文本、圖片、容器、輸入框和動畫等等。在Flutter中「一切皆是控件」,經過組合、嵌套不一樣類型的控件,就能夠構建出任意功能、任意複雜度的界面。它包含的最主要的幾個類有:
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控件系統開發的程序都須要使用WidgetsFlutterBinding
,它是Flutter的控件框架和Flutter引擎的膠水層。Widget
就是全部控件的基類,它自己全部的屬性都是隻讀的。RenderObjectWidget
全部的實現類則負責提供配置信息並建立具體的RenderObjectElement
。Element
是Flutter用來分離控件樹和真正的渲染對象的中間層,控件用來描述對應的element屬性,控件重建後可能會複用同一個element。RenderObjectElement
持有真正負責佈局、繪製和碰撞測試(hit test)的RenderObject
對象。
StatelessWidget
和StatefulWidget
並不會直接影響RenderObject
建立,只負責建立對應的RenderObjectWidget
StatelessElement
和StatefulElement
也是相似的功能。
它們之間的關係以下圖:
若是控件的屬性發生了變化(由於控件的屬性是隻讀的,因此變化也就意味着從新建立了新的控件樹),可是其樹上每一個節點的類型沒有變化時,element樹和render樹能夠徹底重用原來的對象(由於element和render object的屬性都是可變的):
可是,若是控件樹種某個節點的類型發生了變化,則element樹和render樹中的對應節點也須要從新建立:
外賣全品類頁面實踐
在調研了Flutter的各項特性和實現原理以後,外賣計劃灰度上線Flutter版的全品類頁面。對於將Flutter頁面做爲App的一部分這種集成模式,官方並無提供完善的支持,因此咱們首先須要瞭解Flutter是如何編譯、打包而且運行起來的。
最簡單的Flutter工程至少包含兩個文件:
運行Flutter程序時須要對應平臺的宿主工程,在Android上Flutter經過自動建立一個Gradle項目來生成宿主,在項目目錄下執行flutter create .
,Flutter會建立ios和android兩個目錄,分別構建對應平臺的宿主項目,Android目錄內容以下:
此Gradle項目中只有一個app module,構建產物便是宿主APK。Flutter在本地運行時默認採用Debug模式,在項目目錄執行flutter run
便可安裝到設備中並自動運行,Debug模式下Flutter使用JIT方式來執行Dart代碼,全部的Dart代碼都會打包到APK文件中assets目錄下,由libflutter.so中提供的DartVM讀取並執行:
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/*.dart
platform.dill則是實現了頁面邏輯的代碼,也包括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/*.dart
kernel_blob.bin和platform.dill都是由flutter_tools中的bundle.dart中調用KernelCompiler生成。
在Release模式(flutter run --release
)下,Flutter會使用Dart的AOT運行模式,編譯時將Dart代碼轉換成ARM指令:
kernel_blob.bin和platform.dill都不在打包後的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四個文件。snapshot文件由Flutter SDK中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot
命令生成,vm_snapshot_*是Dart虛擬機運行所須要的數據和代碼指令,isolate_snapshot_*則是每一個isolate運行所須要的數據和代碼指令。
Flutter App運行機制
Flutter構建出的APK在運行時會將全部assets目錄下的資源文件解壓到App私有文件目錄中的flutter目錄下,主要包括處理字符編碼的icudtl.dat,還有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4個snapshot文件。默認狀況下Flutter在Application#onCreate
時調用FlutterMain#startInitialization
來啓動解壓任務,而後在FlutterActivityDelegate#onCreate
中調用FlutterMain#ensureInitializationComplete
來等待解壓任務結束。
Flutter在Debug模式下使用JIT執行方式,主要是爲了支持廣受歡迎的熱刷新功能:
觸發熱刷新時Flutter會檢測發生改變的Dart文件,將其同步到App私有緩存目錄下,DartVM加載而且修改對應的類或者方法,重建控件樹後當即能夠在設備上看到效果。
在Release模式下Flutter會直接將snapshot文件映射到內存中執行其中的指令:
在Release模式下,FlutterActivityDelegate#onCreate
中調用FlutterMain#ensureInitializationComplete
方法中會將AndroidManifest中設置的snapshot(沒有設置則使用上面提到的默認值)文件名等運行參數設置到對應的C++同名類對象中,構造FlutterNativeView
實例時調用nativeAttach
來初始化DartVM,運行編譯好的Dart代碼。
打包Android Library
瞭解Flutter項目的構建和運行機制後,咱們就能夠按照其需求打包成AAR而後集成到現有原生App中了。首先在andorid/app/build.gradle中修改:
簡單修改後咱們就可使用Android Studio或者Gradle命令行工具將Flutter代碼打包到aar中了。Flutter運行時所須要的資源都會包含在aar中,將其發佈到maven服務器或者本地maven倉庫後,就能夠在原生App項目中引用。
但這只是集成的第一步,爲了讓Flutter頁面無縫銜接到外賣App中,咱們須要作的還有不少。
Flutter默認將全部的圖片資源文件打包到assets目錄下,可是咱們並非用Flutter開發全新的頁面,圖片資源原來都會按照Android的規範放在各個drawable目錄,即便是全新的頁面也會有不少圖片資源複用的場景,因此在assets目錄下新增圖片資源並不合適。
Flutter官方並無提供直接調用drawable目錄下的圖片資源的途徑,畢竟drawable這類文件的處理會涉及大量的Android平臺相關的邏輯(屏幕密度、系統版本、語言等等),assets目錄文件的讀取操做也在引擎內部使用C++實現,在Dart層面實現讀取drawable文件的功能比較困難。Flutter在處理assets目錄中的文件時也支持添加多倍率的圖片資源,並可以在使用時自動選擇,可是Flutter要求每一個圖片必須提供1x圖,而後纔會識別到對應的其餘倍率目錄下的圖片:
flutter:
assets:
- images/cat.png
- images/2x/cat.png
- images/3.5x/cat.pngnew Image.asset('images/cat.png');
這樣配置後,才能正確地在不一樣分辨率的設備上使用對應密度的圖片。可是爲了減少APK包體積咱們的位圖資源通常只提供經常使用的2x分辨率,其餘分辨率的設備會在運行時自動縮放到對應大小。針對這種特殊的狀況,咱們在不增長包體積的前提下,一樣提供了和原生App同樣的能力:
在調用Flutter頁面以前將指定的圖片資源按照設備屏幕密度縮放,並存儲在App私有目錄下。
Flutter中使用時經過自定義的WMImage
控件來加載,實際是經過轉換成FileImage並自動設置scale爲devicePixelRatio來加載。
這樣就能夠同時解決APK包大小和圖片資源缺失1x圖的問題。
Flutter和原生代碼的通訊
咱們只用Flutter實現了一個頁面,現有的大量邏輯都是用Java實現,在運行時會有許多場景必須使用原生應用中的邏輯和功能,例如網絡請求,咱們統一的網絡庫會在每一個網絡請求中添加許多通用參數,也會負責成功率等指標的監控,還有異常上報,咱們須要在捕獲到關鍵異常時將其堆棧和環境信息上報到服務器。這些功能不太可能當即使用Dart實現一套出來,因此咱們須要使用Dart提供的Platform Channel功能來實現Dart→Java之間的互相調用。
以網絡請求爲例,咱們在Dart中定義一個MethodChannel
對象:
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端實現相同名稱的MethodChannel:
public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {
private static final String CHANNEL_NAME = "com.sankuai.waimai/network";
@Override
public 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>() {
@Override
public void onError(Throwable e) {
result.error(e.getClass().getCanonicalName(), e.getMessage(), null);
}
@Override
public void onNext(Map stringBaseResponse) {
result.success(stringBaseResponse);
}
}, tag);
break;
default:
result.notImplemented();
break;
}
}
}
在Flutter頁面中註冊後,調用post方法就能夠調用對應的Java實現:
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架構的SO庫:armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模式,可是外賣使用的大量SDK都只提供了armeabi架構的庫。
雖然咱們能夠經過修改引擎src
根目錄和third_party/dart
目錄下build/config/arm.gni
,third_party/skia
目錄下的BUILD.gn
等配置文件來編譯出armeabi版本的Flutter引擎,可是實際上市面上絕大部分設備都已經支持armeabi-v7a,其提供的硬件加速浮點運算指令能夠大大提升Flutter的運行速度,在灰度階段咱們能夠主動屏蔽掉不支持armeabi-v7a的設備,直接使用armeabi-v7a版本的引擎。
作到這點咱們首先須要修改Flutter提供的引擎,在Flutter安裝目錄下的bin/cache/artifacts/engine
下有Flutter下載的全部平臺的引擎:
咱們只須要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,將其中的lib/armeabi-v7a/libflutter.so移動到lib/armeabi/libflutter.so便可:
cd $FLUTTER_ROOT/bin/cache/artifacts/engine
for arch in android-arm android-arm-profile android-arm-release; do
pushd $arch
cp flutter.jar flutter-armeabi-v7a.jar # 備份
unzip flutter.jar lib/armeabi-v7a/libflutter.so
mv lib/armeabi-v7a lib/armeabi
zip -d flutter.jar lib/armeabi-v7a/libflutter.so
zip flutter.jar lib/armeabi/libflutter.so
popd
done
這樣在打包後Flutter的SO庫就會打到APK的lib/armeabi目錄中。在運行時若是設備不支持armeabi-v7a可能會崩潰,因此咱們須要主動識別並屏蔽掉這類設備,在Android上判斷設備是否支持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;
}
灰度和自動降級策略
Horn是一個美團內部的跨平臺配置下發SDK,使用Horn能夠很方便地指定灰度開關:
在條件配置頁面定義一系列條件,而後在參數配置頁面添加新的字段flutter便可:
由於在客戶端作了ABI兜底策略,因此這裏定義的ABI規則並無啓用。
Flutter目前仍然處於Beta階段,灰度過程當中不免發生崩潰現象,觀察到崩潰後再針對機型或者設備ID來作降級雖然能夠儘可能下降影響,可是咱們能夠作到更迅速。外賣的Crash採集SDK同時也支持JNI Crash的收集,咱們專門爲Flutter註冊了崩潰監聽器,一旦採集到Flutter相關的JNI Crash就當即中止該設備的Flutter功能,啓動Flutter以前會先判斷FLUTTER_NATIVE_CRASH_FLAG
文件是否存在,若是存在則表示該設備發生過Flutter相關的崩潰,頗有多是不兼容致使的問題,當前版本週期內在該設備上就再也不使用Flutter功能。
除了崩潰之外,Flutter頁面中的Dart代碼也可能發生異常,例如服務器下發數據格式錯誤致使解析失敗等等,Dart也提供了全局的異常捕獲功能:
import 'package:wm_app/plugins/wm_metrics.dart';
void main() {
runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) {
uploadException("$obj\n$stack");
});
}
這樣咱們就能夠實現全方位的異常監控和完善的降級策略,最大程度減小灰度時可能對用戶帶來的影響。
分析崩潰堆棧和異常數據
Flutter的引擎部分所有使用C/C++實現,爲了減小包大小,全部的SO庫在發佈時都會去除符號表信息。和其餘的JNI崩潰堆棧同樣,咱們上報的堆棧信息中只能看到內存地址偏移量等信息:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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 0x0
backtrace:
r0 00000000 r1 ffffffff r2 c0e7cb2c r3 c15affcc
r4 c15aff88 r5 c0e7cb2c r6 c15aff90 r7 bf567800
r8 c0e7cc58 r9 00000000 sl c15aff0c fp 00000001
ip 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頁面直接下載帶有符號表的SO文件,根據打包時使用的Flutter工具版本下載對應的文件便可。好比0.4.4 beta版本:
$ flutter --version # version命令能夠看到Engine對應的版本 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
拿到引擎版本號後在https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到該版本對應的全部構建產物,下載android-arm-release、android-arm64-release和android-x86目錄下的symbols.zip,並存放到對應目錄:
執行ndk-stack便可看到實際發生崩潰的代碼和具體行數信息:
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_instr
Dart異常則比較簡單,默認狀況下Dart代碼在編譯成機器碼時並無去除符號表信息,因此Dart的異常堆棧自己就能夠標識真實發生異常的代碼文件和行數信息:
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和原生性能對比
雖然使用原生實現(左)和Flutter實現(右)的全品類頁面在實際使用過程當中幾乎分辨不出來:
可是咱們還須要在性能方面有一個比較明確的數據對比。
咱們最關心的兩個頁面性能指標就是頁面加載時間和頁面渲染速度。測試頁面加載速度能夠直接使用美團內部的Metrics性能測試工具,咱們將頁面Activity對象建立做爲頁面加載的開始時間,頁面API數據返回做爲頁面加載結束時間。
從兩個實現的頁面分別啓動400屢次的數據中能夠看到,原生實現(AllCategoryActivity)的加載時間中位數爲210ms,Flutter實現(FlutterCategoryActivity)的加載時間中位數爲231ms。考慮到目前咱們尚未針對FlutterView作緩存和重用,FlutterView每次建立都須要初始化整個Flutter環境並加載相關代碼,多出的20ms還在預期範圍內:
由於Flutter的UI邏輯和繪製代碼都不在主線程執行,Metrics原有的FPS功能沒法統計到Flutter頁面的真實狀況,咱們須要用特殊方法來對比兩種實現的渲染效率。Android原生實現的界面渲染耗時使用系統提供的FrameMetrics
接口進行監控:
public class AllCategoryActivity extends WmBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
List<Integer> frameDurations = new ArrayList<>(100);
@Override
public 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耗時,GPU操做在Flutter引擎內部實現,因此要修改引擎來監控完整的渲染耗時,在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
}
}
便可獲得每幀繪製時真正消耗的時間。測試時咱們將兩種實現的頁面分別打開100次,每次打開後執行兩次滾動操做,使其繪製100幀,將這100幀的每幀耗時記錄下來:
for (( i = 0; i < 100; i++ )); do
openWMPage allcategory
sleep
adb shell input swipe 500 1000 500 300 900
adb shell input swipe 500 1000 500 300 900
adb shell input keyevent 4
done
將測試結果的100次啓動中每幀耗時取平均値,獲得每幀平均耗時狀況(橫座標軸爲幀序列,縱座標軸爲每幀耗時,單位爲毫秒):
Android原生實現和Flutter版本都會在頁面打開的前5幀超過16ms,剛打開頁面時原生實現須要建立大量View,Flutter也須要建立大量Widget,後續幀中能夠重用大部分控件和渲染節點(原生的RenderNode和Flutter的RenderObject),因此啓動時的佈局和渲染操做都是最耗時的。
10000幀(100次×100幀每次)中Android原生總平均値爲10.21ms,Flutter總平均値爲12.28ms,Android原生實現總丟幀數851幀8.51%,Flutter總丟幀987幀9.87%。在原生實現的觸摸事件處理和過分繪製充分優化的前提下,Flutter徹底能夠媲美原生的性能。
Flutter目前仍處於早期階段,也尚未發佈正式的Release版本,不過咱們看到Flutter團隊一直在爲這一目標而努力。雖然Flutter的開發生態不如Android和iOS原生應用那麼成熟,許多經常使用的複雜控件還須要本身實現,有的甚至會比較困難(好比官方還沒有提供的ListView.scrollTo(index)功能),可是在高性能和跨平臺方面Flutter在衆多UI框架中仍是有很大優點的。
開發Flutter應用只能使用Dart語言,Dart自己既有靜態語言的特性,也支持動態語言的部分特性,對於Java和JavaScript開發者來講門檻都不高,3-5天能夠快速上手,大約1-2周能夠熟練掌握。
在開發全品類頁面的Flutter版本時咱們也深入體會到了Dart語言的魅力,Dart的語言特性使得Flutter的界面構建過程也比Android原生的XML+JAVA更直觀,代碼量也從原來的900多行減小到500多行(排除掉引用的公共組件)。Flutter頁面集成到App後APK體積至少會增長5.5MB,其中包括3.3MB的SO庫文件和2.2MB的ICU數據文件,此外業務代碼1300行編譯產物的大小有2MB左右。
Flutter自己的特性適合追求iOS和Android跨平臺的一致體驗,追求高性能的UI交互效果的場景,不適合追求動態化部署的場景。Flutter在Android上已經能夠實現動態化部署,可是因爲Apple的限制,在iOS上實現動態化部署很是困難,Flutter團隊也正在和Apple積極溝通。
美團外賣大前端團隊未來也會繼續在更多場景下使用Flutter實現,而且將實踐過程當中發現和修復的問題積極反饋到開源社區,幫助Flutter更好地發展。若是你也對Flutter感興趣,歡迎加入咱們。
若是有問題,能夠掃描下方二維碼,加入美團Flutter俱樂部,跟做者直接進行交流。
如羣已滿,加美團小助手微信 MTDPtech,回覆【Flutter】,小助手拉你入羣
參考資料
做者簡介
少傑,美團高級工程師,2017年加入美團,目前主要負責外賣App監控等基礎設施建設工做。
招聘信息
美團外賣誠招Android、iOS、FE高級/資深工程師和技術專家,Base北京、上海、成都,歡迎有興趣的同窗投遞簡歷到 wukai05#meituan.com。
也許你還想看