<簡書 — 劉小壯> https://www.jianshu.com/p/d27c1f5ee3ffshell
在進行iOS
和Flutter
的混編時,iOS
比Android
的接入方式略複雜,但也還好。如今市面上有很多接入Flutter
的方案,但大多數都是千篇一概相互抄的,沒什麼意義。xcode
進行Flutter
混編以前,有一些必要的文件。微信
xcode_backend.sh
文件,在配置flutter
環境的時候由Flutter
工具包提供。xcconfig
環境變量文件,在Flutter
工程中自動生成,每一個工程都不同。xcconfig
是Xcode
的配置文件,Flutter
在裏面配置了一些基本信息和路徑,接入Flutter
前須要先將xcconfig
接入進來,不然一些路徑等信息將會出錯或找不到。網絡
Flutter
的xcconfig
包含三個文件,Debug.xcconfig
、Release.xcconfig
、Generated.xcconfig
,須要將這些文件配置在下面的位置,而且按照不一樣環境配置不一樣的文件。app
Project -> Info -> Development Target -> Configurations
有些比較大的工程中已經在Configurations
中設置了xcconfig
文件,因爲每一個Target
的一種環境只能配置一個xcconfig
文件,因此能夠在已有的xcconfig
文件中import
引入Generated.xcconfig
文件,而且不須要區分環境。less
xcode_backend.sh
腳本文件用來構建和導出Flutter
產物,這是Flutter
開發包爲咱們默認提供的。須要在工程Target
的Build Phases
加入一個Run Script
文件,並將下面的腳本代碼粘貼進去。須要注意的是,不要忘記前面的/bin/sh
操做,不然會致使權限錯誤。async
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
在xcode_backend.sh
中有三個參數類型,build
、thin
、embed
,thin
沒有太大意義,其餘兩個則負責構建和導出。ide
隨後能夠對Xcode
工程進行編譯,這時候確定會報錯的。可是不要慌張,報錯後咱們在工程主目錄下會發現一個名爲Flutter
的文件夾,其中會包含兩個framework
,這個文件夾就是Flutter
的編譯產物,咱們將這個文件夾總體拖入項目中便可。函數
這時候就能夠在iOS
工程中添加Flutter
代碼了,下面是詳細步驟。工具
AppDelegate
的集成改成FlutterAppDelegate
,而且須要遵循FlutterAppLifeCycleProvider
代理。#import <Flutter/Flutter.h> #import <UIKit/UIKit.h> @interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider> @end
FlutterPluginAppLifeCycleDelegate
的實例對象,這個對象負責管理Flutter
的生命週期,並從Platform
側接收AppDelegate
的事件。我直接將其聲明爲一個屬性,在AppDelegate
中的各個方法中,調用其方法進行中轉操做。- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { [self.lifeCycleDelegate applicationWillResignActive:application]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { [self.lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; return YES; }
Flutter
代碼,加入的方式也很簡單,直接實例化一個FlutterViewController
控制器便可,也不須要傳其餘參數進去(這裏先不考慮多實例的問題)。FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
Flutter
將其看作是一個畫布,實例化一個畫布上去以後,任何操做其實都是在當前頁面完成的。
到這個步驟集成操做就已經完成,可是不少人在集成過程當中會遇到一些錯誤,下面是一些常見錯誤。
xcode_backend.sh
文件等。這是由於環境變量FLUTTER_ROOT
沒有獲取到,FLUTTER_ROOT
配置在Generated.xcconfig
中,能夠看一下這個文件是否是配置的有問題。lipo info *** arm64
相似這樣的錯誤,通常都是由於xcode_backend.sh
腳本致使的,能夠檢查一下FLUTTER_ROOT
環境變量是否正確。Build Phases
的腳本寫的是否是有問題。***/flutter_tools/bin/xcode_backend.sh: Permission denied
在進行混編過程當中,Flutter
有一個很大的優點,就是若是Flutter
代碼出問題,不會致使原生應用的崩潰。當Flutter
代碼出現崩潰時,會在屏幕上顯示錯誤信息。
在開發過程當中常常會涉及到網絡請求和持久化的問題,若是混編的話可能會涉及到寫兩套邏輯。例如網絡請求有一些公共參數,或返回數據的統一處理等,若是維護兩套邏輯的話會容易出問題。因此建議將網絡請求和持久化操做都交給Platform
處理,Flutter
側只負責向Platform
請求並拿來使用便可。
這個過程就涉及到兩端數據交互的問題,Flutter
對於混編給出了兩套方案,MethodChannel
和EventChannel
。從名字上來看,一個是方法調用,另外一個是事件傳遞。但實際開發過程當中,只須要使用MethodChannel
便可完成全部需求。
下面是Flutter
調用Native
的代碼,在Native
中經過FlutterMethodChannel
設置指定的回調代碼,而且在接收參數並處理。由Flutter
經過MethodChannel
對Native
發起調用,並傳入對應的參數。
代碼中在Flutter
側構建好數據模型,而後調用MethodChannel
的invokeMethod
,會觸發Native
的回調。Native
拿到Flutter
傳過來的數據,進行解析並執行播放操做,隨後會把播放的狀態碼回調給Flutter
側,交互完成。
import 'package:flutter/services.dart'; Future<Null> playVideo() async{ var methodChannel = MethodChannel('flutterChannelName'); Map params = {'playID' : '302998298', 'duration' : '2520', 'name' : '三生三世十里桃花'}; String result; result = await methodChannel.invokeMethod('PlayAlbumVideo', params); String playID = params['playID']; String duration = params['duration']; String name = params['name']; showCupertinoDialog(context: context, builder: (BuildContext context){ return CupertinoAlertDialog( title: Text(result), content: Text('name:$name playID:$playID duration:$duration'), actions: <Widget>[ FlatButton( child: Text('肯定'), onPressed: (){ Navigator.pop(context); }, ) ], ); }); }
NSString *channelName = @"flutterChannelName"; FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC]; [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { if ([call.method isEqualToString:@"PlayAlbumVideo"]) { NSDictionary *params = call.arguments; VideoPlayerModel *model = [[VideoPlayerModel alloc] init]; model.playID = [params stringForKey:@"playID"]; model.duration = [params stringForKey:@"duration"]; model.name = [params stringForKey:@"name"]; NSString *playStatus = [SVHistoryPlayUtil playVideoWithModel:model showPlayerVC:self.flutterVC]; result([NSString stringWithFormat:@"播放狀態 %@", playStatus]); } }];
Native
調用Flutter
的代碼和Flutter
調用Native
的基本相似,只是調用和設置回調的角色不一樣。一樣的,Flutter
因爲要接收Native
的消息回調,因此須要註冊一個回調,由Native
發起對Flutter
的調用並傳入參數。
Native
和Flutter
的相互調用都須要設置一個名字,每個名字對應一個MethodChannel
對象,每個對象能夠發起屢次調用,不一樣調用以invokeMethod
作區分。
import 'package:flutter/services.dart'; @override void initState() { super.initState(); MethodChannel methodChannel = MethodChannel('nativeChannelName'); methodChannel.setMethodCallHandler(callbackHandler); } Future<dynamic> callbackHandler(MethodCall call) { if(call.method == 'requestHomeData') { String title = call.arguments['title']; String content = call.arguments['content']; showCupertinoDialog(context: context, builder: (BuildContext context){ return CupertinoAlertDialog( title: Text(title), content: Text(content), actions: <Widget>[ FlatButton( child: Text('肯定'), onPressed: (){ Navigator.pop(context); }, ) ], ); }); } }
NSString *channelName = @"nativeChannelName"; FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC]; [RequestManager requestWithURL:url success:^(NSDictionary *result) { [methodChannel invokeMethod:@"requestHomeData" arguments:result]; }];
在iOS
和Android
開發中,各自的編譯器都提供了很好的調試工具集,方便進行內存、性能、視圖等調試。Flutter
也提供了調試工具和命令,下面基於VSCode
編譯器來說一下Flutter
調試,相對而言Android Studio
提供的調試功能可能會更多一些。
VSCode
支持一些簡單的命令行調試指令,在程序運行過程當中,在Command Palette
命令行面板中輸入performance
,並選擇Toggle Performance Overlay
命令便可。此命令有一個要求就是須要App在運行狀態。
隨後會在界面上出現一個性能面板,這個頁面分爲兩部分,GPU線程和UI線程的幀率。每一個部分分爲三個橫線,表明着不一樣的卡頓層級。若是是綠色則表示不會影響界面渲染,若是是紅色則有可能會影響界面的流暢性。若是出現紅色線條,則表示當前執行的代碼須要優化。
VSCode
爲Flutter
提供了一套調試工具集-Dart DevTools
,這套工具集功能很是全,包含性能、UI、熱更新、熱重載、log日誌等不少功能。
安裝Dart DevTools
後,在App運行狀態下,能夠在VSCode
的右下角啓動這個工具,工具會以網頁的形式展示,而且能夠控制App。
下面是Dart DevTools
的主界面,我運行的是一個界面相似於微信的App。從Inspector
中能夠看到頁面的視圖結構,Android Studio
也有相似的功能。頁面總體是一個樹形結構,而且選中某一個控件後,會在右側展現出控件的變量值,例如frame
、color
等,這個功能很是實用。
我運行的設備是Xcode
模擬器,若是想切換Android
的Material Design
,點擊上面的iOS
按鈕便可直接切換設備。剛纔上面說到的查看內存的性能面板,點擊iOS
按鈕旁邊的Performance Overlay
便可出現。
若是想知道在Dart DevTools
中選擇的節點,具體對應哪一個控件,能夠選擇Select Widget Mode
使屏幕上被選中的控件高亮。
點擊Debug Paint
可讓每一個控件都高亮,經過這個模式能夠看到ListView
的滑動方向,以及每一個控件的大小及控件之間的距離。
除此以外,還能夠選擇Paint Baseline
使全部控件的底線高亮,功能和Debug Paint
相似,不作敘述。
Dart DevTools
中提供的內存調試工具更加直觀,能夠實時顯示內存使用狀況。在剛開始運行時,咱們發現一個內存峯值,把鼠標放上去能夠看到具體的內存使用狀況。內存會有具體分類,Used
、GC
等。
Dart DevTools
的內存工具仍是不夠完美,Xcode
能夠選擇某段內存,看到這塊內存中涉及到主要堆棧調用,而且點擊調用棧能夠跳轉到Xcode
對應的代碼中,而Dart DevTools
還不具有這個功能,可能和Web
的展現形式有關係。
內存管理Flutter
使用的是GC
,回收速度可能不是很快,iOS
中的ARC
則是基於引用計數當即回收的。還有不少其餘的功能,這裏就不一一詳細敘述了,各位同窗能夠本身探索。
項目中是經過實例化FlutterViewController
控制器來顯示Flutter
界面的,整個Flutter
頁面能夠理解爲一個畫布,經過頁面不斷的變化,改變畫布上的東西。因此,在單實例的狀況下,Flutter
頁面中間不能插入原生頁面。
這時候若是咱們想在多個地方展現Flutter
頁面,而這些頁面並非Flutter -> Flutter
的連貫跳轉形式,那怎麼來實現這個場景呢?Google
的建議是建立Flutter
的多實例,並經過傳入不一樣的參數實例化不一樣的頁面。但這樣會形成很嚴重的內存問題,因此並不能這麼作。
若是不能真正建立多個實例對象,那就須要經過其餘方式來實現多實例。Flutter
頁面顯示其實並非跟着FlutterVC
走的,而是跟着FlutterEngine
走的。因此在建立一次FlutterVC
以後,就將FlutterEngine
保存下來,在其餘位置建立FlutterVC
時直接經過FlutterEngine
的方式建立,而且在建立後進行跳轉操做。
在進行頁面切換時,經過channelMethod
調用Flutter
側的路由切換代碼,並將切換後的新頁面FlutterVC
添加到Native
上。這種實現方式,就是經過Flutter
的Router
的方式實現的,下面將會介紹Router
的兩種表現形式,靜態路由和動態路由。
靜態路由是MaterialApp
提供的一個API
,routes
本質上是一個Map
對象,其組成結構是key
是調用頁面的惟一標識符,value
就是對應頁面的Widget
。
在定義靜態路由時,能夠在建立Widget
時傳入參數,例如實例化ContactWidget
時就能夠傳入對應的參數過去。
void main() { runApp( MaterialApp( home: Page2(), routes: { 'page1': (_) => Page1(), 'page2': (_) => Page2() }, ), ); } class Page1 extends StatelessWidget { @override Widget build(BuildContext context) { return ContactWidget(); } } class Page2 extends StatelessWidget { @override Widget build(BuildContext context) { return HomeScreen(); } }
進行頁面跳轉時,經過Navigator
進行調用,每次調用都會從新建立對應的Widget
。進行調用時pushNamed
函數會傳入一個參數,這個參數就是定義Map
時對應頁面的key
。
Navigator.of(context).pushNamed('page1');
靜態路由的方式並非很靈活,相對而言動態路由更加靈活。動態路由不須要預先設定routes
,直接調用便可。和普通push
不一樣的是,動態路由在push
時經過PageRouteBuilder
來構建push
對象,在Builder
的構建方法中執行對應的頁面跳轉操做便可。
結合以前說的channelMethod
,就是在channelMethod
對應的Callback
回調中,執行Navigator
的push
函數,接收Native
傳遞過來的參數並構建對應的Widget
頁面,將Widget
返回給Builder
便可完成頁面跳轉操做。因此說動態路由的方式很是靈活。
不管是經過靜態路由仍是動態路由的方式建立,均可以經過then
函數接收新頁面返回時的返回值。
Navigator.of(context).push(PageRouteBuilder( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return ContactWidget('next page value'); } transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { return FadeTransition( child: child, opacity: animation, ); } )).then((onValue){ print('pop的返回值 $onValue'); });
但動態路由的跳轉方式也有一些問題,會致使動畫失效。因此須要重寫Builder
的transitionsBuilder
函數,來自定義轉場動畫。
不管是經過靜態路由仍是動態路由的方式建立,都會存在一些問題。因爲每次都是新建立Widget
,因此在建立時會有黑屏的問題。並且每次建立的話,都會丟失當前頁面上次的上下文狀態,每次進來都是一個新頁面。