服務項目

需求溝通

videoplayer

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

  初識Flutter

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

  基于WebView的框架優點很明顯,它們幾乎可以完全繼承現代Web開發的所有成果(豐富得多的控件庫、滿足各種需求的頁面框架、完全的動態化、自動化測試工具等等),當然也包括Web開發人員,不需要太多的學習和遷移成本就可以開發一個App。

  為了解決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: 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 { 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 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, RenderBoxContainerDefaultsMixin, 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創建,只負責創建對應的RenderObjectWidgetStatelessElement和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/*.dartthird_party/dart/runtime/lib/*.dartthird_party/dart/sdk/lib/_http/*.dartthird_party/dart/sdk/lib/async/*.dartthird_party/dart/sdk/lib/collection/*.dartthird_party/dart/sdk/lib/convert/*.dartthird_party/dart/sdk/lib/core/*.dartthird_party/dart/sdk/lib/developer/*.dartthird_party/dart/sdk/lib/html/*.dartthird_party/dart/sdk/lib/internal/*.dartthird_party/dart/sdk/lib/io/*.dartthird_party/dart/sdk/lib/isolate/*.dartthird_party/dart/sdk/lib/math/*.dartthird_party/dart/sdk/lib/mirrors/*.dartthird_party/dart/sdk/lib/profiler/*.dartthird_party/dart/sdk/lib/typed_data/*.dartthird_party/dart/sdk/lib/vmservice/*.dartflutter/lib/ui/*.dart

  platform.dill則是實現了頁面邏輯的代碼,也包括Flutter Framework和其他由pub依賴的庫代碼:

  flutter_tutorial_2/lib/main.dartflutter/packages/flutter/lib/src/widgets/*.dartflutter/packages/flutter/lib/src/services/*.dartflutter/packages/flutter/lib/src/semantics/*.dartflutter/packages/flutter/lib/src/scheduler/*.dartflutter/packages/flutter/lib/src/rendering/*.dartflutter/packages/flutter/lib/src/physics/*.dartflutter/packages/flutter/lib/src/painting/*.dartflutter/packages/flutter/lib/src/gestures/*.dartflutter/packages/flutter/lib/src/foundation/*.dartflutter/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.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> post(String path, [Map form]) async { return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) { return new Map.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() { @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 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/enginefor 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 popddone

  這樣在打包后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 <<

  單純這些信息很難定位問題,所以我們需要使用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對應的版本 06afdfe54eFlutter 0.4.4 • channel beta • https://github.com/flutter/flutter.gitFramework • revision f9bb4289e9 (5 weeks ago) • 2018-05-11 21:44:54 -0700Engine • revision 06afdfe54eTools • Dart 2.0.0-dev.54.0.flutter-46ab040e58$ cat flutter/bin/internal/engine.version # flutter安裝目錄下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa06afdfe54ebef9168a90ca00a6721c2d36e6aafa

  拿到引擎版本號后在 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 <<, 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:150Stack frame #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:198Stack 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:198Stack 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:348Stack frame #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr

  Dart異常則比較簡單,默認情況下Dart代碼在編譯成機器碼時并沒有去除符號表信息,所以Dart的異常堆棧本身就可以標識真實發生異常的代碼文件和行數信息:

  FlutterException: type '_InternalLinkedHashMap' 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. (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. (package:wm_app/all_category/category_page.dart:46)#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 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 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 &engine_laps = compositor_context_->engine_time().Laps(); const std::vector &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 1 adb shell input swipe 500 1000 500 300 900 adb shell input swipe 500 1000 500 300 900 adb shell input keyevent 4done

  將測試結果的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框架中還是有很大優勢的。


      溫馨提示:大連仟源科技有限公司匯聚眾多網絡精英與行業頂尖設計師、程序員,以WEB互動應用、移動互聯網應用、商業軟件為戰略方向。以成熟的技術、創新的理念、不斷開拓進取的精神服務于各行業客戶。

上一篇:dialogbox 下一篇:calendar.js
文章標簽:
文章評論:

專業的游戲開發/系統開發、品牌設計/網站建設,選仟源!

選擇專業的企業服務公司,服務更靠譜!

立即點擊咨詢>
客服圖標
客服圖標
118旺角心水论坛