大家好,我是郭樹煜,Github GSY 系列開源項目的作者,系列包括有 GSYVideoPlayer 、GSYGitGithubApp(FlutterReactNativeKotlinWeex) 四大版本,目前總 star 在 17 k+ 左右,主要活躍在掘金社區,id 是戀貓的小郭,主要專欄有《Flutter完整開發實戰詳解》系列等,平時工作負責移動端項目的開發,工作經歷從 Android 到 React Native 、Weex 再到如今的 Flutter ,期間也參與過 React 、 Vue 、小程序等相關的開發,算是一個大前端的選手吧。

這次主要是給大家分享 Flutter 相關的內容,主要涉及做一些實戰和科普性質的內容。

一、移動開發的現狀

恰逢最近谷歌 IO 大會結束,大會後也在線上線下和大家有過交流,總結了下大家最關係的問題有:

1、谷歌在 Kotlin-First 的口號下又推廣 Dart + Flutter 衝突嗎?

這個問題算是被問得最多的一個,先說觀點:我個人認為其實這並不衝突,因為有個 誤區就是認為跨平台開發就可以拋棄原生開發!

如果從事過跨平台開發的同學應該知道,平台提供的功能向來是有限的,而面對產品經理的各種 「點歪技能樹」 的需求,很多時候你是需要基於框架外提供支持,常見的就是 混合開發或者原生插件支持

所以這裡我表達的是,目前 KotlinDart 更多是相輔相成 ,而一旦業務複雜度到一定程度,跨平台框架還可能存在降低工作效率的問題,比如針對新需求,需要重複開發 Android/IOS 的原生插件做支持,這也是 Aribnb 曾經選擇放棄 React Native 的原因之一。

與我而言,跨平台的意義在於解決的是端邏輯的統一 ,至少避免了邏輯重複實現,或者 IOSAndroid 之間爭論 誰對誰錯 的問題,甚至可以統一到 web 端等等。

2、React Native 和 Flutter 之間的對比

Flutter 作為後來者,難免會被用來和 React Native 進行對比,在這個萬物皆是 JS 的時代,DartFlutter 的出現顯得尤為扎眼。

在設計上它們有著許多相似之處,響應式設計/async支持/setState更新 等等,同時也有著各種的差異,而大家最為關心的,無非 性能、支持、上手難易、穩定性程度 這四方面:

  • 性能上 Flutter 的確實會比 React Native 好 ,如下圖所示,這是由框架底層決定的,當然目前 React Native 也在進行下一代的優化, 而對此最直觀的數據就是:GSY系列 在18年用於閑魚測試下的對比數據了

同時注意不要用模擬器測試性能,特別是IOS模擬器做性能測試,因為 Flutter 在 IOS模擬器中純 CPU ,而實際設備會是 GPU 硬體加速,同時只在 Release 下對比性能。

  • 支持上 Flutter 和 React Native , 都存在第三方包質量參差不齊的問題,而目前在這一塊 Flutter 是弱於 React Native 的 ,畢竟 React Native 發展已久,雖然版本號一直不到 1.0,但是在 JS 的加持下生態豐富,同時也是因為平台特性的原因,諸如 WebView 、地圖等控制項的支持上現在依舊不夠好,這個後面也會說道。
  • 上手難易度上,Flutter 配置環境和運行的「成功率」比 React Native 高不少 ,這裡面有 node_module 黑洞這個坑,也有 React Native 本身依賴平台控制項導致的,至少我曾經試過接手一個 React Native 跑了一天都沒跑起來的經歷,同時 Flutter 在運行和SDK版本升級的陣痛也會少很多。
  • 穩定性:Flutter 中大部分異常是不會引起應用崩潰 ,更多會在 Debug 上體現為紅色錯誤堆棧,Release 上 UI 異常等等。

如果你是前端,我會推薦你先學 React Native,如果你是原生開發,我推薦你學 Flutter

在 React Native 0.59.x 版本開始,React 已經將許多內置控制項和庫移出主項目,希望模糊 React 和 React Native 的界線,統一開發,這裡的理念和 Flutter 很像。 Flutter 暫時不支持熱更新!!!!!!!!

二、Flutter 實戰

1、Dart 中有意思的一些東西

1.1、var 的語法糖和 dynamic

var 的語法糖是在賦值時才自推導出類型的 ,而 dynamic 是動態聲明,在運行時檢測,它們的使用有時候容易出現錯誤。

如下圖所以說, - var 初始化時被指定為 dynamic 類型的。 - 然後賦值的時候初始化為 String 類型,這時候進行 ++ 操作就會出現運行時報錯, - 如下圖2如果在初始化指定類型的,那麼編譯時就會告訴你錯誤了。

1.2、各類操作符

如下圖所示,Dart 支持很多有意思的操作符,如下圖: - 執行的時候首先是判斷 AA 如果為空,就返回 999 ; - 之後如果 AA 為空,就為 AA 賦值 999; - 之後對 AA 進行整除 999 ,輸出結果 10

1.3、支持操作符重載

如下圖所示,Dart 中是支持操作符重載的,這樣可以比較直觀我們的代碼邏輯,並且簡化代碼時的調用。

1.4、方法當做參數傳遞

如下圖所示,在 Dart 中方法時可以作為參數傳遞的,這樣的形式可以讓我們更靈活的組織代碼的邏輯。

1.5、async await / async* yield

Dartasync await / async* yield 等語法糖,代表 Dart 中的 FutureStream 操作,它們對應 Dart 中的非同步邏輯支持。

sync* / yield 對應 Stream 的同步操作。

1.6、Mixins

Dart 中支持混入的模式,如下圖所示,混入時的基礎順序是從右到左依次執行的,而且和 super 有關,同時 Dart 還支持 mixin 關鍵字的定義。

Flutter 的啟動類用的就是 mixins 方式

1.7、isolate

Dart 中單線程模式中增加了 isolate 提供跨線程的真非同步操作,而因為 Dart 中線程不會共享內存,所以也不存在死鎖,從而也導致了 isolate 之間數據只能通過 port 的埠方式發送介面,類似於 Scoket 的方式,同時提供了 compute 的封裝介面方便調用。

1.8 call

Dart 為了讓類可以像函數一樣調用,默認都可以實現 call() 方法,同樣 typedef 定義的方法也是具備 call() 條件。

比如我定義了一個 CallObject

class CallObject {

List<Widget> footerButton = [];

call(int i, double e) => "$i xxxx $e";
}

就可以通過以下執行

CallObject callObject = CallObject();
print(callObject(11, 11.0));
print(callObject?.call(11, 11.0));

然後我定義了

typedef void ValueFunction(int i);

ValueFunction vt = (int i){
print("zzz $i");
};

就可以通過直接執行和判空執行處理

vt(666);
vt?.call(777);

2、Flutter 中常見的

2.1、ChangeNotifier

如下圖所示,ChangeNotifier 模式在 Flutter 中是十分常見的,比如 TextField 控制項中,通過 TextEditingController 可以快速設置值的顯示,這是為什麼呢?

如下圖所示,這是因為 TextEditingController 它是 ChangeNotifier 的子類,而 TextField 的內部對其進行了 addListener,同時我們改變值的時候調用了notifyListener,觸發內部 setState

2.2、InheritedWidget

Flutter 中所有的狀態共享都是通過它實現的,如自帶的 ThemeLocalizations ,或者狀態管理的 scoope_modelflutter_redux 等等,都是基於它實現的。

如下圖是 SliderTheme 的自定義實現邏輯,默認 Theme 中是包含了 SliderTheme,但是我們可以通過覆蓋一個新的 SliderTheme 嵌套去實現自定義,然後通過 SliderTheme theme = SliderTheme(context); 獲取,其中而 context 的實現就是 Element

ElementinheritFromWidgetOfExactType 方法實現里,有一個 Map<Type, InheritedElement> _inheritedWidgets 的對象。

_inheritedWidgets 一般情況下是空的,只有當父控制項是 InheritedWidget 或者本身是 InheritedWidgets 時才會有被初始化,而當父控制項是 InheritedWidget 時,這個 Map 會被一級一級往下傳遞與合併 。 所以當我們通過 context 調用 inheritFromWidgetOfExactType 時,就可以往上查找到父控制項的 Widget

2.3、StreamBuilder

StreamBuilder 一般用於通過 Stream 非同步構建頁面的,如下圖所示,通過點擊之後,綠色方框的文字會變成 addNewxxx,因為 Stream 進行了 map 變化,同時一般實現 bloc 模式的時候,經常會用到它們。

類似的還有 FutureBuilder

2.4、State 中的參數使用

一般 Widget 都是一幀的,而 State 實現了 Widget 的跨幀繪製,一般定義的時候,我們可以如下圖一樣實現,而如下圖尖頭所示,這時候我們點擊 setState 改變的時候,是不會出現效果的,為什麼呢?

其實 State 對象的創建和更新時機導致的:

  • 1、createState 只在 StatefulElement 創建時才會被創建的。
  • 2、StatefulElement 的 createElement 一般只在 inflateWidget 調用。
  • 3、updateChild 執行 inflateWidget 時, 如果 child 存在可以更新的話,不會執行 inflateWidget。

3、四棵樹

Flutter 中主要有 WidgetElementRenderObjectLayer 四棵樹,它們的作用是:

  • Widget :就是我們平常寫的控制項,Flutter 宇宙中萬物皆 Widget ,它們都是不可變一幀,同時也是被人吐槽很多的嵌套模式,當然換個角度,事實上你把他當作 Widget 配置文件來寫或者就好理解了。
  • Element :它是 BuildContext 的實現類,Widget 實現跨幀保存的 state 就是存放在這裡,同時它也充當了 WidgetRenderObject 之間的橋樑。
  • RenderObject :它才是真正幹活(layout、paint)等,同時它才是真實的 「dom」 。
  • Layer :一整塊的重繪區域(isRepaintBoundary),決定重繪的影響區域。

skia 在繪製的時候,saveLayer 是比較消耗性能的,比如透明合成、clipRRect 等等都會可能需要 saveLayer 的調用, 而 saveLayer 會清空GPU繪製的緩存,導致性能上的損耗,所以開發過程中如果掉幀嚴重,可以針對這一塊進行優化。

4、手勢

Flutter 在手勢中引入了競技的概念, Down 事件在 Flutter 中尤為重要。

  • PointerDownEvent 是一切的起源,在 Down 事件中一般不會決出勝利者。
  • MOVEUP 的時候才競爭得到響應。
  • 以點擊為例子:Down 時添加進去參與競爭,UP 的時候才決定誰勝利,勝利條件是:

I、UP 的時候如果只有一個,那麼就是它了。

II、UP 的時候如果有多個,那麼強制隊列里第一個直接勝利。

  • 這裡包含了有趣的點就是,都在 UP 的時候才響應,那麼 Down 事件怎麼先傳遞出去了?

FLutter 在這裡做了一個 didExceedDeadline 機制 ,事實上在上面的 addPointer 的時候,會啟動了一個定時器,默認 100 ms,如果超過指定時間沒 UP ,那就先執行這個 didExceedDeadline 響應 Down 事件。

  • 那問題又來了,如果這時候隊列里兩個呢?

它們的 onTapDown 都會被觸發,但是 onTap 只有一個獲得。

  • 如果有兩個滑動 ScrollView 嵌套呢?

舉個簡單的例子,兩個 SingleChildScrollView 的嵌套時,在布局會經歷:

performLayout -> applyContentDimensions -> applyNewDimensions -> context.setCanDrag(physics.shouldAcceptUserOffset(this));

只有 shouldAcceptUserOffsetture 時,才會添加 VerticalDragGestureRecognizer 去處理手勢。

而判斷條件主要是 return math.max(0.0, child.size.height - size.height); ,也就是如果 child Scroll 的 height 小於父控制項 Scroll 的時候,就會出現 child 不添加 VerticalDragGestureRecognizer 的情況,這時候根本就沒有競爭了。

5、動畫

Flutter 中的動畫是怎麼執行的呢?

我們先看一段代碼,然後這段代碼執行的效果如下圖2所示。

那既然 Widget 都是一幀,那麼動畫肯定有 setState 的地方了。

首先這裡有個地方可以看下,這時候 200 這個數值執行後是會報錯的,因為白框內可見 Tween 中的 T 在這時候會出現既有 int 又有 double ,無法判斷的問題,所以真實應該是 200.0 。

同時你發現沒有,代碼中 parentContainer 在 只有100的情況下,它的 child 可以正常的畫 200,這是因為我們的 paint 沒有跟著 RenerObjcet 的大小走, 所以一般情況下,整個屏幕都是我們的畫版,Canvas 繪製與父控制項大小可以沒關係。

同時動畫是通過 vsync 同步信號去觸發的,就是我們 mixin 的 SingleTickerProviderStateMixin,它內部的 Ticker 會通過 SchedulerBindingscheduleFrameCallback 同步信號觸發重繪 。

動畫後的控制項的點擊區域,和你的動畫數據改變的是 paint 還是 layout 有關 。

6、狀態管理

scope_modelflutter_reduxfish_redux 、甚至還有有 dva_flutter 等等,可以看出狀態管理在 flutter 中和前端十分相近。

這裡簡單說說 scope_model ,它只有一個文件,但是很巧妙,它利用的就是 AnimationBuilder 的特性。

如下圖是使用代碼,在前面我們知道,狀態管理使用的是 InheritedWidget 實現共享的,而當我們對 Model 進行數據改變時,通過調用 notifyListeners 通知頁面更新了。

這裡的原理是什麼呢?

  • 其實 scope_model 內部利用了 AnimationBuilder ,而 Model 實現了 Listenable 介面。
  • Model 設置給了 AnimationBuilder 時, AnimationBuilder 會執行 addListener 添加監聽,而監聽方法里會執行 setState
  • 所以我們改變 set 方法時調用 notifyListeners 就觸發了 setState 去更新了,這樣體現出了前面說的 FLutter 常見的開發模式。

三、混合開發

Android 的角度來說,從方便調試和解耦集成上,我們一般會以 aar 的形式集成混合開發,這裡就會涉及到 gradle 打包的一個概念。

1、如下代碼所示,在項目中進行 gradle 腳本修改,組件化開發模式,用 apk 開發,用 aar 提供集成,正常修改 gradle 代碼即可快速打包。

那如果 Flutter 的項目插件帶有本地代碼呢?

如果開發過 React Native 的應該知道,在原生插件安裝時會需要執行 react-native link ,而這時候會修改項目的gradle 和java代碼。

2、 和 React Native 很有侵入性相比, Flutter 就很巧妙了。

如下圖所示,安裝過的插件會出現在 .flutter_plugins 文件中,然後通過讀取文件,動態在 setting.gradleflutter.gradle 中引入和依賴:

所以這時候我們可以參考打包,修改我們的gradle腳本,利用 fat-aar 插件將本地 projcet 也打包的 aar 里。

3、混合開發的最大痛點是什麼?

肯定是堆棧管理!!! 所以項目開發了 flutter_boost 來解決這個問題。

  • 堆棧統一到了原生層。
  • 通過一個唯一 engine ,切換 Surface 渲染顯示。
  • 每個 Activity 就是一個 Surface ,不渲染的頁面通過截圖緩存畫面。

flutter_boost 截止到我測試的時間 2019-05-16, 只支持 1.2之前的版本

四、PlatformView

混合開發除了集成到原生工程,也有將原生控制項集成到 Flutter 渲染樹里里的需求。

首先我們看看沒有 PlatformView 之前是如何實現 WebView 的,這樣會有什麼問題?

如下圖所示,事實上 dart 中僅僅是用了一個 SingleChildRenderObjectWidget 用於佔位,將大小傳遞給原生代碼,然後在原生代碼里顯示出來而已。

這樣的時候必定會代碼畫面堆棧問題,因為這個顯示脫離了 Flutter 的渲染樹,通過出現動畫肯定會不一致。

4.1 AndroidView

AndroidView -> TextureLayer,利用Android 上的副屏顯示與虛擬內存顯示原理。

  • 共享內存,實時截圖渲染技術。
  • 存在問題,耗費內存,頁面複雜時慢。

這部分因為之前以前聊過,就不贅述了

三、Flutter Web

RN因為是原生控制項,所以在react 和react native 整合這件事上存在難度。

flutter 作為一個UI 框架,與平台無關,在web上利用的是dart2js的能力。 比如Image

  • 因為 Flutter 是一套 UI 框架,整體 UI 幾乎和平台無關,這和 React Native 有很大的區別。(我在開發過程中幾乎無知覺)
  • 在 flutter_web 中 UI 層面與渲染邏輯和 Flutter 幾乎沒有什麼區別,底層的一些區別如: flutter_web 中的 Canvas 是 EngineCanvas 抽象,內部會藉助 dart2js 的能力去生成標籤。
  • React Native 平台關聯性太強,而 Flutter 在多平台上優勢明顯。我們期待官方幫我們解決大部分的適配問題。

Flutter 的平台無關能力能帶來什麼?

  • 1、某些功能頁面,可以一套代碼實現,利用插件安裝引入,在web、移動app、甚至 pc 上,都可以編譯出對應平台的高性能代碼,而不會像 Weex 等一樣存在各種兼容問題。
  • 2、在應用上可以快速實現「降級策略」,比如某種情況下應用產生奔潰了,可以替換為同等 UI 的 h5 顯示,而這些代碼只需要維護一份。

資源推薦

  • Github : github.com/CarGuo
  • RTC社區 : rtcdeveloper.com


推薦閱讀:
相关文章