日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Flutter原理与实践

發布時間:2024/7/5 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Flutter原理与实践 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Flutter是Google開發的一套全新的跨平臺、開源UI框架,支持iOS、Android系統開發,并且是未來新操作系統Fuchsia的默認開發套件。自從2017年5月發布第一個版本以來,目前Flutter已經發布了近60個版本,并且在2018年5月發布了第一個“Ready for Production Apps”的Beta 3版本,6月20日發布了第一個“Release Preview”版本。

初識Flutter

Flutter的目標是使同一套代碼同時運行在Android和iOS系統上,并且擁有媲美原生應用的性能,Flutter甚至提供了兩套控件來適配Android和iOS(滾動效果、字體和控件圖標等等)為了讓App在細節處看起來更像原生應用。

在Flutter誕生之前,已經有許多跨平臺UI框架的方案,比如基于WebView的Cordova、AppCan等,還有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。

基于WebView的框架優點很明顯,它們幾乎可以完全繼承現代Web開發的所有成果(豐富得多的控件庫、滿足各種需求的頁面框架、完全的動態化、自動化測試工具等等),當然也包括Web開發人員,不需要太多的學習和遷移成本就可以開發一個App。同時WebView框架也有一個致命(在對體驗&性能有較高要求的情況下)的缺點,那就是WebView的渲染效率和JavaScript執行性能太差。再加上Android各個系統版本和設備廠商的定制,很難保證所在所有設備上都能提供一致的體驗。

為了解決WebView性能差的問題,以React Native為代表的一類框架將最終渲染工作交還給了系統,雖然同樣使用類HTML+JS的UI構建邏輯,但是最終會生成對應的自定義原生控件,以充分利用原生控件相對于WebView的較高的繪制效率。與此同時這種策略也將框架本身和App開發者綁在了系統的控件系統上,不僅框架本身需要處理大量平臺相關的邏輯,隨著系統版本變化和API的變化,開發者可能也需要處理不同平臺的差異,甚至有些特性只能在部分平臺上實現,這樣框架的跨平臺特性就會大打折扣。

Flutter則開辟了一種全新的思路,從頭到尾重寫一套跨平臺的UI框架,包括UI控件、渲染邏輯甚至開發語言。渲染引擎依靠跨平臺的Skia圖形庫來實現,依賴系統的只有圖形繪制相關的接口,可以在最大程度上保證不同平臺、不同設備的體驗一致性,邏輯處理使用支持AOT的Dart語言,執行效率也比JavaScript高得多。

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風格的框架中使用控件樹和各自的狀態來構建界面,當某個控件的狀態發生變化時由框架負責對比前后狀態差異并且采取最小代價來更新渲染結果。

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: fluttershared_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>表示包含int類型的列表,List<dynamic>則表示包含任意類型的列表。
    • 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 App構建過程

    最簡單的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中修改:

    APKAAR
    修改android插件類型apply plugin: ‘com.android.application’apply plugin: ‘com.android.library’
    刪除applicationId字段applicationId “com.example.fluttertutorial”applicationId “com.example.fluttertutorial”
    建議添加發布所有配置功能,方便調試-defaultPublishConfig ‘release’
    publishNonDefault true

    簡單修改后我們就可以使用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.png new 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";@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頁面中注冊后,調用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; 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庫就會打到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 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頁面直接下載帶有符號表的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 {@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耗時,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++ )); doopenWMPage allcategorysleep 1adb shell input swipe 500 1000 500 300 900adb shell input swipe 500 1000 500 300 900adb 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中文官網
  • Flutter框架技術概覽
  • Flutter插件倉庫
  • A Tour of the Dart Language
  • A Tour of the Dart Libraries
  • Why Flutter Uses Dart
  • Flutter Layout機制簡介
  • Flutter’s Layered Design
  • Flutter’s Rendering Pipeline
  • Flutter: The Best Way to Build for Mobile?@GOTO conf
  • Flutter Engine
  • Writing custom platform-specific code with platform channels
  • Flutter Engine Operation in AOT Mode
  • Flutter’s modes
  • Symbolicating-production-crash-stacks
  • 作者簡介

    • 少杰,美團高級工程師,2017年加入美團,目前主要負責外賣App監控等基礎設施建設工作。

    招聘信息

    美團外賣誠招Android、iOS、FE高級/資深工程師和技術專家,Base北京、上海、成都,歡迎有興趣的同學投遞簡歷到wukai05#meituan.com。

    總結

    以上是生活随笔為你收集整理的Flutter原理与实践的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 青春草视频在线免费观看 | 在线观看视频毛片 | 玖玖久久 | 欧美成人一区在线 | 精品69| 牛牛精品视频 | 日韩福利在线视频 | 好男人在线视频www 亚洲福利国产 | 全黄一级裸体 | 艳妇臀荡乳欲伦交换在线看 | 日韩中文字幕2019 | 亚洲毛片在线观看 | 99热最新| 三级中文字幕 | 日韩成人福利视频 | 免费一二区 | 欲求不满在线小早川怜子 | 国产视频二 | 九热在线视频 | 人人艹在线观看 | 久久潮| 黄网站视频在线观看 | 奇米四色777 | 免费视频亚洲 | 欧美色图久久 | 日韩欧美中文字幕一区二区三区 | 91传媒视频在线观看 | 中文字幕精品视频在线观看 | 成人免费毛片视频 | 国产亚洲天堂 | 99re这里 | sese在线视频 | 两个人看的www视频免费完整版 | 国产午夜无码视频在线观看 | 国产精品久久久久久亚洲毛片 | 亚洲色图狠狠干 | 午夜黄色av | 久久这里 | 另类av在线| 欧美日韩电影一区二区 | 日本免费看 | 国产精品久久久久久久久动漫 | 粉嫩av.com | 国产日日操 | 国产精品极品白嫩 | 欧美少妇视频 | 国产激情无套内精对白视频 | 9色视频在线观看 | 精品国产一区二区三区四 | 国产无遮挡又黄又爽免费视频 | 亚洲国产影视 | 黄色的网站免费观看 | 日韩欧美超碰 | 成人激情小视频 | 午夜黄色福利视频 | 农民人伦一区二区三区 | 久久理论片 | 成年人免费在线 | 亚洲黄一区 | 久久666| 67194午夜| 狠狠人妻久久久久久综合 | 国产女人精品视频 | 成人国产免费 | 亚洲成人播放 | 黄色激情视频在线观看 | 国产一区在线视频观看 | 精品亚洲aⅴ无码一区二区三区 | 男人懂得网站 | 美女污软件 | 亚州黄色网址 | 一区二区三区韩国 | 日韩av无码一区二区三区不卡 | 亚洲天堂免费视频 | 亚洲艹| 黄色欧美大片 | 亚洲午夜小视频 | 97在线超碰 | 欧美在线不卡 | 18色av| 亚洲一区二区三区影视 | 成人免费网站黄 | 久久人妻一区二区 | 看av免费毛片手机播放 | 中文字幕日韩经典 | 天天躁日日躁狠狠躁 | 日韩六十路 | 草草在线影院 | 嫩草大剧院 | 日本一区二区三区电影在线观看 | 免费精品视频 | 国内三级在线 | 欧美丰满艳妇bbwbbw | 五十路母| 国产一区二区91 | 女优视频在线观看 | 在线 日本 制服 中文 欧美 | 久久精品99久久久久久 | 骚虎av|