首頁>技術>

背景

Flutter是Google推出的跨平臺、高效能開發框架,使用Skia作為渲染引擎,不使用平臺控制元件,保證Android和iOS上UI一致性。使用Flutter開發,Android、iOS使用一套Dart程式碼,可以節省開發成本。

通常具有一定規模的App都有一套成熟通用的基礎庫,而且依賴公司體系內的很多基礎庫。使用Flutter重新開發時間和實現成本都很高。所以在Native App中嵌入Flutter功能的混合開發模式是應用Flutter技術的穩健型改造方式。

公寓PMS是一款給公寓管家提供房源管理的APP,前期功能已使用Native開發上線。我們在該專案中使用了Flutter開發,需要實現以下功能:將Flutter整合到已有Native專案中;實現Flutter與Native頁面混合管理;實現Flutter與Native通訊,複用已有Native資源;實現Dart側程式碼開發框架。

Flutter引擎介紹

1. Flutter架構

首先看下Flutter架構圖:

圖1

Flutter的架構主要分為3層:Framework,Engine,Embedder。

Framework使用dart實現,包括Material Design和Cupertino風格的Widgets,文字/圖片/按鈕等基礎Widgets、渲染、動畫、手勢等。

Engine使用C++實現,主要包括:Skia,Dart和Text。Skia是開源的二維圖形庫,提供了適用於多種軟硬體平臺的通用API。在程式碼呼叫 dart:ui庫時,呼叫最終會走到Engine層,然後實現真正的繪製邏輯。

Embedder是一個嵌入層,是將Flutter引擎移植到各個平臺的中間層程式碼,主要包括渲染Surface設定,執行緒設定,以及外掛等。

2.Flutter執行緒模型

Flutter Engine自己不建立管理執行緒。Flutter Engine執行緒的建立和管理是由Embedder負責的。Flutter Engine要求Embedder提供四個Task Runner。儘管Flutter Engine不在乎Runner具體跑在哪個執行緒,但是它需要執行緒配置在整一個生命週期裡面保持穩定。也就是說一個Runner最好始終保持在同一執行緒執行。這四個主要 的Task Runner包括:

Platform Task Runner

Flutter Engine的主Task Runner,執行Platform Task Runner的執行緒可以理解為是主執行緒。類似於Android Main Thread或者iOS的Main Thread。

UI Task Runner Thread(Dart Runner)

UI Task Runner被Flutter Engine用於執行Dart root isolate程式碼。

GPU Task Runner

GPU Task Runner被用於執行裝置GPU的相關呼叫。

IO Task Runner

IO Runner的主要功能是從圖片儲存(比如磁碟)中讀取壓縮的圖片格式,將圖片資料進行處理為GPU Runner 的渲染做好準備。

前面我們提到Engine Runner的執行緒可以按照實際情況進行配置,各個平臺目前有自己的實現策略。Android和iOS平臺上面每一個Engine例項啟動的時候會為UI,GPU,IO Runner各自建立一個新的執行緒。所有Engine例項共享同一個Platform Runner和執行緒。

Flutter官方預設混合方案

多引擎模式

在混合方案中解決的主要問題是如何去處理交替出現的Flutter和Native頁面。Flutter官方給出了一個Keep It Simple的方案:對於連續的Flutter頁面(Widget)只需要在當前FlutterActivity開啟即可,對於間隔的Flutter頁面初始化新的引擎。頁面示意如下圖所示:

這個方案的好處就是簡單易懂,容易使用,但是存在比較嚴重的問題。如果Native頁面與Flutter頁面交替出現,Flutter Engine的數量會線性增加,多引擎模式會造成以下問題:

記憶體問題。多引擎模式下每個引擎之間的Isolate是相互獨立的,所以每一個引擎底層都維護了圖片快取等比較消耗記憶體的物件。冗餘資源問題。通過前文可以知道,引擎在Android和iOS的實現中,每一個Flutter例項會新啟動三個執行緒(IO,GPU和UI),從而帶來了額外的資源使用。頁面間通訊複雜。每一個Flutter頁面在一個隔離的isolate中,頁面間通訊將會變得非常複雜。 外掛的註冊問題。外掛依賴Messenger傳遞訊息,而Messenger由FlutterNativeView實現。多引擎方式使得外掛的註冊和通訊將會變得混亂且難以維護。

綜上,由於多引擎混合方案存在比較多的問題,所以專案中沒有采用此方案。

Flutter Boost實現方案

通過調研發現,阿里閒魚推出了Flutter Boost解決方案,該方案採用的是多個Flutter頁面共享引擎的實現方式,示意圖如下所示:

所有的Flutter頁面共享一個Flutter例項(FlutterView),這種方式能夠有效避免多引擎方式帶來的各種問題,但是單例的實現也使頁面的管理變得更加複雜。為此Flutter Boost提供了一套完整的解決方案。

下面看下Flutter Boost的整體架構圖:

圖4

方案實現分為Native部分與Dart部分:

Native部分概念

Container:Native容器,Fragment(Android),ViewController(iOS)Container Manager:Native容器管理器Messaging:基於Message Channel的訊息通道

Dart部分概念

Container:Flutter Widget的容器,Flutter NavigatorContainer Manager:Flutter 容器管理器 Coordinator: 協調器,接受Messaging訊息,負責呼叫Container Manager的狀態管理。

Native容器與Flutter容器(Navigator)是一一對應的,生命週期也是同步的。當一個Native容器被建立的時候,Flutter對應的容器也被建立,它們通過相同的唯一id關聯起來。當Native的容器被銷燬的時候,Flutter的容器也被銷燬。Flutter容器的狀態是跟隨Native容器,這也就是Native驅動。由Manager統一管理切換當前在螢幕上展示的容器。

效能對比

下圖對官方預設多引擎混合方案和Flutter Boost方案進行了效能對比:

圖5 預設多引擎方式頁面記憶體圖

從上述對比圖可以看出,當連續開啟多個Flutter頁面時,預設多引擎方式頁面的記憶體呈線性增長,而Flutter Boost頁面記憶體保持在一個比較穩定的範圍。所以我們的專案中選用了Flutter Boost方案。

公寓PMS進入Flutter Boost

1.Dart工程部分

在Dart工程的pubspec.yaml中引入Flutter Boost:

flutter_boost:     git:       url: 'https://github.com/alibaba/flutter_boost.git' ref: '0.0.410' 

然後執行flutter packages get獲取Flutter Boost程式碼到本地。

2. Native工程部分(Android)

(1)在setting.gradle中依賴Flutter工程:

setBinding(new Binding([gradle: this, mainModuleName: 'ApartmentClient'])) evaluate(new File( settingsDir.parentFile, 'flutter_apartment/.android/include_flutter.groovy' ))

(2)在build.gradle中引入Flutter Boost的Native工程:

implementation project(':flutter') implementation project(':flutter_boost')

至此就把Flutter Boost接入到公寓PMS工程裡面了,但是要使用Flutter Boost,還需要以下工作要完成。

設計Flutter跳轉協議,接入跳轉框架

Flutter Boost框架沒有整合ARouter等路由跳轉框架。所以我們需要結合自己的業務特點設計跳轉協議。仿照WubaRN的設計思想,我們需要在Native端有一個Flutter通用載體頁,所有的路由跳轉都經由Native側跳轉中心。跳轉框架我們用的是58JumpCenterLib,[h1]跳轉協議如下所示:

wbapartment://jump/house/flutter?params={"container_name":"personalCenter","show_guide":true}

“flutter”:Native側載體頁頁面型別

“params”:跳轉協議引數,其中“container_name”是固定引數,標識Dart側的具體顯示頁面(Navigator);“params”裡面的所有引數都經由MessageChannel透傳到Dart側。

最後需要處理一下Dart側傳過來的跳轉協議,程式碼如下:

private void initFlutterBoost() {    FlutterBoostPlugin.init(new IPlatform() {        ......        /**         * 當Dart側開啟一個本地頁面,將會回撥這個方法,頁面引數拼接在url中         * @param context         * @param url         * @param requestCode         * @return         */        @Override        public boolean startActivity(Context context, String url, int requestCode) {            return PageTransferManager.jump(context, url);        }    });}
完善Native側Flutter載體頁

由於在公寓PMS APP中,我們需要在首頁TAB頁中嵌入Flutter頁面,還需要支援跳轉協議的單獨展示頁面。所以我們的做法是基於Fragment進行封裝,單獨頁面使用FragmentActivity/Fragment的方式。

通過完成以上工作,就可以在公寓PMS專案中使用Flutter Boost框架了。

Flutter Boost的缺點及改進

Flutter Boost是從應用層出發,直接複用FlutterView從而共享Flutter Engine。Native側實現時,需要共享FlutterView,不同Activity/ViewController切換時,需要將FlutterView從前頁面的Activity/ViewController移除,然後新增到當前頁面的Activity/ViewController。這個過程在Android上能夠明顯的感覺到頁面的閃動。Flutter 1.12的釋出完美的解決了這個問題,Flutter 1.12支援將Flutter Engine通過id快取起來,然後啟動頁面時,可以指定使用快取中的Engine,從而徹底解決了混合開發共享引擎的問題。頁面間使用快取引擎方案,需要將Native側頁面和Dart側頁面一一對應。可以使用Message Channel通訊,結合路由跳轉中心,由Native頁面驅動即可。

混合開發中遇到的問題

1. Dart側網路請求問題

在公寓PMS專案中,Dart側網路請求使用的是開源框架dio。但是開發過程中遇到問題,登入資訊、裝置版本等資訊是Native側實現的,Dart側的網路請求header沒法直接獲取這些資訊。解決辦法是通過Message Channel將Native側的header資訊共享給Dart側。

Native側實現:

new MethodChannel(getBoostFlutterView(), METHOD_CHANNEL).setMethodCallHandler(        new MethodChannel.MethodCallHandler() {            @Override            public void onMethodCall(MethodCall call, MethodChannel.Result result) {                if (call.method.equals("getHeader")) {                    IHeadersIntegration commonHeaderUtils = CommonHeaderUtils.getInstance(MainTabActivity.sRef.get());                    Map<String, String> headerMap = commonHeaderUtils.generateParamMap(MainTabActivity.sRef.get());                    result.success(JsonUtils.hashMapToJson(headerMap));                } else {                    result.notImplemented();                }            }        });

Dart側實現:

Map<String, dynamic> headers;try {  final String headerString = await platform.invokeMethod('getHeader');  headers = jsonDecode(headerString);} on PlatformException catch (e) {}Map<String, String> params = new Map();Response response = await dio.get(  dataUrl,  queryParameters: params,  options: Options(headers: headers),);

2. 複用Native的資源圖片問題

Flutter預設將所有的圖片資原始檔打包到assets目錄下,但是我們並不是用Flutter全新開發的專案,圖片資源放在Native側的drawable目錄下,即使是全新的Flutter頁面也會有很多圖片複用已有的Native側圖片,所以在assets目錄下新增圖片資源並不合適。但是Flutter官方並沒有提供直接呼叫drawable目錄下的圖片資源的途徑。

通過調研,可以通過以下方式實現Native側的圖片共享:

Message Channel方式

Dart側通過Message Channel將資原始檔名傳遞到Native側;Native側將對應名稱的drawable以二進位制格式傳遞到Dart側;Dart側接收到二進位制格式圖片後進行渲染。

Native側程式碼:

BasicMessageChannel<Object> messageChannel = new BasicMessageChannel<>(getFlutterView(),        "getPic", StandardMessageCodec.INSTANCE);messageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {    @Override    public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) {     reply.reply(drawableToByte(getResources().getDrawable(getResId(o.toString()))));    }});

Dart側程式碼:

const _messageChannel =    const BasicMessageChannel<Object>("getPic", StandardMessageCodec());Future<Uint8List> getNativeImage(String name) async {  Uint8List result = await _messageChannel.send(name);  return result;}

通過以上步驟,就可以將Android Native側drawable目錄下側資源圖片共享給Dart側控制元件使用,從而避免了重複引入資源。

Dart側開發框架使用

在使用Dart開發需求之初,為了快速實現功能,還有對Flutter特性不熟悉,我們沒有使用開發框架,功能就是程式碼的堆砌。但是,隨著使用頁面的增多,發現專案中業務程式碼耦合嚴重,程式碼可維護性很差。為此,我們進行了相關調研,發現閒魚開源了一款Flutter應用框架——Fish-Redux。

1. Fish-Redux介紹

Fish-Redux是一個基於Redux資料管理的組裝式Flutter應用框架,特別適用於構建中大型的複雜應用。它的最大特點是配置式組裝,它會非常乾淨,易編寫、易維護、易協作。

下面看下Fish-Redux架構圖:

架構主要分為3層,自下向上依次為:

Redux

Redux是一個用來做[可預測][集中式][易除錯][靈活性]的資料管理的框架。所有對資料的增刪改查等操作都由Redux來集中負責。

Fish-Redux通過Redux做集中化的可觀察的資料狀態管理。Fish-Redux在Flutter中對傳統的Redux做了改良。一個元件需要定義一個數據(Struct)和一個Reducer。同時元件之間存在著父依賴子的關係。通過這層依賴關係,解決了【集中】和【分治】之間的矛盾,同時對Reducer的手動層層Combine變成由框架自動完成,簡化了使用Redux的困難。

Component

Component是對區域性的展示和功能的封裝。基於Redux的原則,Fish-Redux對功能細分為修改資料的功能(Reducer)和非修改資料的功能(Effect)。元件是對檢視的分治,也是對資料的分治。通過逐層分治,將複雜的頁面和資料切分為相互獨立的小模組,有利於團隊內的協作開發。

Adapter

Adapter也是對區域性的展示和功能的封裝。它是Component實現上的一種變化,優化了Flutter在使用ListView場景下的效能問題。

綜上所述,Fish-Redux不僅實現了Flutter頁面的狀態管理,更是一套完整的Flutter應用開發框架。下面介紹一下公寓PMS是如何使用Fish-Redux進行開發的。

2. Fish-Redux在公寓PMS的應用

Fish-Redux的接入非常簡單,只需在Flutter專案中pubspec.yaml的dependencies模組設定fish-redux及依賴版本,然後執行flutter packages get即可。

下面以公寓PMS中個人中心頁面介紹:

下圖是個人中心頁面,

該頁面使用Flutter ListView控制元件實現,主要由6個item,5種item組合而成。

下面是個人中心的Page程式碼:

class PersonalCenterPage    extends Page<PersonalCenterPageState, Map<dynamic, dynamic>> {  PersonalCenterPage(): super(          initState: initState,          effect: buildEffect(),          view: buildView,          dependencies: Dependencies<PersonalCenterPageState>(              adapter: NoneConn<PersonalCenterPageState>() +                  PersonalCenterListAdapter()),        );}

PersonalCenterPage由State,Effect,View,Adapter組成。其中,State定義了頁面的資料及狀態資訊;Effect定義了在頁面生命週期開始時,呼叫網路請求api獲取頁面資料;View定義了頁面具體的UI,包括ListView,Loading圖,TitleBar等;Adapter裡面定義了列表包含的Component等。下面著重看下Adapter實現:

class PersonalCenterListAdapter    extends DynamicFlowAdapter<PersonalCenterPageState> {  PersonalCenterListAdapter(): super(          pool: <String, Component<Object>>{            NORMAL_ITEM: NormalItemComponent(),            USER_INFO_ITEM: UserItemComponent(),            LOGOUT_ITEM: LogoutItemComponent(),            TODO_ITEM: TodoItemComponent(),            CONTACT_ITEM: ContactItemComponent(),          },          connector: _HouseListConnector(),          reducer: buildReducer(),        );}

在PageCenterListAdapter中,pool中註冊了列表中所包含的Component及型別;connector是聯結器,負責將網路請求返回的資料轉化成Component渲染時需要的資料;reducer裡定義了修改頁面資料的行為,當網路請求成功後,會呼叫該action觸發頁面渲染。

最後看下Component實現,以UserItemComponent為例:

class UserItemComponent extends Component<UserItemState> {  UserItemComponent() : super(    view: buildView  );}

其中,UserItemState是該模組渲染所需資料,view則是該模組UI邏輯。

下面看下該頁面整體的程式碼結構圖:

從上圖可以看出,使用Fish-Redux開發會使程式碼結構非常清晰,尤其是當頁面邏輯複雜的時候。Fish-Redux使Flutter開發變得簡單,只要按照方法的要求傳入對應的引數即可,實現了面向方法程式設計。

該實現中,將頁面分解成Page->Adapter->Component的結構。當列表頁中新增樣式,只需要開發對應的Component並註冊到Adapter中的pool即可。由於模組拆分到粒度比較細的業務單元,該頁面中實現的Component也可以複用到別的頁面中,避免重複開發。

由於Fish-Redux中包含了Redux的功能,使得開發過程中的狀態傳遞變得非常簡單,只需註冊Action,在接收Action的地方設定響應邏輯,在觸發的地方呼叫dispatch(Action action)方法即可。

此外,Fish-Redux的好處是將邏輯與檢視隔離開,view只負責具體的頁面渲染;而邏輯通過Effect和Reducer實現。所以有這樣的公式Component = View + Effect(可選) + Reducer(可選) + Dependencies(可選)。這不僅很好的實現了程式碼的解耦,也為以後實現UI程式碼自動生成,開發人員只開發業務邏輯程式碼的開發模式提供了可能。

借鑑Flutter中面向函式程式設計,可插拔的頁面元件化思想,我們目前正在對Native專案58APP租房頁面進行重構,以實現程式碼結構的統一,不同頁面元件間的複用,並且頁面可以根據Server返回資料靈活組裝。

總結

本文介紹了Flutter混合開發中遇到的問題及解決辦法,以及開發中應用Fish-Redux的實踐。Flutter混合開發,主要的問題是共享Flutter引擎的實現。Flutter-Boost提供了共享FlutterView的實現方式。我們引入了Flutter-Boost,開發了Native側載體頁,設計了通用Flutter跳轉協議,結合58跳轉中心解決了Flutter混合開發的問題。在業務開發過程中,隨著開發的深入和業務邏輯的複雜,通過調研,使用了Fish-Redux進行了程式碼的重構,對複雜業務進行了細粒度的拆分,對邏輯和試圖進行隔離,優化了程式碼結構。

參考文獻

1、Flutter中文網,https://flutterchina.club/

2、深入理解Flutter引擎執行緒模式,https://mp.weixin.qq.com/s/hZ5PUvPpMlEYBAJggGnJsw

3、已開源|碼上用它開始Flutter混合開發——FlutterBoost,https://mp.weixin.qq.com/s/v-wwruadJntX1n-YuMPC7g

4、Fish-Redux介紹文件,https://github.com/alibaba/fish-redux/tree/master/doc

作者簡介

萬兵 :58同城房產技術部-Android開發工程師。主要負責58和安居客APP租房和商業地產業務的開發和維護工作。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 第六章 ZooKeeper在分散式環境中的應用