Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
文作者:少杰
原文出處:美團(tuán)技術(shù)團(tuán)隊(duì)
Flutter是Google開發(fā)的一套全新的跨平臺(tái)、開源UI框架,支持iOS、Android系統(tǒng)開發(fā),并且是未來(lái)新操作系統(tǒng)Fuchsia的默認(rèn)開發(fā)套件。自從2017年5月發(fā)布第一個(gè)版本以來(lái),目前Flutter已經(jīng)發(fā)布了近60個(gè)版本,并且在2018年5月發(fā)布了第一個(gè)“Ready for Production Apps”的Beta 3版本,6月20日發(fā)布了第一個(gè)“Release Preview”版本。
Flutter的目標(biāo)是使同一套代碼同時(shí)運(yùn)行在Android和iOS系統(tǒng)上,并且擁有媲美原生應(yīng)用的性能,F(xiàn)lutter甚至提供了兩套控件來(lái)適配Android和iOS(滾動(dòng)效果、字體和控件圖標(biāo)等等),為了讓App在細(xì)節(jié)處看起來(lái)更像原生應(yīng)用。
在Flutter誕生之前,已經(jīng)有許多跨平臺(tái)UI框架的方案,比如基于WebView的Cordova、AppCan等,還有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。
基于WebView的框架優(yōu)點(diǎn)很明顯,它們幾乎可以完全繼承現(xiàn)代Web開發(fā)的所有成果(豐富得多的控件庫(kù)、滿足各種需求的頁(yè)面框架、完全的動(dòng)態(tài)化、自動(dòng)化測(cè)試工具等等),當(dāng)然也包括Web開發(fā)人員,不需要太多的學(xué)習(xí)和遷移成本就可以開發(fā)一個(gè)App。同時(shí)WebView框架也有一個(gè)致命(在對(duì)體驗(yàn)&性能有較高要求的情況下)的缺點(diǎn),那就是WebView的渲染效率和JavaScript執(zhí)行性能太差。再加上Android各個(gè)系統(tǒng)版本和設(shè)備廠商的定制,很難保證所在所有設(shè)備上都能提供一致的體驗(yàn)。
為了解決WebView性能差的問(wèn)題,以React Native為代表的一類框架將最終渲染工作交還給了系統(tǒng),雖然同樣使用類HTML+JS的UI構(gòu)建邏輯,但是最終會(huì)生成對(duì)應(yīng)的自定義原生控件,以充分利用原生控件相對(duì)于WebView的較高的繪制效率。與此同時(shí)這種策略也將框架本身和App開發(fā)者綁在了系統(tǒng)的控件系統(tǒng)上,不僅框架本身需要處理大量平臺(tái)相關(guān)的邏輯,隨著系統(tǒng)版本變化和API的變化,開發(fā)者可能也需要處理不同平臺(tái)的差異,甚至有些特性只能在部分平臺(tái)上實(shí)現(xiàn),這樣框架的跨平臺(tái)特性就會(huì)大打折扣。
Flutter則開辟了一種全新的思路,從頭到尾重寫一套跨平臺(tái)的UI框架,包括UI控件、渲染邏輯甚至開發(fā)語(yǔ)言。渲染引擎依靠跨平臺(tái)的Skia圖形庫(kù)來(lái)實(shí)現(xiàn),依賴系統(tǒng)的只有圖形繪制相關(guān)的接口,可以在最大程度上保證不同平臺(tái)、不同設(shè)備的體驗(yàn)一致性,邏輯處理使用支持AOT的Dart語(yǔ)言,執(zhí)行效率也比JavaScript高得多。
Flutter同時(shí)支持Windows、Linux和macOS操作系統(tǒng)作為開發(fā)環(huán)境,并且在Android Studio和VS Code兩個(gè)IDE上都提供了全功能的支持。Flutter所使用的Dart語(yǔ)言同時(shí)支持AOT和JIT運(yùn)行方式,JIT模式下還有一個(gè)備受歡迎的開發(fā)利器“熱刷新”(Hot Reload),即在Android Studio中編輯Dart代碼后,只需要點(diǎn)擊保存或者“Hot Reload”按鈕,就可以立即更新到正在運(yùn)行的設(shè)備上,不需要重新編譯App,甚至不需要重啟App,立即就可以看到更新后的樣式。
在Flutter中,所有功能都可以通過(guò)組合多個(gè)Widget來(lái)實(shí)現(xiàn),包括對(duì)齊方式、按行排列、按列排列、網(wǎng)格排列甚至事件處理等等。Flutter控件主要分為兩大類,StatelessWidget和StatefulWidget,StatelessWidget用來(lái)展示靜態(tài)的文本或者圖片,如果控件需要根據(jù)外部數(shù)據(jù)或者用戶操作來(lái)改變的話,就需要使用StatefulWidget。State的概念也是來(lái)源于Facebook的流行Web框架React,React風(fēng)格的框架中使用控件樹和各自的狀態(tài)來(lái)構(gòu)建界面,當(dāng)某個(gè)控件的狀態(tài)發(fā)生變化時(shí)由框架負(fù)責(zé)對(duì)比前后狀態(tài)差異并且采取最小代價(jià)來(lái)更新渲染結(jié)果。
Hot Reload
在Dart代碼文件中修改字符串“Hello, World”,添加一個(gè)驚嘆號(hào),點(diǎn)擊保存或者熱刷新按鈕就可以立即更新到界面上,僅需幾百毫秒:
Flutter通過(guò)將新的代碼注入到正在運(yùn)行的DartVM中,來(lái)實(shí)現(xiàn)Hot Reload這種神奇的效果,在DartVM將程序中的類結(jié)構(gòu)更新完成后,F(xiàn)lutter會(huì)立即重建整個(gè)控件樹,從而更新界面。但是熱刷新也有一些限制,并不是所有的代碼改動(dòng)都可以通過(guò)熱刷新來(lái)更新:
熱刷新無(wú)法實(shí)現(xiàn)更新時(shí),執(zhí)行一次熱重啟(Hot Restart)就可以全量更新所有代碼,同樣不需要重啟App,區(qū)別是restart會(huì)將所有Dart代碼打包同步到設(shè)備上,并且所有狀態(tài)都會(huì)重置。
Flutter插件
Flutter使用的Dart語(yǔ)言無(wú)法直接調(diào)用Android系統(tǒng)提供的Java接口,這時(shí)就需要使用插件來(lái)實(shí)現(xiàn)中轉(zhuǎn)。Flutter官方提供了豐富的原生接口封裝:
在Flutter中,依賴包由Pub倉(cāng)庫(kù)管理,項(xiàng)目依賴配置在pubspec.yaml文件中聲明即可(類似于NPM的版本聲明Pub Versioning Philosophy),對(duì)于未發(fā)布在Pub倉(cāng)庫(kù)的插件可以使用git倉(cāng)庫(kù)地址或文件路徑:
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"
脫字號(hào)“^”開頭的版本表示和當(dāng)前版本接口保持兼容的最新版,^1.2.3 等效于 >=1.2.3 <2.0.0 而^0.1.2 等效于 >=0.1.2 <0.2.0,添加依賴后點(diǎn)擊“Packages get”按鈕即可下載插件到本地,在代碼中添加import語(yǔ)句就可以使用插件提供的接口:
import 'package:shared_preferences/shared_preferences.Dart';
class _MyAppState extends State<MyAppCounter> {
int _count = 0;
static const String COUNTER_KEY = 'counter';
_MyAppState() {
init();
}
init() async {
var pref = await SharedPreferences.getInstance();
_count = pref.getInt(COUNTER_KEY) ?? 0;
setState(() {});
}
increaseCounter() async {
SharedPreferences pref = await SharedPreferences.getInstance();
pref.setInt(COUNTER_KEY, ++_count);
setState(() {});
}
...
Dart
Dart是一種強(qiáng)類型、跨平臺(tái)的客戶端開發(fā)語(yǔ)言。具有專門為客戶端優(yōu)化、高生產(chǎn)力、快速高效、可移植(兼容ARM/x86)、易學(xué)的OO編程風(fēng)格和原生支持響應(yīng)式編程(Stream & Future)等優(yōu)秀特性。Dart主要由Google負(fù)責(zé)開發(fā)和維護(hù),在2011年10啟動(dòng)項(xiàng)目,2017年9月發(fā)布第一個(gè)2.0-dev版本。
Dart本身提供了三種運(yùn)行方式:
Flutter在篩選了20多種語(yǔ)言后,最終選擇Dart作為開發(fā)語(yǔ)言主要有幾個(gè)原因:
在Dart中,有一些重要的基本概念需要了解:
DartVM的內(nèi)存分配策略非常簡(jiǎn)單,創(chuàng)建對(duì)象時(shí)只需要在現(xiàn)有堆上移動(dòng)指針,內(nèi)存增長(zhǎng)始終是線形的,省去了查找可用內(nèi)存段的過(guò)程:
Dart中類似線程的概念叫做Isolate,每個(gè)Isolate之間是無(wú)法共享內(nèi)存的,所以這種分配策略可以讓Dart實(shí)現(xiàn)無(wú)鎖的快速分配。
Dart的垃圾回收也采用了多生代算法,新生代在回收內(nèi)存時(shí)采用了“半空間”算法,觸發(fā)垃圾回收時(shí)Dart會(huì)將當(dāng)前半空間中的“活躍”對(duì)象拷貝到備用空間,然后整體釋放當(dāng)前空間的所有內(nèi)存:
整個(gè)過(guò)程中Dart只需要操作少量的“活躍”對(duì)象,大量的沒(méi)有引用的“死亡”對(duì)象則被忽略,這種算法也非常適合Flutter框架中大量Widget重建的場(chǎng)景。
Flutter Framework
Flutter的框架部分完全使用Dart語(yǔ)言實(shí)現(xiàn),并且有著清晰的分層架構(gòu)。分層架構(gòu)使得我們可以在調(diào)用Flutter提供的便捷開發(fā)功能(預(yù)定義的一套高質(zhì)量Material控件)之外,還可以直接調(diào)用甚至修改每一層實(shí)現(xiàn)(因?yàn)檎麄€(gè)框架都屬于“用戶空間”的代碼),這給我們提供了最大程度的自定義能力。Framework底層是Flutter引擎,引擎主要負(fù)責(zé)圖形繪制(Skia)、文字排版(libtxt)和提供Dart運(yùn)行時(shí),引擎全部使用C++實(shí)現(xiàn),F(xiàn)ramework層使我們可以用Dart語(yǔ)言調(diào)用引擎的強(qiáng)大能力。
分層架構(gòu)
Framework的最底層叫做Foundation,其中定義的大都是非常基礎(chǔ)的、提供給其他所有層使用的工具類和方法。繪制庫(kù)(Painting)封裝了Flutter Engine提供的繪制接口,主要是為了在繪制控件等固定樣式的圖形時(shí)提供更直觀、更方便的接口,比如繪制縮放后的位圖、繪制文本、插值生成陰影以及在盒子周圍繪制邊框等等。
Animation是動(dòng)畫相關(guān)的類,提供了類似Android系統(tǒng)的ValueAnimator的功能,并且提供了豐富的內(nèi)置插值器。Gesture提供了手勢(shì)識(shí)別相關(guān)的功能,包括觸摸事件類定義和多種內(nèi)置的手勢(shì)識(shí)別器。GestureBinding類是Flutter中處理手勢(shì)的抽象服務(wù)類,繼承自BindingBase類。
Binding系列的類在Flutter中充當(dāng)著類似于Android中的SystemService系列(ActivityManager、PackageManager)功能,每個(gè)Binding類都提供一個(gè)服務(wù)的單例對(duì)象,App最頂層的Binding會(huì)包含所有相關(guān)的Bingding抽象類。如果使用Flutter提供的控件進(jìn)行開發(fā),則需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接調(diào)用Render層,則需要使用RenderingFlutterBinding。
Flutter本身支持Android和iOS兩個(gè)平臺(tái),除了性能和開發(fā)語(yǔ)言上的“native”化之外,它還提供了兩套設(shè)計(jì)語(yǔ)言的控件實(shí)現(xiàn)Material & Cupertino,可以幫助App更好地在不同平臺(tái)上提供原生的用戶體驗(yàn)。
渲染庫(kù)(Rendering)
Flutter的控件樹在實(shí)際顯示時(shí)會(huì)轉(zhuǎn)換成對(duì)應(yīng)的渲染對(duì)象(RenderObject)樹來(lái)實(shí)現(xiàn)布局和繪制操作。一般情況下,我們只會(huì)在調(diào)試布局,或者需要使用自定義控件來(lái)實(shí)現(xiàn)某些特殊效果的時(shí)候,才需要考慮渲染對(duì)象樹的細(xì)節(jié)。渲染庫(kù)主要提供的功能類有:
abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { ... }
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
abstract class RenderBox extends RenderObject { ... }
class RenderParagraph extends RenderBox { ... }
class RenderImage extends RenderBox { ... }
class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>,
DebugOverflowIndicatorMixin { ... }
RendererBinding是渲染樹和Flutter引擎的膠水層,負(fù)責(zé)管理幀重繪、窗口尺寸和渲染相關(guān)參數(shù)變化的監(jiān)聽。RenderObject渲染樹中所有節(jié)點(diǎn)的基類,定義了布局、繪制和合成相關(guān)的接口。RenderBox和其三個(gè)常用的子類RenderParagraph、RenderImage、RenderFlex則是具體布局和繪制邏輯的實(shí)現(xiàn)類。
在Flutter界面渲染過(guò)程分為三個(gè)階段:布局、繪制、合成,布局和繪制在Flutter框架中完成,合成則交由引擎負(fù)責(zé):
控件樹中的每個(gè)控件通過(guò)實(shí)現(xiàn)RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject方法來(lái)創(chuàng)建對(duì)應(yīng)的不同類型的RenderObject對(duì)象,組成渲染對(duì)象樹。因?yàn)镕lutter極大地簡(jiǎn)化了布局的邏輯,所以整個(gè)布局過(guò)程中只需要深度遍歷一次:
渲染對(duì)象樹中的每個(gè)對(duì)象都會(huì)在布局過(guò)程中接受父對(duì)象的Constraints參數(shù),決定自己的大小,然后父對(duì)象就可以按照自己的邏輯決定各個(gè)子對(duì)象的位置,完成布局過(guò)程。
子對(duì)象不存儲(chǔ)自己在容器中的位置,所以在它的位置發(fā)生改變時(shí)并不需要重新布局或者繪制。子對(duì)象的位置信息存儲(chǔ)在它自己的parentData字段中,但是該字段由它的父對(duì)象負(fù)責(zé)維護(hù),自身并不關(guān)心該字段的內(nèi)容。同時(shí)也因?yàn)檫@種簡(jiǎn)單的布局邏輯,F(xiàn)lutter可以在某些節(jié)點(diǎn)設(shè)置布局邊界(Relayout boundary),即當(dāng)邊界內(nèi)的任何對(duì)象發(fā)生重新布局時(shí),不會(huì)影響邊界外的對(duì)象,反之亦然:
布局完成后,渲染對(duì)象樹中的每個(gè)節(jié)點(diǎn)都有了明確的尺寸和位置,F(xiàn)lutter會(huì)把所有對(duì)象繪制到不同的圖層上:
因?yàn)槔L制節(jié)點(diǎn)時(shí)也是深度遍歷,可以看到第二個(gè)節(jié)點(diǎn)在繪制它的背景和前景不得不繪制在不同的圖層上,因?yàn)榈谒膫€(gè)節(jié)點(diǎn)切換了圖層(因?yàn)椤?”節(jié)點(diǎn)是一個(gè)需要獨(dú)占一個(gè)圖層的內(nèi)容,比如視頻),而第六個(gè)節(jié)點(diǎn)也一起繪制到了紅色圖層。這樣會(huì)導(dǎo)致第二個(gè)節(jié)點(diǎn)的前景(也就是“5”)部分需要重繪時(shí),和它在邏輯上毫不相干但是處于同一圖層的第六個(gè)節(jié)點(diǎn)也必須重繪。為了避免這種情況,F(xiàn)lutter提供了另外一個(gè)“重繪邊界”的概念:
在進(jìn)入和走出重繪邊界時(shí),F(xiàn)lutter會(huì)強(qiáng)制切換新的圖層,這樣就可以避免邊界內(nèi)外的互相影響。典型的應(yīng)用場(chǎng)景就是ScrollView,當(dāng)滾動(dòng)內(nèi)容重繪時(shí),一般情況下其他內(nèi)容是不需要重繪的。雖然重繪邊界可以在任何節(jié)點(diǎn)手動(dòng)設(shè)置,但是一般不需要我們來(lái)實(shí)現(xiàn),F(xiàn)lutter提供的控件默認(rèn)會(huì)在需要設(shè)置的地方自動(dòng)設(shè)置。
控件庫(kù)(Widgets)
Flutter的控件庫(kù)提供了非常豐富的控件,包括最基本的文本、圖片、容器、輸入框和動(dòng)畫等等。在Flutter中“一切皆是控件”,通過(guò)組合、嵌套不同類型的控件,就可以構(gòu)建出任意功能、任意復(fù)雜度的界面。它包含的最主要的幾個(gè)類有:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding,
PaintingBinding, RendererBinding, WidgetsBinding { ... }
abstract class Widget extends DiagnosticableTree { ... }
abstract class StatelessWidget extends Widget { ... }
abstract class StatefulWidget extends Widget { ... }
abstract class RenderObjectWidget extends Widget { ... }
abstract class Element extends DiagnosticableTree implements BuildContext { ... }
class StatelessElement extends ComponentElement { ... }
class StatefulElement extends ComponentElement { ... }
abstract class RenderObjectElement extends Element { ... }
...
基于Flutter控件系統(tǒng)開發(fā)的程序都需要使用WidgetsFlutterBinding,它是Flutter的控件框架和Flutter引擎的膠水層。Widget就是所有控件的基類,它本身所有的屬性都是只讀的。RenderObjectWidget所有的實(shí)現(xiàn)類則負(fù)責(zé)提供配置信息并創(chuàng)建具體的RenderObjectElement。Element是Flutter用來(lái)分離控件樹和真正的渲染對(duì)象的中間層,控件用來(lái)描述對(duì)應(yīng)的element屬性,控件重建后可能會(huì)復(fù)用同一個(gè)element。RenderObjectElement持有真正負(fù)責(zé)布局、繪制和碰撞測(cè)試(hit test)的RenderObject對(duì)象。
StatelessWidget和StatefulWidget并不會(huì)直接影響RenderObject創(chuàng)建,只負(fù)責(zé)創(chuàng)建對(duì)應(yīng)的RenderObjectWidgetStatelessElement和StatefulElement也是類似的功能。
它們之間的關(guān)系如下圖:
如果控件的屬性發(fā)生了變化(因?yàn)榭丶膶傩允侵蛔x的,所以變化也就意味著重新創(chuàng)建了新的控件樹),但是其樹上每個(gè)節(jié)點(diǎn)的類型沒(méi)有變化時(shí),element樹和render樹可以完全重用原來(lái)的對(duì)象(因?yàn)閑lement和render object的屬性都是可變的):
但是,如果控件樹種某個(gè)節(jié)點(diǎn)的類型發(fā)生了變化,則element樹和render樹中的對(duì)應(yīng)節(jié)點(diǎn)也需要重新創(chuàng)建:
在調(diào)研了Flutter的各項(xiàng)特性和實(shí)現(xiàn)原理之后,外賣計(jì)劃灰度上線Flutter版的全品類頁(yè)面。對(duì)于將Flutter頁(yè)面作為App的一部分這種集成模式,官方并沒(méi)有提供完善的支持,所以我們首先需要了解Flutter是如何編譯、打包并且運(yùn)行起來(lái)的。
Flutter App構(gòu)建過(guò)程
最簡(jiǎn)單的Flutter工程至少包含兩個(gè)文件:
運(yùn)行Flutter程序時(shí)需要對(duì)應(yīng)平臺(tái)的宿主工程,在Android上Flutter通過(guò)自動(dòng)創(chuàng)建一個(gè)Gradle項(xiàng)目來(lái)生成宿主,在項(xiàng)目目錄下執(zhí)行flutter create .,F(xiàn)lutter會(huì)創(chuàng)建ios和android兩個(gè)目錄,分別構(gòu)建對(duì)應(yīng)平臺(tái)的宿主項(xiàng)目,Android目錄內(nèi)容如下:
此Gradle項(xiàng)目中只有一個(gè)app module,構(gòu)建產(chǎn)物即是宿主APK。Flutter在本地運(yùn)行時(shí)默認(rèn)采用Debug模式,在項(xiàng)目目錄執(zhí)行flutter run即可安裝到設(shè)備中并自動(dòng)運(yùn)行,Debug模式下Flutter使用JIT方式來(lái)執(zhí)行Dart代碼,所有的Dart代碼都會(huì)打包到APK文件中assets目錄下,由libflutter.so中提供的DartVM讀取并執(zhí)行:
kernel_blob.bin是Flutter引擎的底層接口和Dart語(yǔ)言基本功能部分代碼:
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則是實(shí)現(xiàn)了頁(yè)面邏輯的代碼,也包括Flutter Framework和其他由pub依賴的庫(kù)代碼:
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中調(diào)用KernelCompiler生成。
在Release模式(flutter run --release)下,F(xiàn)lutter會(huì)使用Dart的AOT運(yùn)行模式,編譯時(shí)將Dart代碼轉(zhuǎn)換成ARM指令:
kernel_blob.bin和platform.dill都不在打包后的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四個(gè)文件。snapshot文件由Flutter SDK中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot命令生成,vm_snapshot_*是Dart虛擬機(jī)運(yùn)行所需要的數(shù)據(jù)和代碼指令,isolate_snapshot_*則是每個(gè)isolate運(yùn)行所需要的數(shù)據(jù)和代碼指令。
Flutter App運(yùn)行機(jī)制
Flutter構(gòu)建出的APK在運(yùn)行時(shí)會(huì)將所有assets目錄下的資源文件解壓到App私有文件目錄中的flutter目錄下,主要包括處理字符編碼的icudtl.dat,還有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4個(gè)snapshot文件。默認(rèn)情況下Flutter在Application#onCreate時(shí)調(diào)用FlutterMain#startInitialization來(lái)啟動(dòng)解壓任務(wù),然后在FlutterActivityDelegate#onCreate中調(diào)用FlutterMain#ensureInitializationComplete來(lái)等待解壓任務(wù)結(jié)束。
Flutter在Debug模式下使用JIT執(zhí)行方式,主要是為了支持廣受歡迎的熱刷新功能:
觸發(fā)熱刷新時(shí)Flutter會(huì)檢測(cè)發(fā)生改變的Dart文件,將其同步到App私有緩存目錄下,DartVM加載并且修改對(duì)應(yīng)的類或者方法,重建控件樹后立即可以在設(shè)備上看到效果。
在Release模式下Flutter會(huì)直接將snapshot文件映射到內(nèi)存中執(zhí)行其中的指令:
在Release模式下,F(xiàn)lutterActivityDelegate#onCreate中調(diào)用FlutterMain#ensureInitializationComplete方法中會(huì)將AndroidManifest中設(shè)置的snapshot(沒(méi)有設(shè)置則使用上面提到的默認(rèn)值)文件名等運(yùn)行參數(shù)設(shè)置到對(duì)應(yīng)的C++同名類對(duì)象中,構(gòu)造FlutterNativeView實(shí)例時(shí)調(diào)用nativeAttach來(lái)初始化DartVM,運(yùn)行編譯好的Dart代碼。
打包Android Library
了解Flutter項(xiàng)目的構(gòu)建和運(yùn)行機(jī)制后,我們就可以按照其需求打包成AAR然后集成到現(xiàn)有原生App中了。首先在andorid/app/build.gradle中修改:
簡(jiǎn)單修改后我們就可以使用Android Studio或者Gradle命令行工具將Flutter代碼打包到aar中了。Flutter運(yùn)行時(shí)所需要的資源都會(huì)包含在aar中,將其發(fā)布到maven服務(wù)器或者本地maven倉(cāng)庫(kù)后,就可以在原生App項(xiàng)目中引用。
但這只是集成的第一步,為了讓Flutter頁(yè)面無(wú)縫銜接到外賣App中,我們需要做的還有很多。
圖片資源復(fù)用
Flutter默認(rèn)將所有的圖片資源文件打包到assets目錄下,但是我們并不是用Flutter開發(fā)全新的頁(yè)面,圖片資源原來(lái)都會(huì)按照Android的規(guī)范放在各個(gè)drawable目錄,即使是全新的頁(yè)面也會(huì)有很多圖片資源復(fù)用的場(chǎng)景,所以在assets目錄下新增圖片資源并不合適。
Flutter官方并沒(méi)有提供直接調(diào)用drawable目錄下的圖片資源的途徑,畢竟drawable這類文件的處理會(huì)涉及大量的Android平臺(tái)相關(guān)的邏輯(屏幕密度、系統(tǒng)版本、語(yǔ)言等等),assets目錄文件的讀取操作也在引擎內(nèi)部使用C++實(shí)現(xiàn),在Dart層面實(shí)現(xiàn)讀取drawable文件的功能比較困難。Flutter在處理assets目錄中的文件時(shí)也支持添加多倍率的圖片資源,并能夠在使用時(shí)自動(dòng)選擇,但是Flutter要求每個(gè)圖片必須提供1x圖,然后才會(huì)識(shí)別到對(duì)應(yīng)的其他倍率目錄下的圖片:
flutter:
assets:
- images/cat.png
- images/2x/cat.png
- images/3.5x/cat.png
new Image.asset('images/cat.png');
這樣配置后,才能正確地在不同分辨率的設(shè)備上使用對(duì)應(yīng)密度的圖片。但是為了減小APK包體積我們的位圖資源一般只提供常用的2x分辨率,其他分辨率的設(shè)備會(huì)在運(yùn)行時(shí)自動(dòng)縮放到對(duì)應(yīng)大小。針對(duì)這種特殊的情況,我們?cè)诓辉黾影w積的前提下,同樣提供了和原生App一樣的能力:
這樣就可以同時(shí)解決APK包大小和圖片資源缺失1x圖的問(wèn)題。
Flutter和原生代碼的通信
我們只用Flutter實(shí)現(xiàn)了一個(gè)頁(yè)面,現(xiàn)有的大量邏輯都是用Java實(shí)現(xiàn),在運(yùn)行時(shí)會(huì)有許多場(chǎng)景必須使用原生應(yīng)用中的邏輯和功能,例如網(wǎng)絡(luò)請(qǐng)求,我們統(tǒng)一的網(wǎng)絡(luò)庫(kù)會(huì)在每個(gè)網(wǎng)絡(luò)請(qǐng)求中添加許多通用參數(shù),也會(huì)負(fù)責(zé)成功率等指標(biāo)的監(jiān)控,還有異常上報(bào),我們需要在捕獲到關(guān)鍵異常時(shí)將其堆棧和環(huán)境信息上報(bào)到服務(wù)器。這些功能不太可能立即使用Dart實(shí)現(xiàn)一套出來(lái),所以我們需要使用Dart提供的Platform Channel功能來(lái)實(shí)現(xiàn)Dart→Java之間的互相調(diào)用。
以網(wǎng)絡(luò)請(qǐng)求為例,我們?cè)贒art中定義一個(gè)MethodChannel對(duì)象:
import 'dart:async';
import 'package:flutter/services.dart';
const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network');
Future<Map<String, dynamic>> post(String path, [Map<String, dynamic> form]) async {
return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) {
return new Map<String, dynamic>.from(result);
}).catchError((_) => null);
}
然后在Java端實(shí)現(xiàn)相同名稱的MethodChannel:
public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {
private static final String CHANNEL_NAME = "com.sankuai.waimai/network";
@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頁(yè)面中注冊(cè)后,調(diào)用post方法就可以調(diào)用對(duì)應(yīng)的Java實(shí)現(xiàn):
loadData: (callback) async {
Map<String, dynamic> data = await post("home/groups");
if (data == null) {
callback(false);
return;
}
_data = AllCategoryResponse.fromJson(data);
if (_data == null || _data.code != 0) {
callback(false);
return;
}
callback(true);
}),
SO庫(kù)兼容性
Flutter官方只提供了四種CPU架構(gòu)的SO庫(kù):armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模式,但是外賣使用的大量SDK都只提供了armeabi架構(gòu)的庫(kù)。
雖然我們可以通過(guò)修改引擎src根目錄和third_party/dart目錄下build/config/arm.gni,third_party/skia目錄下的BUILD.gn等配置文件來(lái)編譯出armeabi版本的Flutter引擎,但是實(shí)際上市面上絕大部分設(shè)備都已經(jīng)支持armeabi-v7a,其提供的硬件加速浮點(diǎn)運(yùn)算指令可以大大提高Flutter的運(yùn)行速度,在灰度階段我們可以主動(dòng)屏蔽掉不支持armeabi-v7a的設(shè)備,直接使用armeabi-v7a版本的引擎。
做到這點(diǎn)我們首先需要修改Flutter提供的引擎,在Flutter安裝目錄下的bin/cache/artifacts/engine下有Flutter下載的所有平臺(tái)的引擎:
我們只需要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,將其中的lib/armeabi-v7a/libflutter.so移動(dòng)到lib/armeabi/libflutter.so即可:
cd $FLUTTER_ROOT/bin/cache/artifacts/engine
for arch in android-arm android-arm-profile android-arm-release; 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庫(kù)就會(huì)打到APK的lib/armeabi目錄中。在運(yùn)行時(shí)如果設(shè)備不支持armeabi-v7a可能會(huì)崩潰,所以我們需要主動(dòng)識(shí)別并屏蔽掉這類設(shè)備,在Android上判斷設(shè)備是否支持armeabi-v7a也很簡(jiǎn)單:
public static boolean isARMv7Compatible() {
try {
if (SDK_INT >= LOLLIPOP) {
for (String abi : Build.SUPPORTED_32_BIT_ABIS) {
if (abi.equals("armeabi-v7a")) {
return true;
}
}
} else {
if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) {
return true;
}
}
} catch (Throwable e) {
L.wtf(e);
}
return false;
}
灰度和自動(dòng)降級(jí)策略
Horn是一個(gè)美團(tuán)內(nèi)部的跨平臺(tái)配置下發(fā)SDK,使用Horn可以很方便地指定灰度開關(guān):
在條件配置頁(yè)面定義一系列條件,然后在參數(shù)配置頁(yè)面添加新的字段flutter即可:
因?yàn)樵诳蛻舳俗隽薃BI兜底策略,所以這里定義的ABI規(guī)則并沒(méi)有啟用。
Flutter目前仍然處于Beta階段,灰度過(guò)程中難免發(fā)生崩潰現(xiàn)象,觀察到崩潰后再針對(duì)機(jī)型或者設(shè)備ID來(lái)做降級(jí)雖然可以盡量降低影響,但是我們可以做到更迅速。外賣的Crash采集SDK同時(shí)也支持JNI Crash的收集,我們專門為Flutter注冊(cè)了崩潰監(jiān)聽器,一旦采集到Flutter相關(guān)的JNI Crash就立即停止該設(shè)備的Flutter功能,啟動(dòng)Flutter之前會(huì)先判斷FLUTTER_NATIVE_CRASH_FLAG文件是否存在,如果存在則表示該設(shè)備發(fā)生過(guò)Flutter相關(guān)的崩潰,很有可能是不兼容導(dǎo)致的問(wèn)題,當(dāng)前版本周期內(nèi)在該設(shè)備上就不再使用Flutter功能。
除了崩潰以外,F(xiàn)lutter頁(yè)面中的Dart代碼也可能發(fā)生異常,例如服務(wù)器下發(fā)數(shù)據(jù)格式錯(cuò)誤導(dǎo)致解析失敗等等,Dart也提供了全局的異常捕獲功能:
import 'package:wm_app/plugins/wm_metrics.dart';
void main() {
runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) {
uploadException("$obj\n$stack");
});
}
這樣我們就可以實(shí)現(xiàn)全方位的異常監(jiān)控和完善的降級(jí)策略,最大程度減少灰度時(shí)可能對(duì)用戶帶來(lái)的影響。
分析崩潰堆棧和異常數(shù)據(jù)
Flutter的引擎部分全部使用C/C++實(shí)現(xiàn),為了減少包大小,所有的SO庫(kù)在發(fā)布時(shí)都會(huì)去除符號(hào)表信息。和其他的JNI崩潰堆棧一樣,我們上報(bào)的堆棧信息中只能看到內(nèi)存地址偏移量等信息:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys'
Revision: '0'
Author: collect by 'libunwind'
ABI: 'arm64-v8a'
pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 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
單純這些信息很難定位問(wèn)題,所以我們需要使用NDK提供的ndk-stack來(lái)解析出具體的代碼位置:
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)
如果使用了定制過(guò)的引擎,必須使用engine/src/out/android-release下編譯出的libflutter.so文件。一般情況下我們使用的是官方版本的引擎,可以在flutter_infra頁(yè)面直接下載帶有符號(hào)表的SO文件,根據(jù)打包時(shí)使用的Flutter工具版本下載對(duì)應(yīng)的文件即可。比如0.4.4 beta版本:
$ flutter --version # version命令可以看到Engine對(duì)應(yīng)的版本 06afdfe54e
Flutter 0.4.4 ? channel beta ? https://github.com/flutter/flutter.git
Framework ? revision f9bb4289e9 (5 weeks ago) ? 2018-05-11 21:44:54 -0700
Engine ? revision 06afdfe54e
Tools ? Dart 2.0.0-dev.54.0.flutter-46ab040e58
$ cat flutter/bin/internal/engine.version # flutter安裝目錄下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa
06afdfe54ebef9168a90ca00a6721c2d36e6aafa
拿到引擎版本號(hào)后在https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到該版本對(duì)應(yīng)的所有構(gòu)建產(chǎn)物,下載android-arm-release、android-arm64-release和android-x86目錄下的symbols.zip,并存放到對(duì)應(yīng)目錄:
執(zhí)行ndk-stack即可看到實(shí)際發(fā)生崩潰的代碼和具體行數(shù)信息:
ndk-stack -sym flutter-production-syms/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/armeabi-v7a -dump flutter_jni_crash.txt
********** Crash dump: **********
Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys'
pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Stack frame #00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::WordBreaker::setText(unsigned short const*, unsigned int) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/WordBreaker.cpp:55
Stack frame #01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::LineBreaker::setText() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/LineBreaker.cpp:74
Stack frame #02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::ComputeLineBreaks() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:273
Stack frame #03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::Layout(double, bool) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:428
Stack frame #04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine blink::ParagraphImplTxt::layout(double) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/lib/ui/text/paragraph_impl_txt.cc:54
Stack frame #05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine tonic::DartDispatcher<tonic::IndicesHolder<0u>, void (blink::Paragraph::*)(double)>::Dispatch(void (blink::Paragraph::*)(double)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:150
Stack frame #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall<void (blink::Paragraph::*)(double)>(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:198
Stack frame #07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::AutoScopeNativeCallWrapperNoStackCheck(_Dart_NativeArguments*, void (*)(_Dart_NativeArguments*)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:198
Stack frame #08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::LinkNativeCall(_Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:348
Stack frame #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr
Dart異常則比較簡(jiǎn)單,默認(rèn)情況下Dart代碼在編譯成機(jī)器碼時(shí)并沒(méi)有去除符號(hào)表信息,所以Dart的異常堆棧本身就可以標(biāo)識(shí)真實(shí)發(fā)生異常的代碼文件和行數(shù)信息:
FlutterException: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'num' in type cast
#0 _$CategoryGroupFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:29)
#1 new CategoryGroup.fromJson (package:wm_app/all_category/model/category_model.dart:51)
#2 _$CategoryListDataFromJson.<anonymous closure> (package:wm_app/lib/all_category/model/category_model.g.dart:5)
#3 MappedListIterable.elementAt (dart:_internal/iterable.dart:414)
#4 ListIterable.toList (dart:_internal/iterable.dart:219)
#5 _$CategoryListDataFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:6)
#6 new CategoryListData.fromJson (package:wm_app/all_category/model/category_model.dart:19)
#7 _$AllCategoryResponseFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:19)
#8 new AllCategoryResponse.fromJson (package:wm_app/all_category/model/category_model.dart:29)
#9 AllCategoryPage.build.<anonymous closure> (package:wm_app/all_category/category_page.dart:46)
<asynchronous suspension>
#10 _WaimaiLoadingState.build (package:wm_app/all_category/widgets/progressive_loading_page.dart:51)
#11 StatefulElement.build (package:flutter/src/widgets/framework.dart:3730)
#12 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3642)
#13 Element.rebuild (package:flutter/src/widgets/framework.dart:3495)
#14 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2242)
#15 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:626)
#16 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:208)
#17 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:990)
#18 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:930)
#19 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:842)
#20 _rootRun (dart:async/zone.dart:1126)
#21 _CustomZone.run (dart:async/zone.dart:1023)
#22 _CustomZone.runGuarded (dart:async/zone.dart:925)
#23 _invoke (dart:ui/hooks.dart:122)
#24 _drawFrame (dart:ui/hooks.dart:109)
Flutter和原生性能對(duì)比
雖然使用原生實(shí)現(xiàn)(左)和Flutter實(shí)現(xiàn)(右)的全品類頁(yè)面在實(shí)際使用過(guò)程中幾乎分辨不出來(lái):
但是我們還需要在性能方面有一個(gè)比較明確的數(shù)據(jù)對(duì)比。
我們最關(guān)心的兩個(gè)頁(yè)面性能指標(biāo)就是頁(yè)面加載時(shí)間和頁(yè)面渲染速度。測(cè)試頁(yè)面加載速度可以直接使用美團(tuán)內(nèi)部的Metrics性能測(cè)試工具,我們將頁(yè)面Activity對(duì)象創(chuàng)建作為頁(yè)面加載的開始時(shí)間,頁(yè)面API數(shù)據(jù)返回作為頁(yè)面加載結(jié)束時(shí)間。
從兩個(gè)實(shí)現(xiàn)的頁(yè)面分別啟動(dòng)400多次的數(shù)據(jù)中可以看到,原生實(shí)現(xiàn)(AllCategoryActivity)的加載時(shí)間中位數(shù)為210ms,F(xiàn)lutter實(shí)現(xiàn)(FlutterCategoryActivity)的加載時(shí)間中位數(shù)為231ms。考慮到目前我們還沒(méi)有針對(duì)FlutterView做緩存和重用,F(xiàn)lutterView每次創(chuàng)建都需要初始化整個(gè)Flutter環(huán)境并加載相關(guān)代碼,多出的20ms還在預(yù)期范圍內(nèi):
因?yàn)镕lutter的UI邏輯和繪制代碼都不在主線程執(zhí)行,Metrics原有的FPS功能無(wú)法統(tǒng)計(jì)到Flutter頁(yè)面的真實(shí)情況,我們需要用特殊方法來(lái)對(duì)比兩種實(shí)現(xiàn)的渲染效率。Android原生實(shí)現(xiàn)的界面渲染耗時(shí)使用系統(tǒng)提供的FrameMetrics接口進(jìn)行監(jiān)控:
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耗時(shí),GPU操作在Flutter引擎內(nèi)部實(shí)現(xiàn),所以要修改引擎來(lái)監(jiān)控完整的渲染耗時(shí),在Flutter引擎目錄下src/flutter/shell/common/rasterizer.cc文件中添加:
void Rasterizer::DoDraw(std::unique_ptr<flow::LayerTree> layer_tree) {
if (!layer_tree || !surface_) {
return;
}
if (DrawToSurface(*layer_tree)) {
last_layer_tree_ = std::move(layer_tree);
#if defined(OS_ANDROID)
if (compositor_context_->frame_count().count() == 101) {
std::ostringstream os;
os << "[";
const std::vector<TimeDelta> &engine_laps = compositor_context_->engine_time().Laps();
const std::vector<TimeDelta> &frame_laps = compositor_context_->frame_time().Laps();
size_t i = 1;
for (auto engine_iter = engine_laps.begin() + 1, frame_iter = frame_laps.begin() + 1;
i < 101 && engine_iter != engine_laps.end(); i++, engine_iter++, frame_iter++) {
os << (*engine_iter + *frame_iter).ToMilliseconds() << ",";
}
os << "]";
__android_log_write(ANDROID_LOG_WARN, "AllCategory", os.str().c_str());
}
#endif
}
}
即可得到每幀繪制時(shí)真正消耗的時(shí)間。測(cè)試時(shí)我們將兩種實(shí)現(xiàn)的頁(yè)面分別打開100次,每次打開后執(zhí)行兩次滾動(dòng)操作,使其繪制100幀,將這100幀的每幀耗時(shí)記錄下來(lái):
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
將測(cè)試結(jié)果的100次啟動(dòng)中每幀耗時(shí)取平均値,得到每幀平均耗時(shí)情況(橫坐標(biāo)軸為幀序列,縱坐標(biāo)軸為每幀耗時(shí),單位為毫秒):
Android原生實(shí)現(xiàn)和Flutter版本都會(huì)在頁(yè)面打開的前5幀超過(guò)16ms,剛打開頁(yè)面時(shí)原生實(shí)現(xiàn)需要?jiǎng)?chuàng)建大量View,F(xiàn)lutter也需要?jiǎng)?chuàng)建大量Widget,后續(xù)幀中可以重用大部分控件和渲染節(jié)點(diǎn)(原生的RenderNode和Flutter的RenderObject),所以啟動(dòng)時(shí)的布局和渲染操作都是最耗時(shí)的。
10000幀(100次×100幀每次)中Android原生總平均値為10.21ms,F(xiàn)lutter總平均値為12.28ms,Android原生實(shí)現(xiàn)總丟幀數(shù)851幀8.51%,F(xiàn)lutter總丟幀987幀9.87%。在原生實(shí)現(xiàn)的觸摸事件處理和過(guò)度繪制充分優(yōu)化的前提下,F(xiàn)lutter完全可以媲美原生的性能。
Flutter目前仍處于早期階段,也還沒(méi)有發(fā)布正式的Release版本,不過(guò)我們看到Flutter團(tuán)隊(duì)一直在為這一目標(biāo)而努力。雖然Flutter的開發(fā)生態(tài)不如Android和iOS原生應(yīng)用那么成熟,許多常用的復(fù)雜控件還需要自己實(shí)現(xiàn),有的甚至?xí)容^困難(比如官方尚未提供的ListView.scrollTo(index)功能),但是在高性能和跨平臺(tái)方面Flutter在眾多UI框架中還是有很大優(yōu)勢(shì)的。
開發(fā)Flutter應(yīng)用只能使用Dart語(yǔ)言,Dart本身既有靜態(tài)語(yǔ)言的特性,也支持動(dòng)態(tài)語(yǔ)言的部分特性,對(duì)于Java和JavaScript開發(fā)者來(lái)說(shuō)門檻都不高,3-5天可以快速上手,大約1-2周可以熟練掌握。
在開發(fā)全品類頁(yè)面的Flutter版本時(shí)我們也深刻體會(huì)到了Dart語(yǔ)言的魅力,Dart的語(yǔ)言特性使得Flutter的界面構(gòu)建過(guò)程也比Android原生的XML+JAVA更直觀,代碼量也從原來(lái)的900多行減少到500多行(排除掉引用的公共組件)。Flutter頁(yè)面集成到App后APK體積至少會(huì)增加5.5MB,其中包括3.3MB的SO庫(kù)文件和2.2MB的ICU數(shù)據(jù)文件,此外業(yè)務(wù)代碼1300行編譯產(chǎn)物的大小有2MB左右。
Flutter本身的特性適合追求iOS和Android跨平臺(tái)的一致體驗(yàn),追求高性能的UI交互效果的場(chǎng)景,不適合追求動(dòng)態(tài)化部署的場(chǎng)景。Flutter在Android上已經(jīng)可以實(shí)現(xiàn)動(dòng)態(tài)化部署,但是由于Apple的限制,在iOS上實(shí)現(xiàn)動(dòng)態(tài)化部署非常困難,F(xiàn)lutter團(tuán)隊(duì)也正在和Apple積極溝通。
于注解,相信大家都不陌生了,但是這種熟悉對(duì)于Android開發(fā)者來(lái)說(shuō),也就是僅僅是它認(rèn)識(shí)你,你不認(rèn)識(shí)它吧,因?yàn)椋覀儙缀醵纪A粼谟蒙狭恕O馾agger2,像retrofit,像greenDao,他們都是滿滿的使用了注解,而我們只是停留在知其然缺不知其所以然的層面,那么我本次分享講分成幾批博客共享給大家一個(gè)體系的又不失針對(duì)Android平臺(tái)需要的恰到好處的給大家整理真正需要掌握的知識(shí)體系。
注解的定義
Java 注解用于為 Java 代碼提供元數(shù)據(jù)。作為元數(shù)據(jù),注解不直接影響你的代碼執(zhí)行,但也有一些類型的注解實(shí)際上可以用于這一目的。Java 注解是從 Java5 開始添加到 Java 的。
注解即標(biāo)簽
如果把代碼想象成一個(gè)具有生命的個(gè)體,注解就是給這些代碼的某些個(gè)體打標(biāo)簽
如何自定義注解
public @interface Test { }
它的形式跟接口很類似,不過(guò)前面多了一個(gè) @ 符號(hào)。上面的代碼就創(chuàng)建了一個(gè)名字為 Test 的注解。
你可以簡(jiǎn)單理解為創(chuàng)建了一張名字為 Test的標(biāo)簽。
@Test public class TestAnnotation { }
創(chuàng)建一個(gè)類 TestAnnotation,然后在類定義的地方加上 @Test就可以用 Test注解這個(gè)類了
你可以簡(jiǎn)單理解為將 Test 這張標(biāo)簽貼到 TestAnnotation這個(gè)類上面。
元注解
元注解是可以注解到注解上的注解,或者說(shuō)元注解是一種基本注解,但是它能夠應(yīng)用到其它的注解上面。
如果難于理解的話,你可以這樣理解。元注解也是一張標(biāo)簽,但是它是一張?zhí)厥獾臉?biāo)簽,它的作用和目的就是給其他普通的標(biāo)簽進(jìn)行解釋說(shuō)明的。
元標(biāo)簽有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。
注解的屬性
注解的屬性也叫做成員變量。注解只有成員變量,沒(méi)有方法。
需要注意的是,在注解中定義屬性時(shí)它的類型必須是 8 種基本數(shù)據(jù)類型外加 類、接口、注解及它們的數(shù)組
注解中屬性可以有默認(rèn)值,默認(rèn)值需要用 default 關(guān)鍵值指定
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Test{ int id() default -1; String msg() default "Hello"; }
上面代碼定義了 TestAnnotation 這個(gè)注解中擁有 id 和 msg 兩個(gè)屬性。在使用的時(shí)候,我們應(yīng)該給它們進(jìn)行賦值。
賦值的方式是在注解的括號(hào)內(nèi)以 value="" 形式,多個(gè)屬性之前用 ,隔開
@Test(id=1,msg="hello annotation") public class TestAnnotation { }
注解的提取
注解與反射。
注解通過(guò)反射獲取。首先可以通過(guò) Class 對(duì)象的 isAnnotationPresent() 方法判斷它是否應(yīng)用了某個(gè)注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通過(guò) getAnnotation() 方法來(lái)獲取 Annotation 對(duì)象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
或者是 getAnnotations() 方法。
public Annotation[] getAnnotations() {}
前一種方法返回指定類型的注解,后一種方法返回注解到這個(gè)元素上的所有注解。
如果獲取到的 Annotation 如果不為 null,則就可以調(diào)用它們的屬性方法了。比如
@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()); } } }
注解的使用場(chǎng)景
作為一個(gè)資歷不淺的Android開發(fā),這幾年我面試過(guò)不少人。發(fā)現(xiàn)大多數(shù)面試者,雖然看起來(lái)工作努力,但他們表現(xiàn)出來(lái)的能力水平,卻不足以通過(guò)面試,或拿不到期望的薪資。
在我看來(lái),造成這種情況的原因,主要有這么兩方面:
第一,“知其然不知其所以然”。做了幾年技術(shù),開發(fā)了一些業(yè)務(wù)應(yīng)用,但沒(méi)有思考過(guò)這些技術(shù)選擇背后的邏輯。所以,我很難定位他們?nèi)蘸蟮某砷L(zhǎng)潛力,也不會(huì)放心把有一定深度的任務(wù)交給他們。
第二,知識(shí)碎片化,不成系統(tǒng)。事實(shí)上,當(dāng)面試者無(wú)法完整、清晰地描述自己所開發(fā)的系統(tǒng)或使用的相關(guān)技術(shù)時(shí),面試官就會(huì)懷疑他是否具備解決復(fù)雜問(wèn)題、設(shè)計(jì)復(fù)雜系統(tǒng)的能力。
所以,如果你平時(shí)只知道埋頭苦干,或過(guò)于死磕某個(gè)實(shí)現(xiàn)細(xì)節(jié),沒(méi)有抬頭審視過(guò)這些技術(shù),那么在準(zhǔn)備面試時(shí),很有必要好好梳理一下 Android 知識(shí)體系,這樣才能拿下滿意的 Offer。
那么,作為Android 開發(fā)者,該怎樣規(guī)劃自己的學(xué)習(xí)路線,然后一步一個(gè)腳印的向中高級(jí)進(jìn)階呢?本篇文章根據(jù)自己的一些見(jiàn)解梳理一下。
我發(fā)現(xiàn)好多人始終停留在兩三年的水平上沒(méi)有突破。
那么為什么很多人會(huì)一直停留在兩三年的水平上,而后一直在重復(fù)以往的經(jīng)驗(yàn)?
我認(rèn)為最主要的一點(diǎn)就是主觀能動(dòng)性,或者說(shuō)興趣,如果你對(duì)Android開發(fā)沒(méi)有太大的興趣,那么還是盡早換方向吧。有了興趣,然后就是要有一個(gè)比較正確的鉆研路線,不要這也搞那也抓,最后什么都沒(méi)精通又好像什么知道。
我覺(jué)得一個(gè)比較好的路線是,把日常開發(fā)常用的各種系統(tǒng)庫(kù),開源庫(kù),代碼好好研究一遍,比如我用了butternife就要了解背后的apt,以及apt衍生出來(lái)的一系列庫(kù)比如Hugo。當(dāng)然這大部分情況是初級(jí)進(jìn)階的第一步。這個(gè)階段應(yīng)該是盡量很好的用技術(shù)完成業(yè)務(wù)需求。
第二個(gè)階段,我覺(jué)得可以嘗試去了解Android Framework比較細(xì)節(jié)的一些東西,比如activity啟動(dòng)流程,順便分析清除Activity stack的管理,比如了解Android的資源加載機(jī)制,順便了解aapt是如何打包Android資源的;又比如Java的類加載機(jī)制,這里配合資源的加載機(jī)制,很自然的就可以去了解Android的hotpatch機(jī)制,插件化的實(shí)現(xiàn),開一些這方便的開源庫(kù)或者自己擼一個(gè)也就自然而然。
第三個(gè)階段,橫向擴(kuò)展,到這個(gè)階段并不是說(shuō)比第二個(gè)階段更加高級(jí)了,而是對(duì)第二個(gè)階段的一些補(bǔ)充,比如你是不是可以了解一下web開發(fā),這樣做hybrid開發(fā)的時(shí)候就會(huì)更順手。是不是要了解一下這么火爆的ReactNative&Weex技術(shù),甚至可以把他們的一些思想拿過(guò)來(lái)自己用,比如我司內(nèi)部就有很多項(xiàng)目是用了JSCore和CssLayout來(lái)實(shí)現(xiàn)一些更輕量的動(dòng)態(tài)化技術(shù)的。正如科學(xué)領(lǐng)域很多重大貢獻(xiàn)都是在交叉學(xué)科領(lǐng)域出現(xiàn)的。技術(shù)上到了這個(gè)階段甚至可以做到技術(shù)影響業(yè)務(wù),技術(shù)驅(qū)動(dòng)業(yè)務(wù)。
我也已經(jīng)做了好幾年開發(fā)了,還記得剛出來(lái)工作的時(shí)候覺(jué)得自己很牛逼,現(xiàn)在回想起來(lái)也蠻好笑的。懂的越多的時(shí)候你才會(huì)發(fā)現(xiàn)懂的越少。因?yàn)槿绻愕闹R(shí)是一個(gè)圓,當(dāng)你的圓越大時(shí),圓外面的世界也就越大。
最近看到很多Android新手問(wèn)Android學(xué)習(xí)路線,學(xué)習(xí)方法啊,如何入門啊,所以我以我的工作經(jīng)驗(yàn)給大家總結(jié)一下,讓大家少走彎路,提取一些工作中經(jīng)常用到的技術(shù)。當(dāng)然,說(shuō)一千道一萬(wàn),最重要的還是學(xué)以致用,把學(xué)過(guò)的知識(shí)融會(huì)貫通。
相信大家在覺(jué)得迷茫的時(shí)候,經(jīng)常會(huì)選擇自暴自棄、或者完全按照自己的喜好去學(xué)習(xí),這是有利有弊的,總體來(lái)說(shuō)弊大于利。我一直認(rèn)同一個(gè)觀點(diǎn),就是“你的認(rèn)知比你的知識(shí)更加重要”,換句話說(shuō),也就是“你知道你需要去學(xué)什么,比你所學(xué)的知識(shí)更加重要”,如何知道自己缺乏什么,應(yīng)該去學(xué)習(xí)哪些技能,這才是從思維上、思想上改變你對(duì)學(xué)習(xí)的態(tài)度!
如何知道自己應(yīng)該具備哪些技能呢?或者說(shuō),我自己還缺乏哪些技能呢?
這個(gè)問(wèn)題一直是困擾著很多人,解答這個(gè)問(wèn)題其實(shí)很簡(jiǎn)單,這個(gè)問(wèn)題的回答,往往離不開行業(yè)的最新動(dòng)態(tài):從下面培訓(xùn)機(jī)構(gòu)的培訓(xùn)清單、招聘信息的招聘要求就可以看出來(lái)。下面先介紹如何從培訓(xùn)機(jī)構(gòu)的培訓(xùn)清單開出行業(yè)的動(dòng)態(tài)。
培訓(xùn)機(jī)構(gòu)一直是互聯(lián)網(wǎng)行業(yè)的風(fēng)向標(biāo),培訓(xùn)機(jī)構(gòu)的嗅覺(jué)是十分敏銳的,他們必須關(guān)注行業(yè)的最新技術(shù)方向、最新技術(shù)。所以,在一定程度上面,雖然我們大多數(shù)人都挺抵觸培訓(xùn)機(jī)構(gòu),但是培訓(xùn)機(jī)構(gòu)的培訓(xùn)清單可以作為我們學(xué)習(xí)的風(fēng)向標(biāo)、學(xué)習(xí)清單!
例子1:(介紹一下往高級(jí)Android開發(fā)工程師進(jìn)階需要具備的哪些知識(shí))
其實(shí)這個(gè)培訓(xùn)清單我個(gè)人覺(jué)得是目前來(lái)說(shuō)總結(jié)得比較好的Android進(jìn)階資料。基本涵蓋了各種企業(yè)開發(fā)必備的新技術(shù):RN、異步操作RXJava庫(kù)、熱修復(fù)、插件化、設(shè)計(jì)模式、性能優(yōu)化等知識(shí)。這些其實(shí)都是我們學(xué)習(xí)的方向標(biāo),按照這些清單,自己去找各種資料學(xué)習(xí)。
其實(shí)對(duì)于Android基礎(chǔ)的話,不外乎四大組件的基本使用、UI控件、布局的使用、版本控制工具、NDK基礎(chǔ)等等。
當(dāng)然隨著Android技術(shù)的日新月異,Android開發(fā)對(duì)開發(fā)人員的基礎(chǔ)知識(shí)的要求就更上一層樓了,比如說(shuō):動(dòng)態(tài)權(quán)限管理、Kotlin基礎(chǔ)、Gradle基礎(chǔ)、AndroidStudio基本使用等等。
例子2:(從招聘信息上了解目前互聯(lián)網(wǎng)大廠需要學(xué)習(xí)的一些熱門技術(shù))
我一直都強(qiáng)調(diào)要多看招聘信息,雖然我們不一定要找工作,但是按照企業(yè)的招聘要求,一定程度上也可以知道我們需要什么,并且同時(shí)可以知道,越高級(jí)的Android開發(fā)有什么更高的要求。
我們常見(jiàn)的招聘網(wǎng)站有:智聯(lián)招聘、拉勾網(wǎng)、boss直聘等,這里大家手機(jī)上安裝一個(gè)對(duì)應(yīng)的APP,吃飯或者上班路上沒(méi)事可以翻一翻。那么如何通過(guò)招聘信息獲取我們需要什么呢?那么就需要我們仔細(xì)分析下面幾份招聘信息。
大家第一時(shí)間關(guān)注的肯定是薪水問(wèn)題,20K到50K,可以看到,這是一份相對(duì)來(lái)說(shuō)比較高級(jí)Android開發(fā)工程師的招聘信息,根據(jù)上面的要求,其實(shí)我們可以知道更多。
一般人可能會(huì)覺(jué)得這是一份很普通的招聘要求,但是在我的角度來(lái)看,可以看出很多有用的信息,下面我?guī)е蠹襾?lái)分析解讀一下這份招聘要求吧。
1. Android底層的機(jī)制的熟悉,關(guān)于這個(gè)問(wèn)題,其實(shí)在這里我并不想談?wù)撎啵驗(yàn)楸鞠盗形恼碌暮罄m(xù)我會(huì)專門有一篇文章講這個(gè)怎么去學(xué)習(xí)。一句話總結(jié)就是,熟悉Android底層機(jī)制,或者說(shuō)Android源碼,對(duì)上層應(yīng)用的編程開發(fā)是有很多好處的,例如寫出高可復(fù)用性、高效率、高質(zhì)量的代碼。其中,面試常問(wèn)的源碼分析有消息機(jī)制、四大組件、進(jìn)程間通信、WMS、PMS等等。
2. 各種架構(gòu)設(shè)計(jì)能力,這就需要大家多去學(xué)習(xí)設(shè)計(jì)模式、各種軟件架構(gòu)、設(shè)計(jì)思想上的東西,例如MVC、MVP、MVVM、重構(gòu)、代碼規(guī)范等等。
3. 在一般的互聯(lián)網(wǎng)公司,網(wǎng)絡(luò)通信是最重要的,因此我們就有必要掌握基本的Socket編程、各種網(wǎng)絡(luò)請(qǐng)求框架,比如Retrofit2等等,進(jìn)階的話,就需要自己去研究分析這些框架的源碼、自己動(dòng)手用組件化的思想去封裝這些框架,防止代碼的侵入性等等。
4. 新技術(shù)的調(diào)研與學(xué)習(xí),企業(yè)需要的是在盡量少的時(shí)間,開發(fā)出最優(yōu)秀的產(chǎn)品,因此新技術(shù)是必不可少的。因此我們也需要保持不斷學(xué)習(xí)充實(shí)自己的習(xí)慣。這里提到“研究新技術(shù)”,既然是研究,那么公司為了減少大家的學(xué)習(xí)成本,肯定是需要你將研究的成功通過(guò)文檔的形式輸出,以方便其他員工的快速上手,因此這里隱含了文檔的輸出能力。
5. 經(jīng)驗(yàn),關(guān)于這個(gè)也沒(méi)有什么好說(shuō)的,很多東西,如果你遇到過(guò)就有經(jīng)驗(yàn)了,處理起來(lái)就比較快了。但是有一些難題你從來(lái)沒(méi)有遇到過(guò),那么難度就是無(wú)限大的,經(jīng)驗(yàn)需要一點(diǎn)一滴地積累,多向大牛取經(jīng)往往是實(shí)現(xiàn)彎道超車的最好辦法。
6. 成熟項(xiàng)目的工作經(jīng)驗(yàn),這是作為應(yīng)聘者一個(gè)最有閃光點(diǎn)的地方,也是在校生校招的一個(gè)最有說(shuō)服力的點(diǎn)。當(dāng)然,除了在公司工作,也推薦大家去一些培訓(xùn)網(wǎng)站上面學(xué)習(xí)一些項(xiàng)目的實(shí)戰(zhàn)視頻教程,這也是一種快速學(xué)習(xí)的方法,但是學(xué)習(xí)完一定要加以總結(jié),最好以文檔、博客的方式進(jìn)行輸出,并保持分享,互相交流才能知道自己的錯(cuò)誤、不足,才能獲取更多技術(shù),進(jìn)步更加快。
7. 性能優(yōu)化,這是面試常見(jiàn)的問(wèn)題。性能優(yōu)化跟一個(gè)APP的用戶體驗(yàn)息息相關(guān),很多公司都十分重視這方面的知識(shí)。
程序員是吃青春飯的嗎?等我們老了,技術(shù)過(guò)時(shí)了,公司有什么理由不裁掉我們,去雇一些既有活力、薪資要求又低的年輕人呢?這個(gè)老生常談的問(wèn)題困擾著諸多漸入中年的程序員。接下來(lái)講解教你如何增強(qiáng)自己的核心競(jìng)爭(zhēng)力,在知識(shí)飛速更新的行業(yè)中站穩(wěn)腳跟,跨過(guò)“初級(jí)工程師”和“高級(jí)工程師”之間的鴻溝。
我認(rèn)為開發(fā)者中有很多我們不太重視的軟技能,這些軟技能都有可能成倍地增加我們工作的影響力(作為個(gè)人貢獻(xiàn)者和技術(shù)負(fù)責(zé)人)。這些軟技能包括:
作為一名程序員 ,編碼硬實(shí)力固然很重要,但是這些軟技能也同樣重要,這決定著你的核心競(jìng)爭(zhēng)力的強(qiáng)弱。
高級(jí)開發(fā)者,會(huì)在工作中解決問(wèn)題,而非制造問(wèn)題。
他們減少壓力。他們按時(shí)完成任務(wù)。他們知道如何編寫經(jīng)得起時(shí)間考驗(yàn)、可維護(hù)的代碼。他們值得更高的工資。他們對(duì)項(xiàng)目的方向可以有準(zhǔn)確的把控。他們可以發(fā)現(xiàn)當(dāng)前流程中的缺陷,并使每個(gè)人都接受他們的想法以進(jìn)行改進(jìn)。他們可以指導(dǎo)應(yīng)屆畢業(yè)生。他們處事冷靜,不會(huì)在周二與你的最大客戶的電話會(huì)議上情緒崩潰、破口大罵。
學(xué)習(xí)一線大廠的各項(xiàng)技術(shù):
大廠是我們的技術(shù)先驅(qū),不僅僅是各種技術(shù),還有很多令你大開眼界的使用方法,學(xué)習(xí)這些有利于自己競(jìng)爭(zhēng)力的提高,深入理解后也可以提升你的學(xué)習(xí)效率。
現(xiàn)在競(jìng)爭(zhēng)這么激烈,只有通過(guò)不斷學(xué)習(xí),提高自己,才能保持競(jìng)爭(zhēng)力。
1.2020大廠面試高頻知識(shí)點(diǎn)
2.Jetpack架構(gòu)組件從入門到精通
3.Framework精編內(nèi)核解析
主要內(nèi)容包含:
4.Kotlin強(qiáng)化實(shí)戰(zhàn)(附Demo)
5.Android設(shè)計(jì)思想解讀開源框架
6.NDK模塊開發(fā)
定期面試:
光學(xué)習(xí)了大廠的技術(shù)還不夠,如何了解當(dāng)下Android市場(chǎng)需要哪些技術(shù)呢?
定期的面試足以讓你了解當(dāng)下市場(chǎng)需要哪些技術(shù),也更加容易確定自身的短板在哪,同時(shí),也可以督促自己學(xué)習(xí)提升(小伙子,你了解的技術(shù)有點(diǎn)落伍了呀…)
萬(wàn)一拿到了高薪Offer,豈不是更賺?
《379頁(yè)Android開發(fā)面試寶典》
多看大佬的學(xué)習(xí)筆記,學(xué)習(xí)大佬的設(shè)計(jì)思想:
只要是程序員,不管是Java還是Android,如果不去閱讀源碼,只看API文檔,那就只是停留于皮毛,這對(duì)我們知識(shí)體系的建立和完備以及實(shí)戰(zhàn)技術(shù)的提升都是不利的。
真正最能鍛煉能力的便是直接去閱讀源碼,不僅限于閱讀各大系統(tǒng)源碼,還包括各種優(yōu)秀的開源庫(kù)。
《486頁(yè)超全面Android開發(fā)相關(guān)源碼精編解析》
以上這些內(nèi)容均免費(fèi)分享給大家,需要完整版的朋友,直接私信回復(fù)【資料】一鍵領(lǐng)取!!!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。