
長期以來,移動端的開發都需要為相同的產品邏輯實作兩套代碼,在大多數情況下,這兩套代碼所描述的邏輯基本是一致的,只是用不同的編程語言在闡述,為的是部署到不同的平臺上,這種模式會產生以下問題:
-
首先,顯而易見的是造成人力資源的浪費,大量可復用的邏輯沒有復用,本來可以釋放出來做深度探索的人力,浪費在重復的UI和基本邏輯的開發中,
-
UI上的不一致,在當前作業流中,設計圖往往只有一套,并不區分iOS和Android,而在某些時候,因為要兼顧到平臺特性和不同平臺的實作成本,這相同的UI設計在兩端展現就有可能不完全一致,
-
測驗壓力增大,測驗同學在測驗代碼的時候不僅僅要考慮兼容性,由于iOS和Android代碼由不同人,不同語言在不同平臺上實作,所以相同的邏輯在iOS上沒有問題,在Android上就可能出錯,所以經常看到測驗同學用兩種手機對比測驗,找出不同,從而消耗大量精力,
-
改進成本增大,首先,也是顯而易見的,兩倍人力,兩套代碼,壞味道以更快的速度產生;其次,也是更重要的,那些依賴于UI結構的工程改進,需要更大的成本來實施,就比如無埋點方案(也即全埋點方案),無埋點簡單來說就是將所有頁面的PV和Action進行過濾并上報,而在具體業務中不做侵入性編程,其中有一個點就是標記某頁面在整個UI樹狀結構中的唯一ID,當然iOS和Android由于不同的平臺結構,不同的代碼實作,其同一頁面所處的位置就有可能不同,統一編碼上報的無埋點方案就需要更多的實作成本,
基于但不僅限于以上問題,我們團隊開始了跨平臺的探索之旅,從基于Web的跨平臺方案,比如Cordova,PhoneGap,一直到React Native,這些方案讓我們看到了很多令人興奮的進步,但基本都存在一個共同的問題,就是在UI渲染的性能上無法媲美原生,直到Flutter的推出!
Flutter是Google推出的一款UI工具包,可以通過一套代碼同時在iOS和Android上構建媲美原生體驗的精美應用,它使用Dart作為開發語言,不依賴原生控制元件,而是將自有的控制元件庫,通過Skia圖形引擎直接繪制在平臺所提供的畫布上,簡單來說,它擁有以下特性:不依賴平臺、組件庫原生實作、能高速渲染復雜頁面、擁有統一的CodeBase,有點像App領域的Unity引擎,或者叫專注于2D渲染的UI引擎,
下面我們在簡單介紹跨平臺方案的演進歷史后,著重介紹我們團隊在Flutter工程實踐上的一些心得,以及遇到的問題和解決方案,
1.跨平臺方案的演進歷史
蘋果的iOS-SDK發布于2008年,谷歌的Android軟體開發工具包發布于2009年,這兩種工具包基于不同的編程語言進行開發,廠商的商業競爭導致無法使用一個CodeBase來開發移動應用,基于開篇所述的種種原因,業界開始了對跨平臺技術的探索,
1.1 基于WebView的實作方案,如Cordova
WebView是我們經常使用的,適應性極強,像Cordova這種方案也是基于WebView的封裝,但這種方案的缺點在于UI完全由Web技識訓制,具有局限性,并且在性能上也不如原生代碼,在這種方案中,JS為了獲取本地的服務資源,如呼叫攝像頭,GPS等,就需要通過JSBridge和本地代碼進行通信,但由于次數較少,通信導致的性能下降也沒有很明顯,
1.2 React Native
從2015年,React Native一直火到現在,很多大廠陸續跟進,這個方案的優點在于使用平臺所提供的原生組件進行繪制,在App和平臺之間建立完整的通信橋梁,其實JS代碼和原生代碼的執行速度是很快的,這個方案的問題在于Bridge,JS訪問原生的UI組件需要經過“橋接器”(圖上的Bridge),當大量UI控制元件高速重繪的時候,就有可能導致性能問題,

1.3 Flutter的設計思想
隨著跨平臺技術的不斷演進,Google提出了Flutter框架,
為了解決對原生控制元件的依賴,Flutter系統框架使用它自己的UI庫,截止2018.12.4發布的1.0-Release版本,Flutter團隊和社區用Dart語言寫了大概200萬行的代碼,而里面絕大多數都是UI組件,叫Widget,這些控制元件最終都會被編譯成對應平臺的機器代碼,使用Skia引擎繪制到原生提供的畫布上(圖中的Canvas),而Skia就是Android和Chrome的圖形引擎,已經在工程上進行過大量的實踐了,
這樣就導致UI的繪制程序不需要經過橋接器,而是直接繪制到畫布上,解決了RN存在的復雜頁面渲染性能下降問題,并提供Platform Channels來訪問平臺硬體,比如攝像頭,藍牙和定位等服務
2.flutter的移動端跨平臺應用實踐
Flutter有很多優秀的特性,并且這些特性都是為了提高開發者的效率而量身定做,但不可否認,和iOS或者Android相比,Flutter還只是一個Baby,有很多不成熟的地方;另外,在編程模式方面和原生端也有不小的差異,需要一定的學習成本,

2.1 UI組件使用上的不適
首先是在UI組件使用上的不適,當我們開始寫前端界面的時候,最最基本的一點是就是控制元件和布局,首先,Flutter中一切都是Widget,你可以理解為組件,我就不翻譯了,除了我們常見的Label,ImageView,TextView,List這類的UI控制元件,影片,手勢,布局Layout都是Widget,他們用一種神奇的child賦值方式嵌套起來,先看一段代碼:
Container(
constraints: BoxConstraints.expand(
height: Theme.of(context).textTheme.display1.fontSize * 1.1 + 200.0,
),
padding: const EdgeInsets.all(8.0),
color: Colors.teal.shade700,
alignment: Alignment.center,
foregroundDecoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('https://www.example.com/images/frame.png'),
centerSlice: Rect.fromLTRB(270.0, 180.0, 1360.0, 730.0),
),
),
transform: Matrix4.rotationZ(0.1),
child: Text('Hello World'),
)
Container就是一個布局控制元件,或者叫Layout Widget,其實就是一個可以設定寬高,顏色等等屬性的盒子,整個Flutter的布局都可以想象成盒子套盒子的形式,而嵌套的形式,不像我們在iOS中將某個控制元件通過addSubview的方式添加到父控制元件上,而是直接將子控制元件設定為父控制元件的child,
這就導致一個視覺上的問題,就是每一個child都要向后縮進,當一個個控制元件,布局嵌套起來的時候,處于UI樹底層的元素,在開始的時候可能就已經距開頭50個字符了,看上去很亂,也難以維護,
Widget build(BuildContext context) {
final Widget row = new GestureDetector(
behavior: HitTestBehavior.opaque, // 列舉值,等下改一個看看
child: new SafeArea(
top: false,
bottom: false,
child: new Padding(
padding: const EdgeInsets.only(
left: 16.0, right: 8.0, top: 8.0, bottom: 8.0),
child: new Row(
children: <Widget>[
new Expanded(
child: new Padding(
padding: const EdgeInsets...,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'Buy this cool color',
style: const TextStyle(
color: const Color(0xFF8E8E93),
),
),
],
),
),
),
],
),
),
),
);
}
就像上面的效果,看上去確實很不優雅,其實如果真的寫成這樣,就很明顯有壞代碼的味道,也提醒我們需要進行重構,另一方面,這種組織結構很好的反映了UI樹狀結構的本質,在一定程度上倒逼我們對通用控制元件做封裝,并讓函式保持功能單一,
目前支持Flutter編程的IDE,如Android Studio也添加了快捷功能,讓開發者方便的生成布局之類的模版代碼,也支持在已有的Widget外層添加布局,并自動縮進,
2.2 資料傳遞和工程組織架構
Widget有了之后,我們就需要考慮資料怎么和組件互動,以及資料怎么在組件之間傳遞的問題了,
我們在iOS或者Android端重繪一個UI控制元件的基本方法就是在從介面獲得資料之后,使用命令的方式,給對應組件賦值,比如更換title,顏色什么的,而Flutter從React中借鑒了大量的靈感,其中就包括回應式視圖,
回應式視圖中的一個重要概念就是虛擬DOM,虛擬DOM在Web視圖中代表HTML檔案物件模型,JavaScript用DOM提供的API來操作HTML檔案,而虛擬DOM使用JS來操作DOM的抽象版本,在回應視圖中,虛擬DOM是不可變的,在資料更新以后,會通過演算法比較,更新虛擬DOM樹,然后以最小的成本重新繪制界面,

React Native 也做類似的作業,但是是在移動應用程式當中進行的,它會操控移動平臺上的原生組件而不是DOM,它構建一個UI組件的虛擬樹,與原生組件進行比較,并只更新已發生更改的組件,
React Native必須通過橋接器與原生部件進行通信,因此,UI組件的虛擬樹機制,可以保證需要通過橋傳遞的資料最小,同時還允許使用原生組件,最后,一旦更新了本地組件,平臺就會將它們渲染到畫布上,

Flutter中,也很相似,只不過虛擬DOM現在是真實的控制元件樹,

下面是一個最基本的示例,來闡述回應式編程的基本思路,
counter作為存盤點擊次數的變數,只有在setState中改變,按鈕系結incrementCounte方法,UI控制元件Text系結counter變數,這樣每次點擊一下按鈕,都呼叫一次setState,從而使counter值加一,但是并不需要用命令式的方法手動重繪UI控制元件,setState后整個build方法會被重新執行,從而導致Text控制元件上的顯示加一,
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
這種思路有一個大好處,就是資料系結UI,只要系結正確,React機制正確,資料的變換就是UI的變換,或者說資料就是UI的抽象,這樣在測驗的時候,我們只要測驗資料的正確性,就能在更大程度上保證UI界面的正確,更大程度上減少集成測驗的壓力,提升單元測驗的作用;另外也可以盡量避免在撰寫或者重構代碼的程序中,因為缺少了某行重繪命令而導致的UI沒有正常初始化或者更新,
Flutter自有的回應式架構已經非常系統了,但其中有一個點需要注意,
當我們需要在組件之間傳遞資料的時候,React設計的結構需要我們先將資料傳遞到父節點上,再由目標節點從父節點取這個資料,簡單來說,就比如在UI樹上,F節點有A,B兩個子節點,那A要把一個值傳給B上顯示,就需要先傳遞給F,再由B系結F上的這個資料來獲得更新,這種模式叫做Lifting State Up,就是舉高高,這種模式簡單輕量,但是隨著應用復雜度的提高,我們常常需要處理很多來源的資料,比如從各種介面讀取的,從本地資料庫讀取的等等,場景諸如我們遇到的主頁多tab,feed流拼接等;另外我們有時還需要保存很多全域狀態,在這些狀態改變之后,很多頁面都需要重繪,比如是否login,主題顏色更改甚至是國際化變更語言,
為了處理復雜多頁面資料管理的問題,我們找到了React的伙伴,Redux,Redux已經比較成熟了,資料眾多,也是我們選擇它的原因之一,Redeux有三個核心設計原則:
Single source of truth:單一資料源,整個app的state都以object tree的形式存在一個store里,就像一個大資料中心,可以在app中的任意位置訪問資料,系結監聽,這樣在傳統編程環境中很難實作的功能,比如撤銷/重做這種就很容易實作,
State is read-only:第二個設計原則為,state是只讀的,也就是說state不可以直接修改,所有修改行為必須通過發送Action,因為所有State的改變都是集中且按照嚴格順序發生的,所以也沒有競態條件,并且Action就是普通的Dart類,所以他們可以被記錄,被序列化,被存盤起來,這就使得我們可以在崩潰之前把程式最后的狀態報告上來,方便除錯,甚至直接復現一下,
Changes are made with pure functions:上面提到的Action只是定義一個行為,Reducer才是執行者,第三個設計原則就是,Reducer是純函式,所謂純函式就是沒有副作用(Side Effect),只進行計算或者呼叫其他純函式,他接收一個Action和一個舊的State,然后回傳新的State,Reducer可以根據專案的大小分層級,App啟動的時候添加一個Reducer,之后可以拆分成很多小的Reducer來管理State樹的各個模塊,
通過Flutter+Redux我們解決了資料在頁面流轉重繪的問題,好多作者用一整本書來講Redux,我就不再贅述,歡迎小伙伴指正交流,
2.3 無反射的Json序列化
我自己是iOS開發,在iOS開發中我們使用反射機制來進行Json決議,而在Flutter中是不行的,
Flutter的Json決議使用dart:convert中內置的解碼器,只要傳入JSON的原始字串給JSON.decode()方法,然后從回傳的Map<String, dynamic>中取你要的資料就行了,當需要用Map生成Model物件的時候,需要在Model中添加fromJson方法,通過呼叫方法實作來逐一取出Map中的值填充到物件中,
class FeedModel {
List imglist;
int commentnum;
FeedModel(this.imglist, this.commentnum)
FeedModel.fromJson(Map<String, dynamic> json)
: imglist = json['imglist'],
commentnum = json['commentnum'];
}
在大型工程中,這種方式就略顯笨拙,Flutter有一個工具來批量生成fromJson方法,需要我們使用一個叫json_annotation的庫,并在yaml檔案中引入以下配置:
dependencies:
flutter:
sdk: flutter
json_annotation: ^2.0.0
...
dev_dependencies:
...
build_runner: ^1.1.2
json_serializable: ^2.0.0
...
json_annotation可以讓你在Model類中進行標記,有點像JavaSpring,build_runner和json_serializable是兩個“dev_dependencies”,就是說在正式發布的時候并不包含這兩個庫,他們只是用來輔助開發的工具,在引入json_annotation.dart之后,我們就可以改造一下Model類了
// feed_model.dart
@JsonSerializable()
class FeedModel {
List imglist;
int commentnum;
FeedModel(this.imglist, this.commentnum)
factory FeedModel.fromJson(Map<String, dynamic> json) =>
_$FeedModelFromJson(json);
}
在類的開頭添加JsonSerializable標記,如果json中欄位名字和類的屬性名稱不一致,還需要在屬性前添加@JsonKey(name: 'name_in_json')標記,然后在控制臺運行flutter packages pub run build_runner build命令,就會生成一個feed_model.g.dart的檔案
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FeedModel _$FeedModelFromJson(Map<String, dynamic> json) {
return FeedModel(
json['imglist'] as List,
json['commentnum'] as int,
}
這樣就自動生成了一部分模版代碼,雖然這非常方便,但如果我們每次在模型類中進行更改時都不需要手動地運行構建,那就更好了,為了持續地生成代碼,我們需要用到watcher工具,它會監控我們專案檔案的改變并在需要的時候自動編譯那些必要的檔案,我們可以在專案根目錄下運行flutter packages pub run build_runner watch來啟動watcher,這樣在我們更改了Model類之后,他就能自動生成匹配的.g.dart檔案了,
但也僅僅如此了,為什么這么說呢,因為我們在flutter中無法使用GSON/Jackson之類的庫,這些庫需要用到運行時的反射機制,而這在Flutter中是被禁用的,
為什么反射被禁用呢?FlutterUI工具包會被打包在每一個App中,也就是說,開發者只寫一個HelloWorld程式,Flutter也需要在包中添加很多相應的支持庫,這就導致Flutter對優化應用大小格外關注,為了優化應用大小,以及在發布版本中“擺脫”一些無用的代碼,Dart支持tree shaking特性,Tree Shaking顧名思義就是搖樹,就是把樹上一些沒用的東西搖掉,特別形象,Flutter的結構本質上就是一棵Widget樹,自main方法以下,引入了很多檔案,依賴了很多模塊,但在實際情況中,雖然依賴了某個模塊,但其實只使用其中的部分功能,通過tree shaking搖掉沒用的模塊功能,來洗掉無用代碼,而運行時反射干涉了tree shaking,因為反射默認隱式地呼叫所有代碼,這讓tree shaking變得困難,工具無法知道哪些部分在運行時未使用,以至于多余的代碼很難被清理掉,所以在使用反射時,應用大小不容易被優化,這個特性就被禁用了,
2.4 視頻外接紋理
視頻外接紋理是阿里巴巴閑魚團隊遇到的一個非常有價值的問題,在我們處理視頻或者影像識別的問題時也很容易遇到,
Flutter Engine和Native之間通過Platform Channel機制進行通信,正如所有橋接機制一樣,在Flutter側需要一些Native側高記憶體占用影像的時候,(比如攝像頭幀,視頻幀),用于影像等資料的傳輸必然引起記憶體和CPU的巨大消耗,為此,Flutter提供了一個特殊的Widget,Texture,
首先,紋理Texture在物理上指的是GPU顯存中一段連續的空間,可以理解為GPU內代表影像資料的一個物件,他有一些屬性,比如高度、寬度、色彩通道數量,其次,Flutter中Texture Widget就是一個可以由Native平臺渲染并填充紋理的一塊矩形區域,而填充紋理的方式,就是使用一個共享的PixelBuffer,Native端將攝像頭獲取的資料(或者視頻幀之類的),直接寫入到PixelBuffer中,Flutter通過copyPixelBuffer方法拿到PixelBuffer以后轉成OpenGLES Texture,交由Skia繪制,
但是在實際工程中,很多情況在Native側需要通過GPU對視頻影像資料進行處理,比如美顏,也就是說在進入PixelBuffer之前已經生成GPU紋理Texture了,然后又要通過CPU計算加入到PixelBuffer中,之后再通過Flutter引擎呼叫,轉化為Texture,GPU->CPU->GPU這就造成了浪費,并且CPU和GPU的記憶體交換是所有操作里面最耗時的,

為了讓Flutter側直接讀取到Native側的Texture,閑魚團隊做了一個ShareGroup的露出,
在說ShareGroup之前,需要簡單說一下Flutter的執行緒機制,通常情況下,Flutter創建4個Runner,UI Runner、GPU Runner、IO Runner和Platform Runner,一個Runner對應一個執行緒,Platform Runner跑在主執行緒上,使用OpenGL的app在執行緒設計上都會有一個執行緒負責加載資源,一個執行緒負責渲染,但是為了能讓負責加載的執行緒創建出的Texture,給負責渲染的執行緒使用,兩個執行緒會共用一個EAGLContext,正如之前介紹Dart設計原理的時候提到的一樣,共享資源是不安全的,而加鎖的話不僅影響速度還會有死鎖饑餓等問題,因此Flutter在EAGLContext的使用上使用了另一種機制:兩個執行緒各自使用自己的EAGLContext,彼此通過ShareGroup(android為shareContext)來共享紋理資料,
而Native側在使用OpenGL的模塊時也會在自己執行緒下創建EAGLContext,閑魚團隊就是讓ShareGroup露出給Native側,然后在Native側保存這個ShareGroup,當Native創建Context時,就使用這個ShareGroup進行創建,這樣,利用Flutter原有的執行緒共享資源機制,打通了和Native執行緒共享資源的能力,
[注]An EAGLContext object manages an OpenGL ES rendering context—the state information, commands, and resources needed to draw using OpenGL ES. EAGLContext物件管理OpenGL ES渲染所需的背景關系環境,包括OpenGL ES繪制所需的狀態資訊,命令和資源,
3.Flutter未來的計劃
2018年12月初,Flutter發布1.0版本,Github上也增加了Stable通道,1.0版本提供了一些全新的iOS風格Widget,優化了很多Git上提出的Issue,另外接入了近20種Firebase服務(目前國內還不能用,但據說在和騰訊磋商),同時優化了性能,減少了包體積,除此之外Dart也更新到了2.1,提供更快的型別檢查以及錯誤提示,
為了更好的使用Flutter框架,專案組也計劃在2019年二月提供更好的工具鏈,以方便開發者將Flutter引入到現有的工程中,因為畢竟不是每個團隊都有機會從頭使用Flutter構建專案的,
上面提到的Add to App功能非常適合于逐漸引入Flutter到現有應用中,但有時候我們反倒需要將Android或iPhone平臺的控制元件嵌入到Flutter應用當中,所以Flutter團隊引入了AndroidView和UiKitView這兩個平臺級視圖的widget到Flutter中,這樣就可以將它們分別嵌入到指定的平臺,之前我們使用一個原生頁面,比如百度地圖,就必須彈出一個純原生頁,上面添加UI比如按鈕文字什么的,都需要iOS和Android分別用原生實作,有了平臺級視圖Widget,我們就可以把兩端分別實作的控制元件包裝起來(如百度地圖,因為百度地圖短時間不會提供Flutter插件,所以還是要兩端分別接入SDK),然后在Flutter中以Widget的形式插入到視圖中,然后在上面添加其他的Widget,Flutter的文字按鈕之類的,以達到多端共用一套代碼的目的,
除了移動端,Flutter還有一個處于實驗中的內部專案-Hummingbird,他是一個基于Web實作的Flutter運行時環境,它利用了Dart語言能被編譯成JavaScript的特性,這個專案讓Flutter應用程式能夠無需改動地運行在標準Web平臺,除了Web端,Google還在開發一個新的作業系統,叫作Fuchsia,和基于Linux內核的Chrome OS以及Android不同,Fuchsia基于一個叫作Zircon的微內核,設計目標之一是可運行在眾多的設備上,包括移動電話和個人計算機,而Fuchsia的用戶界面和應用程式,都是用Flutter撰寫的,
總而言之,Flutter作為Google推出的UI引擎框架,專注但不限于移動端,在提高作業效率,創建精美的UI界面上,不斷探索前進,而對于我們程式員說,可以嘗試新的開發模式,接觸新的設計思想,在Git上和全球開發者討論問題,研讀原始碼,也是很有意思的事,不是么?
作者|尉野
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/Cross-platform-application-practice-of-mobile-terminal-based-on-Flutter.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/543156.html
標籤:其他
