整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          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會立即重建整個控件樹,從而更新界面。但是熱刷新也有一些限制,并不是所有的代碼改動都可以通過熱刷新來更新:

          1. 編譯錯誤,如果修改后的Dart代碼無法通過編譯,Flutter會在控制臺報錯,這時需要修改對應的代碼。
          2. 控件類型從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'”。
          3. 全局變量和靜態成員變量,這些變量不會在熱刷新時更新。
          4. 修改了main函數中創建的根控件節點,Flutter在熱刷新后只會根據原來的根節點重新創建控件樹,不會修改根節點。
          5. 某個類從普通類型轉換成枚舉類型,或者類型的泛型參數列表變化,都會使熱刷新失敗。

          熱刷新無法實現更新時,執行一次熱重啟(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本身提供了三種運行方式:

          1. 使用Dart2js編譯成JavaScript代碼,運行在常規瀏覽器中(Dart Web)。
          2. 使用DartVM直接在命令行中運行Dart代碼(DartVM)。
          3. AOT方式編譯成機器碼,例如Flutter App框架(Flutter)。

          Flutter在篩選了20多種語言后,最終選擇Dart作為開發語言主要有幾個原因:

          1. 健全的類型系統,同時支持靜態類型檢查和運行時類型檢查。
          2. 代碼體積優化(Tree Shaking),編譯時只保留運行時需要調用的代碼(不允許反射這樣的隱式引用),所以龐大的Widgets庫不會造成發布體積過大。
          3. 豐富的底層庫,Dart自身提供了非常多的庫。
          4. 多生代無鎖垃圾回收器,專門為UI框架中常見的大量Widgets對象創建和銷毀優化。
          5. 跨平臺,iOS和Android共用一套代碼。
          6. 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創建,只負責創建對應的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/*.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.png
          new Image.asset('images/cat.png');

          這樣配置后,才能正確地在不同分辨率的設備上使用對應密度的圖片。但是為了減小APK包體積我們的位圖資源一般只提供常用的2x分辨率,其他分辨率的設備會在運行時自動縮放到對應大小。針對這種特殊的情況,我們在不增加包體積的前提下,同樣提供了和原生App一樣的能力:

          1. 在調用Flutter頁面之前將指定的圖片資源按照設備屏幕密度縮放,并存儲在App私有目錄下。
          2. 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 1
              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積極溝通。

          于注解,相信大家都不陌生了,但是這種熟悉對于Android開發者來說,也就是僅僅是它認識你,你不認識它吧,因為,我們幾乎都停留在用上了。像dagger2,像retrofit,像greenDao,他們都是滿滿的使用了注解,而我們只是停留在知其然缺不知其所以然的層面,那么我本次分享講分成幾批博客共享給大家一個體系的又不失針對Android平臺需要的恰到好處的給大家整理真正需要掌握的知識體系。

          注解

          注解的定義

          Java 注解用于為 Java 代碼提供元數據。作為元數據,注解不直接影響你的代碼執行,但也有一些類型的注解實際上可以用于這一目的。Java 注解是從 Java5 開始添加到 Java 的。

          注解即標簽

          如果把代碼想象成一個具有生命的個體,注解就是給這些代碼的某些個體打標簽

          如何自定義注解

          • 注解通過 @interface關鍵字進行定義。
          public @interface Test {
          }
          

          它的形式跟接口很類似,不過前面多了一個 @ 符號。上面的代碼就創建了一個名字為 Test 的注解。

          你可以簡單理解為創建了一張名字為 Test的標簽。

          • 使用注解
          @Test
          public class TestAnnotation {
          }
          

          創建一個類 TestAnnotation,然后在類定義的地方加上 @Test就可以用 Test注解這個類了

          你可以簡單理解為將 Test 這張標簽貼到 TestAnnotation這個類上面。

          元注解

          元注解是可以注解到注解上的注解,或者說元注解是一種基本注解,但是它能夠應用到其它的注解上面。

          如果難于理解的話,你可以這樣理解。元注解也是一張標簽,但是它是一張特殊的標簽,它的作用和目的就是給其他普通的標簽進行解釋說明的。

          元標簽有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。

          • @Retention
          • Retention 的英文意為保留期的意思。當 @Retention 應用到一個注解上的時候,它解釋說明了這個注解的的存活時間。
          • 它的取值如下:
          1. RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
          2. RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中。
          3. RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們
          • @Target
          • Target 是目標的意思,@Target 指定了注解運用的地方
          • 你可以這樣理解,當一個注解被 @Target 注解時,這個注解就被限定了運用的場景。
          • 類比到標簽,原本標簽是你想張貼到哪個地方就到哪個地方,但是因為 @Target 的存在,它張貼的地方就非常具體了,比如只能張貼到方法上、類上、方法參數上等等。@Target 有下面的取值
          1. ElementType.ANNOTATION_TYPE 可以給一個注解進行注解
          2. ElementType.CONSTRUCTOR 可以給構造方法進行注解
          3. ElementType.FIELD 可以給屬性進行注解
          4. ElementType.LOCAL_VARIABLE 可以給局部變量進行注解
          5. ElementType.METHOD 可以給方法進行注解
          6. ElementType.PACKAGE 可以給一個包進行注解
          7. ElementType.PARAMETER 可以給一個方法內的參數進行注解
          • @Documented
          • 顧名思義,這個元注解肯定是和文檔有關。它的作用是能夠將注解中的元素包含到 Javadoc 中去。ElementType.TYPE 可以給一個類型進行注解,比如類、接口、枚舉
          • @Inherited
          • Inherited 是繼承的意思,但是它并不是說注解本身可以繼承,而是說如果一個超類被 @Inherited 注解過的注解進行注解的話,那么如果它的子類沒有被任何注解應用的話,那么這個子類就繼承了超類的注解。
          • @Repeatable
          • Repeatable 自然是可重復的意思。@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。
          • 什么樣的注解會多次應用呢?通常是注解的值可以同時取多個。

          注解的屬性

          注解的屬性也叫做成員變量。注解只有成員變量,沒有方法。

          需要注意的是,在注解中定義屬性時它的類型必須是 8 種基本數據類型外加 類、接口、注解及它們的數組

          注解中屬性可以有默認值,默認值需要用 default 關鍵值指定

          @Target(ElementType.TYPE)
          @Retention(RetentionPolicy.RUNTIME)
          public @interface Test{
          int id() default -1;
          String msg() default "Hello";
          }
          

          上面代碼定義了 TestAnnotation 這個注解中擁有 id 和 msg 兩個屬性。在使用的時候,我們應該給它們進行賦值。

          賦值的方式是在注解的括號內以 value="" 形式,多個屬性之前用 ,隔開

          @Test(id=1,msg="hello annotation")
          public class TestAnnotation {
          }
          

          注解的提取

          注解與反射。

          注解通過反射獲取。首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個注解

          public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
          

          然后通過 getAnnotation() 方法來獲取 Annotation 對象。

           public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
          

          或者是 getAnnotations() 方法。

          public Annotation[] getAnnotations() {}
          

          前一種方法返回指定類型的注解,后一種方法返回注解到這個元素上的所有注解。

          如果獲取到的 Annotation 如果不為 null,則就可以調用它們的屬性方法了。比如

          @Test()
          public class TestDemo{
          public static void main(String[] args) {
           boolean hasAnnotation = TestDemo.class.isAnnotationPresent(Test.class);
           if ( hasAnnotation ) {
           TestAnnotation testAnnotation = TestDemo.class.getAnnotation(Test.class);
           System.out.println("id:"+testAnnotation.id());
           System.out.println("msg:"+testAnnotation.msg());
           }
           }
          }
          

          注解的使用場景

          • 提供信息給編譯器: 編譯器可以利用注解來探測錯誤和警告信息
          • 編譯階段時的處理: 軟件工具可以用來利用注解信息來生成代碼、Html文檔或者做其它相應處理。
          • 運行時的處理: 某些注解可以在程序運行的時候接受代碼的提取
          • 值得注意的是,注解不是代碼本身的一部分。

          作為一個資歷不淺的Android開發,這幾年我面試過不少人。發現大多數面試者,雖然看起來工作努力,但他們表現出來的能力水平,卻不足以通過面試,或拿不到期望的薪資。

          在我看來,造成這種情況的原因,主要有這么兩方面:

          第一,“知其然不知其所以然”。做了幾年技術,開發了一些業務應用,但沒有思考過這些技術選擇背后的邏輯。所以,我很難定位他們日后的成長潛力,也不會放心把有一定深度的任務交給他們。

          第二,知識碎片化,不成系統。事實上,當面試者無法完整、清晰地描述自己所開發的系統或使用的相關技術時,面試官就會懷疑他是否具備解決復雜問題、設計復雜系統的能力。

          所以,如果你平時只知道埋頭苦干,或過于死磕某個實現細節,沒有抬頭審視過這些技術,那么在準備面試時,很有必要好好梳理一下 Android 知識體系,這樣才能拿下滿意的 Offer。

          那么,作為Android 開發者,該怎樣規劃自己的學習路線,然后一步一個腳印的向中高級進階呢?本篇文章根據自己的一些見解梳理一下。

          Android開發初期之后怎么提升?怎么才能叫精通?方向在哪?

          我發現好多人始終停留在兩三年的水平上沒有突破。

          那么為什么很多人會一直停留在兩三年的水平上,而后一直在重復以往的經驗?

          我認為最主要的一點就是主觀能動性,或者說興趣,如果你對Android開發沒有太大的興趣,那么還是盡早換方向吧。有了興趣,然后就是要有一個比較正確的鉆研路線,不要這也搞那也抓,最后什么都沒精通又好像什么知道。

          我覺得一個比較好的路線是,把日常開發常用的各種系統庫,開源庫,代碼好好研究一遍,比如我用了butternife就要了解背后的apt,以及apt衍生出來的一系列庫比如Hugo。當然這大部分情況是初級進階的第一步。這個階段應該是盡量很好的用技術完成業務需求。

          第二個階段,我覺得可以嘗試去了解Android Framework比較細節的一些東西,比如activity啟動流程,順便分析清除Activity stack的管理,比如了解Android的資源加載機制,順便了解aapt是如何打包Android資源的;又比如Java的類加載機制,這里配合資源的加載機制,很自然的就可以去了解Android的hotpatch機制,插件化的實現,開一些這方便的開源庫或者自己擼一個也就自然而然。

          第三個階段,橫向擴展,到這個階段并不是說比第二個階段更加高級了,而是對第二個階段的一些補充,比如你是不是可以了解一下web開發,這樣做hybrid開發的時候就會更順手。是不是要了解一下這么火爆的ReactNative&Weex技術,甚至可以把他們的一些思想拿過來自己用,比如我司內部就有很多項目是用了JSCore和CssLayout來實現一些更輕量的動態化技術的。正如科學領域很多重大貢獻都是在交叉學科領域出現的。技術上到了這個階段甚至可以做到技術影響業務,技術驅動業務。

          不要學得太雜太亂!學習Android開發只要記住這幾點!

          我也已經做了好幾年開發了,還記得剛出來工作的時候覺得自己很牛逼,現在回想起來也蠻好笑的。懂的越多的時候你才會發現懂的越少。因為如果你的知識是一個圓,當你的圓越大時,圓外面的世界也就越大。

          最近看到很多Android新手問Android學習路線,學習方法啊,如何入門啊,所以我以我的工作經驗給大家總結一下,讓大家少走彎路,提取一些工作中經常用到的技術。當然,說一千道一萬,最重要的還是學以致用,把學過的知識融會貫通。

          如何知道我需要學什么

          相信大家在覺得迷茫的時候,經常會選擇自暴自棄、或者完全按照自己的喜好去學習,這是有利有弊的,總體來說弊大于利。我一直認同一個觀點,就是“你的認知比你的知識更加重要”,換句話說,也就是“你知道你需要去學什么,比你所學的知識更加重要”,如何知道自己缺乏什么,應該去學習哪些技能,這才是從思維上、思想上改變你對學習的態度!

          如何知道自己應該具備哪些技能呢?或者說,我自己還缺乏哪些技能呢?

          這個問題一直是困擾著很多人,解答這個問題其實很簡單,這個問題的回答,往往離不開行業的最新動態:從下面培訓機構的培訓清單、招聘信息的招聘要求就可以看出來。下面先介紹如何從培訓機構的培訓清單開出行業的動態。

          培訓機構一直是互聯網行業的風向標,培訓機構的嗅覺是十分敏銳的,他們必須關注行業的最新技術方向、最新技術。所以,在一定程度上面,雖然我們大多數人都挺抵觸培訓機構,但是培訓機構的培訓清單可以作為我們學習的風向標、學習清單

          例子1:(介紹一下往高級Android開發工程師進階需要具備的哪些知識)

          • 架構師筑基必備技能:深入Java泛型+注解深入淺出+并發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO
          • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化
          • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化
          • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack
          • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發
          • 微信小程序:小程序介紹+UI開發+API操作+微信對接
          • Hybrid 開發與Flutter:Html5項目實戰+Flutter進階

          其實這個培訓清單我個人覺得是目前來說總結得比較好的Android進階資料。基本涵蓋了各種企業開發必備的新技術:RN、異步操作RXJava庫、熱修復、插件化、設計模式、性能優化等知識。這些其實都是我們學習的方向標,按照這些清單,自己去找各種資料學習。

          其實對于Android基礎的話,不外乎四大組件的基本使用、UI控件、布局的使用、版本控制工具、NDK基礎等等。

          當然隨著Android技術的日新月異,Android開發對開發人員的基礎知識的要求就更上一層樓了,比如說:動態權限管理、Kotlin基礎、Gradle基礎、AndroidStudio基本使用等等。

          例子2:(從招聘信息上了解目前互聯網大廠需要學習的一些熱門技術)

          我一直都強調要多看招聘信息,雖然我們不一定要找工作,但是按照企業的招聘要求,一定程度上也可以知道我們需要什么,并且同時可以知道,越高級的Android開發有什么更高的要求。

          我們常見的招聘網站有:智聯招聘、拉勾網、boss直聘等,這里大家手機上安裝一個對應的APP,吃飯或者上班路上沒事可以翻一翻。那么如何通過招聘信息獲取我們需要什么呢?那么就需要我們仔細分析下面幾份招聘信息。

          大家第一時間關注的肯定是薪水問題,20K到50K,可以看到,這是一份相對來說比較高級Android開發工程師的招聘信息,根據上面的要求,其實我們可以知道更多。

          一般人可能會覺得這是一份很普通的招聘要求,但是在我的角度來看,可以看出很多有用的信息,下面我帶著大家來分析解讀一下這份招聘要求吧。

          1. Android底層的機制的熟悉,關于這個問題,其實在這里我并不想談論太多,因為本系列文章的后續我會專門有一篇文章講這個怎么去學習。一句話總結就是,熟悉Android底層機制,或者說Android源碼,對上層應用的編程開發是有很多好處的,例如寫出高可復用性、高效率、高質量的代碼。其中,面試常問的源碼分析有消息機制、四大組件、進程間通信、WMS、PMS等等。

          2. 各種架構設計能力,這就需要大家多去學習設計模式、各種軟件架構、設計思想上的東西,例如MVC、MVP、MVVM、重構、代碼規范等等。

          3. 在一般的互聯網公司,網絡通信是最重要的,因此我們就有必要掌握基本的Socket編程、各種網絡請求框架,比如Retrofit2等等,進階的話,就需要自己去研究分析這些框架的源碼、自己動手用組件化的思想去封裝這些框架,防止代碼的侵入性等等。

          4. 新技術的調研與學習,企業需要的是在盡量少的時間,開發出最優秀的產品,因此新技術是必不可少的。因此我們也需要保持不斷學習充實自己的習慣。這里提到“研究新技術”,既然是研究,那么公司為了減少大家的學習成本,肯定是需要你將研究的成功通過文檔的形式輸出,以方便其他員工的快速上手,因此這里隱含了文檔的輸出能力。

          5. 經驗,關于這個也沒有什么好說的,很多東西,如果你遇到過就有經驗了,處理起來就比較快了。但是有一些難題你從來沒有遇到過,那么難度就是無限大的,經驗需要一點一滴地積累,多向大牛取經往往是實現彎道超車的最好辦法

          6. 成熟項目的工作經驗,這是作為應聘者一個最有閃光點的地方,也是在校生校招的一個最有說服力的點。當然,除了在公司工作,也推薦大家去一些培訓網站上面學習一些項目的實戰視頻教程,這也是一種快速學習的方法,但是學習完一定要加以總結,最好以文檔、博客的方式進行輸出,并保持分享,互相交流才能知道自己的錯誤、不足,才能獲取更多技術,進步更加快。

          7. 性能優化,這是面試常見的問題。性能優化跟一個APP的用戶體驗息息相關,很多公司都十分重視這方面的知識。

          為什么高級程序員不必擔心自己的技術過時?

          程序員是吃青春飯的嗎?等我們老了,技術過時了,公司有什么理由不裁掉我們,去雇一些既有活力、薪資要求又低的年輕人呢?這個老生常談的問題困擾著諸多漸入中年的程序員。接下來講解教你如何增強自己的核心競爭力,在知識飛速更新的行業中站穩腳跟,跨過“初級工程師”和“高級工程師”之間的鴻溝。

          我認為開發者中有很多我們不太重視的軟技能,這些軟技能都有可能成倍地增加我們工作的影響力(作為個人貢獻者和技術負責人)。這些軟技能包括:

          • 代碼審查禮節;
          • 如何優雅地遏制范圍蔓延;
          • 如何向其他部門直觀的方式解釋高科技問題;
          • 如何在生產任務爆滿和日以繼夜的比賽中保持鎮定自若等。

          作為一名程序員 ,編碼硬實力固然很重要,但是這些軟技能也同樣重要,這決定著你的核心競爭力的強弱。

          高級開發者,會在工作中解決問題,而非制造問題。

          他們減少壓力。他們按時完成任務。他們知道如何編寫經得起時間考驗、可維護的代碼。他們值得更高的工資。他們對項目的方向可以有準確的把控。他們可以發現當前流程中的缺陷,并使每個人都接受他們的想法以進行改進。他們可以指導應屆畢業生。他們處事冷靜,不會在周二與你的最大客戶的電話會議上情緒崩潰、破口大罵。

          Android學習的方向

          學習一線大廠的各項技術:

          大廠是我們的技術先驅,不僅僅是各種技術,還有很多令你大開眼界的使用方法,學習這些有利于自己競爭力的提高,深入理解后也可以提升你的學習效率。

          現在競爭這么激烈,只有通過不斷學習,提高自己,才能保持競爭力。

          1.2020大廠面試高頻知識點

          • 圖片
          • 網絡和安全機制
          • 數據庫
          • 插件化、模塊化、組件化、熱修復、增量更新、Gradle
          • 架構設計和設計模式
          • 性能優化
          • Android Framework
          • Android優秀三方庫源碼

          2.Jetpack架構組件從入門到精通

          • Android Jetpack - Navigation
          • Android Jetpack - Data Binding
          • Android Jetpack - ViewModel & LiveData
          • Android Jetpack - Room
          • Android Jetpack - Paging
          • Android Jetpack - WorkManger
          • Android Jetpack架構組件之Lifecycle
          • Android Jetpack Compose 最全上手指南

          3.Framework精編內核解析

          主要內容包含:

          • 深入解析Binder
          • 深入解析Handler
          • Dalvik VM 進程系統
          • 深入解析 WMS
          • PackagerManagerService

          4.Kotlin強化實戰(附Demo)

          • 第一章 Kotlin入門教程
          • 第二章 Kotlin 實戰避坑指南
          • 第三章 項目實戰《Kotlin Jetpack 實戰》
            • 從一個膜拜大神的 Demo 開始
            • Kotlin 寫 Gradle 腳本是一種什么體驗?
            • Kotlin 編程的三重境界
            • Kotlin 高階函數
            • Kotlin 泛型
            • Kotlin 擴展
            • Kotlin 委托
            • 協程“不為人知”的調試技巧
            • 圖解協程:suspend

          5.Android設計思想解讀開源框架

          • 熱修復
          • 插件化
          • 組件化框架設計
          • 圖片加載框架
          • 網絡請求框架
          • RXJava 響應式編程框架設計
          • IOC 架構設計
          • Android架構組件Jetpack

          6.NDK模塊開發

          • NDK 模塊開發
          • JNI 模塊
          • Native 開發工具
          • Linux 編程
          • 底層圖片處理
          • 音視頻開發
          • 機器學習

          定期面試:

          光學習了大廠的技術還不夠,如何了解當下Android市場需要哪些技術呢?

          定期的面試足以讓你了解當下市場需要哪些技術,也更加容易確定自身的短板在哪,同時,也可以督促自己學習提升(小伙子,你了解的技術有點落伍了呀…)

          萬一拿到了高薪Offer,豈不是更賺?

          《379頁Android開發面試寶典》

          多看大佬的學習筆記,學習大佬的設計思想:

          只要是程序員,不管是Java還是Android,如果不去閱讀源碼,只看API文檔,那就只是停留于皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

          真正最能鍛煉能力的便是直接去閱讀源碼,不僅限于閱讀各大系統源碼,還包括各種優秀的開源庫。

          《486頁超全面Android開發相關源碼精編解析》

          以上這些內容均免費分享給大家,需要完整版的朋友,直接私信回復【資料】一鍵領取!!!


          主站蜘蛛池模板: 一区二区高清在线观看| 波多野结衣一区二区三区88| 国产精品久久久久久一区二区三区| 国产精品视频一区二区三区| 国产免费私拍一区二区三区| 亚洲福利精品一区二区三区| 亚洲av无码一区二区三区在线播放 | 日本成人一区二区三区| 国产99视频精品一区| 白丝爆浆18禁一区二区三区| 精品亚洲AV无码一区二区三区| 好吊视频一区二区三区| 免费高清av一区二区三区| 天堂国产一区二区三区| 波多野结衣一区二区三区高清av| 久久久老熟女一区二区三区| 一区二区国产精品| 国产成人av一区二区三区不卡| 天堂va在线高清一区| 国产精品熟女一区二区| 国偷自产Av一区二区三区吞精| 精品人妻一区二区三区四区在线| 亚洲午夜在线一区| 日韩人妻无码一区二区三区99 | 国产视频一区二区在线观看| 精品成人乱色一区二区| 无码日韩精品一区二区三区免费 | 国产一区二区三区影院| 精品无码人妻一区二区三区品| 日本欧洲视频一区| 亚洲国产综合无码一区| 亚洲色无码一区二区三区| 国模大尺度视频一区二区| 黑人大战亚洲人精品一区| 人妻少妇精品一区二区三区| 成人精品视频一区二区三区| 国产一区麻豆剧传媒果冻精品 | 国产成人一区二区三区免费视频| 在线电影一区二区| 日本v片免费一区二区三区| 国产在线步兵一区二区三区|