該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請(qǐng)注明:劉小壯

iOS接入Flutter
在進(jìn)行iOS和Flutter的混編時(shí),iOS比Android的接入方式略復(fù)雜,但也還好?,F(xiàn)在市面上有不少接入Flutter的方案,但大多數(shù)都是千篇一律相互抄的,沒什么意義。
進(jìn)行Flutter混編之前,有一些必要的文件。
-
xcode_backend.sh文件,在配置flutter環(huán)境的時(shí)候由Flutter工具包提供。 -
xcconfig環(huán)境變量文件,在Flutter工程中自動(dòng)生成,每個(gè)工程都不一樣。
xcconfig文件
xcconfig是Xcode的配置文件,Flutter在里面配置了一些基本信息和路徑,接入Flutter前需要先將xcconfig接入進(jìn)來,否則一些路徑等信息將會(huì)出錯(cuò)或找不到。
Flutter的xcconfig包含三個(gè)文件,Debug.xcconfig、Release.xcconfig、Generated.xcconfig,需要將這些文件配置在下面的位置,并且按照不同環(huán)境配置不同的文件。
Project -> Info -> Development Target -> Configurations

有些比較大的工程中已經(jīng)在Configurations中設(shè)置了xcconfig文件,由于每個(gè)Target的一種環(huán)境只能配置一個(gè)xcconfig文件,所以可以在已有的xcconfig文件中import引入Generated.xcconfig文件,并且不需要區(qū)分環(huán)境。
腳本文件
xcode_backend.sh腳本文件用來構(gòu)建和導(dǎo)出Flutter產(chǎn)物,這是Flutter開發(fā)包為我們默認(rèn)提供的。需要在工程Target的Build Phases加入一個(gè)Run Script文件,并將下面的腳本代碼粘貼進(jìn)去。需要注意的是,不要忘記前面的/bin/sh操作,否則會(huì)導(dǎo)致權(quán)限錯(cuò)誤。
/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中有三個(gè)參數(shù)類型,build、thin、embed,thin沒有太大意義,其他兩個(gè)則負(fù)責(zé)構(gòu)建和導(dǎo)出。
混合開發(fā)
隨后可以對(duì)Xcode工程進(jìn)行編譯,這時(shí)候肯定會(huì)報(bào)錯(cuò)的。但是不要慌張,報(bào)錯(cuò)后我們?cè)诠こ讨髂夸浵聲?huì)發(fā)現(xiàn)一個(gè)名為Flutter的文件夾,其中會(huì)包含兩個(gè)framework,這個(gè)文件夾就是Flutter的編譯產(chǎn)物,我們將這個(gè)文件夾整體拖入項(xiàng)目中即可。
這時(shí)候就可以在iOS工程中添加Flutter代碼了,下面是詳細(xì)步驟。
- 將
AppDelegate的集成改為FlutterAppDelegate,并且需要遵循FlutterAppLifeCycleProvider代理。
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>
@end
- 創(chuàng)建一個(gè)
FlutterPluginAppLifeCycleDelegate的實(shí)例對(duì)象,這個(gè)對(duì)象負(fù)責(zé)管理Flutter的生命周期,并從Platform側(cè)接收AppDelegate的事件。我直接將其聲明為一個(gè)屬性,在AppDelegate中的各個(gè)方法中,調(diào)用其方法進(jìn)行中轉(zhuǎn)操作。
- (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代碼,加入的方式也很簡單,直接實(shí)例化一個(gè)FlutterViewController控制器即可,也不需要傳其他參數(shù)進(jìn)去(這里先不考慮多實(shí)例的問題)。
FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
Flutter將其看做是一個(gè)畫布,實(shí)例化一個(gè)畫布上去之后,任何操作其實(shí)都是在當(dāng)前頁面完成的。
常見錯(cuò)誤
到這個(gè)步驟集成操作就已經(jīng)完成,但是很多人在集成過程中會(huì)遇到一些錯(cuò)誤,下面是一些常見錯(cuò)誤。
- 路徑錯(cuò)誤,讀取不到
xcode_backend.sh文件等。這是因?yàn)榄h(huán)境變量FLUTTER_ROOT沒有獲取到,FLUTTER_ROOT配置在Generated.xcconfig中,可以看一下這個(gè)文件是不是配置的有問題。 -
lipo info *** arm64類似這樣的錯(cuò)誤,一般都是因?yàn)?code>xcode_backend.sh腳本導(dǎo)致的,可以檢查一下FLUTTER_ROOT環(huán)境變量是否正確。 - 下面這種問題一般都是因?yàn)闄?quán)限導(dǎo)致的,可以查看
Build Phases的腳本寫的是不是有問題。
***/flutter_tools/bin/xcode_backend.sh: Permission denied
混合開發(fā)
在進(jìn)行混編過程中,Flutter有一個(gè)很大的優(yōu)勢(shì),就是如果Flutter代碼出問題,不會(huì)導(dǎo)致原生應(yīng)用的崩潰。當(dāng)Flutter代碼出現(xiàn)崩潰時(shí),會(huì)在屏幕上顯示錯(cuò)誤信息。
在開發(fā)過程中經(jīng)常會(huì)涉及到網(wǎng)絡(luò)請(qǐng)求和持久化的問題,如果混編的話可能會(huì)涉及到寫兩套邏輯。例如網(wǎng)絡(luò)請(qǐng)求有一些公共參數(shù),或返回?cái)?shù)據(jù)的統(tǒng)一處理等,如果維護(hù)兩套邏輯的話會(huì)容易出問題。所以建議將網(wǎng)絡(luò)請(qǐng)求和持久化操作都交給Platform處理,Flutter側(cè)只負(fù)責(zé)向Platform請(qǐng)求并拿來使用即可。
這個(gè)過程就涉及到兩端數(shù)據(jù)交互的問題,Flutter對(duì)于混編給出了兩套方案,MethodChannel和EventChannel。從名字上來看,一個(gè)是方法調(diào)用,另一個(gè)是事件傳遞。但實(shí)際開發(fā)過程中,只需要使用MethodChannel即可完成所有需求。
Flutter to Native
下面是Flutter調(diào)用Native的代碼,在Native中通過FlutterMethodChannel設(shè)置指定的回調(diào)代碼,并且在接收參數(shù)并處理。由Flutter通過MethodChannel對(duì)Native發(fā)起調(diào)用,并傳入對(duì)應(yīng)的參數(shù)。
代碼中在Flutter側(cè)構(gòu)建好數(shù)據(jù)模型,然后調(diào)用MethodChannel的invokeMethod,會(huì)觸發(fā)Native的回調(diào)。Native拿到Flutter傳過來的數(shù)據(jù),進(jìn)行解析并執(zhí)行播放操作,隨后會(huì)把播放的狀態(tài)碼回調(diào)給Flutter側(cè),交互完成。
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:@"播放狀態(tài) %@", playStatus]);
}
}];
Native to Flutter
Native調(diào)用Flutter的代碼和Flutter調(diào)用Native的基本類似,只是調(diào)用和設(shè)置回調(diào)的角色不同。同樣的,Flutter由于要接收Native的消息回調(diào),所以需要注冊(cè)一個(gè)回調(diào),由Native發(fā)起對(duì)Flutter的調(diào)用并傳入?yún)?shù)。
Native和Flutter的相互調(diào)用都需要設(shè)置一個(gè)名字,每一個(gè)名字對(duì)應(yīng)一個(gè)MethodChannel對(duì)象,每一個(gè)對(duì)象可以發(fā)起多次調(diào)用,不同調(diào)用以invokeMethod做區(qū)分。
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];
}];
調(diào)試工具集
在iOS和Android開發(fā)中,各自的編譯器都提供了很好的調(diào)試工具集,方便進(jìn)行內(nèi)存、性能、視圖等調(diào)試。Flutter也提供了調(diào)試工具和命令,下面基于VSCode編譯器來講一下Flutter調(diào)試,相對(duì)而言Android Studio提供的調(diào)試功能可能會(huì)更多一些。
性能調(diào)試
VSCode支持一些簡單的命令行調(diào)試指令,在程序運(yùn)行過程中,在Command Palette命令行面板中輸入performance,并選擇Toggle Performance Overlay命令即可。此命令有一個(gè)要求就是需要App在運(yùn)行狀態(tài)。

隨后會(huì)在界面上出現(xiàn)一個(gè)性能面板,這個(gè)頁面分為兩部分,GPU線程和UI線程的幀率。每個(gè)部分分為三個(gè)橫線,代表著不同的卡頓層級(jí)。如果是綠色則表示不會(huì)影響界面渲染,如果是紅色則有可能會(huì)影響界面的流暢性。如果出現(xiàn)紅色線條,則表示當(dāng)前執(zhí)行的代碼需要優(yōu)化。
Dart DevTools
VSCode為Flutter提供了一套調(diào)試工具集-Dart DevTools,這套工具集功能非常全,包含性能、UI、熱更新、熱重載、log日志等很多功能。
安裝Dart DevTools后,在App運(yùn)行狀態(tài)下,可以在VSCode的右下角啟動(dòng)這個(gè)工具,工具會(huì)以網(wǎng)頁的形式展現(xiàn),并且可以控制App。
主界面
下面是Dart DevTools的主界面,我運(yùn)行的是一個(gè)界面類似于微信的App。從Inspector中可以看到頁面的視圖結(jié)構(gòu),Android Studio也有類似的功能。頁面整體是一個(gè)樹形結(jié)構(gòu),并且選中某一個(gè)控件后,會(huì)在右側(cè)展示出控件的變量值,例如frame、color等,這個(gè)功能非常實(shí)用。

我運(yùn)行的設(shè)備是Xcode模擬器,如果想切換Android的Material Design,點(diǎn)擊上面的iOS按鈕即可直接切換設(shè)備。剛才上面說到的查看內(nèi)存的性能面板,點(diǎn)擊iOS按鈕旁邊的Performance Overlay即可出現(xiàn)。
Select Widget
如果想知道在Dart DevTools中選擇的節(jié)點(diǎn),具體對(duì)應(yīng)哪個(gè)控件,可以選擇Select Widget Mode使屏幕上被選中的控件高亮。

Debug Paint
點(diǎn)擊Debug Paint可以讓每個(gè)控件都高亮,通過這個(gè)模式可以看到ListView的滑動(dòng)方向,以及每個(gè)控件的大小及控件之間的距離。

除此之外,還可以選擇Paint Baseline使所有控件的底線高亮,功能和Debug Paint類似,不做敘述。
Memory
Dart DevTools中提供的內(nèi)存調(diào)試工具更加直觀,可以實(shí)時(shí)顯示內(nèi)存使用情況。在剛開始運(yùn)行時(shí),我們發(fā)現(xiàn)一個(gè)內(nèi)存峰值,把鼠標(biāo)放上去可以看到具體的內(nèi)存使用情況。內(nèi)存會(huì)有具體分類,Used、GC等。

Dart DevTools的內(nèi)存工具還是不夠完美,Xcode可以選擇某段內(nèi)存,看到這塊內(nèi)存中涉及到主要堆棧調(diào)用,并且點(diǎn)擊調(diào)用??梢蕴D(zhuǎn)到Xcode對(duì)應(yīng)的代碼中,而Dart DevTools還不具備這個(gè)功能,可能和Web的展示形式有關(guān)系。
內(nèi)存管理Flutter使用的是GC,回收速度可能不是很快,iOS中的ARC則是基于引用計(jì)數(shù)立即回收的。還有很多其他的功能,這里就不一一詳細(xì)敘述了,各位同學(xué)可以自己探索。
多實(shí)例
項(xiàng)目中是通過實(shí)例化FlutterViewController控制器來顯示Flutter界面的,整個(gè)Flutter頁面可以理解為一個(gè)畫布,通過頁面不斷的變化,改變畫布上的東西。所以,在單實(shí)例的情況下,Flutter頁面中間不能插入原生頁面。
這時(shí)候如果我們想在多個(gè)地方展示Flutter頁面,而這些頁面并不是Flutter -> Flutter的連貫跳轉(zhuǎn)形式,那怎么來實(shí)現(xiàn)這個(gè)場景呢?Google的建議是創(chuàng)建Flutter的多實(shí)例,并通過傳入不同的參數(shù)實(shí)例化不同的頁面。但這樣會(huì)造成很嚴(yán)重的內(nèi)存問題,所以并不能這么做。
Router
如果不能真正創(chuàng)建多個(gè)實(shí)例對(duì)象,那就需要通過其他方式來實(shí)現(xiàn)多實(shí)例。Flutter頁面顯示其實(shí)并不是跟著FlutterVC走的,而是跟著FlutterEngine走的。所以在創(chuàng)建一次FlutterVC之后,就將FlutterEngine保存下來,在其他位置創(chuàng)建FlutterVC時(shí)直接通過FlutterEngine的方式創(chuàng)建,并且在創(chuàng)建后進(jìn)行跳轉(zhuǎn)操作。
在進(jìn)行頁面切換時(shí),通過channelMethod調(diào)用Flutter側(cè)的路由切換代碼,并將切換后的新頁面FlutterVC添加到Native上。這種實(shí)現(xiàn)方式,就是通過Flutter的Router的方式實(shí)現(xiàn)的,下面將會(huì)介紹Router的兩種表現(xiàn)形式,靜態(tài)路由和動(dòng)態(tài)路由。
靜態(tài)路由
靜態(tài)路由是MaterialApp提供的一個(gè)API,routes本質(zhì)上是一個(gè)Map對(duì)象,其組成結(jié)構(gòu)是key是調(diào)用頁面的唯一標(biāo)識(shí)符,value就是對(duì)應(yīng)頁面的Widget。
在定義靜態(tài)路由時(shí),可以在創(chuàng)建Widget時(shí)傳入?yún)?shù),例如實(shí)例化ContactWidget時(shí)就可以傳入對(duì)應(yīng)的參數(shù)過去。
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();
}
}
進(jìn)行頁面跳轉(zhuǎn)時(shí),通過Navigator進(jìn)行調(diào)用,每次調(diào)用都會(huì)重新創(chuàng)建對(duì)應(yīng)的Widget。進(jìn)行調(diào)用時(shí)pushNamed函數(shù)會(huì)傳入一個(gè)參數(shù),這個(gè)參數(shù)就是定義Map時(shí)對(duì)應(yīng)頁面的key。
Navigator.of(context).pushNamed('page1');
動(dòng)態(tài)路由
靜態(tài)路由的方式并不是很靈活,相對(duì)而言動(dòng)態(tài)路由更加靈活。動(dòng)態(tài)路由不需要預(yù)先設(shè)定routes,直接調(diào)用即可。和普通push不同的是,動(dòng)態(tài)路由在push時(shí)通過PageRouteBuilder來構(gòu)建push對(duì)象,在Builder的構(gòu)建方法中執(zhí)行對(duì)應(yīng)的頁面跳轉(zhuǎn)操作即可。
結(jié)合之前說的channelMethod,就是在channelMethod對(duì)應(yīng)的Callback回調(diào)中,執(zhí)行Navigator的push函數(shù),接收Native傳遞過來的參數(shù)并構(gòu)建對(duì)應(yīng)的Widget頁面,將Widget返回給Builder即可完成頁面跳轉(zhuǎn)操作。所以說動(dòng)態(tài)路由的方式非常靈活。
無論是通過靜態(tài)路由還是動(dòng)態(tài)路由的方式創(chuàng)建,都可以通過then函數(shù)接收新頁面返回時(shí)的返回值。
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');
});
但動(dòng)態(tài)路由的跳轉(zhuǎn)方式也有一些問題,會(huì)導(dǎo)致動(dòng)畫失效。所以需要重寫Builder的transitionsBuilder函數(shù),來自定義轉(zhuǎn)場動(dòng)畫。
無論是通過靜態(tài)路由還是動(dòng)態(tài)路由的方式創(chuàng)建,都會(huì)存在一些問題。由于每次都是新創(chuàng)建Widget,所以在創(chuàng)建時(shí)會(huì)有黑屏的問題。而且每次創(chuàng)建的話,都會(huì)丟失當(dāng)前頁面上次的上下文狀態(tài),每次進(jìn)來都是一個(gè)新頁面。
簡書由于排版的問題,閱讀體驗(yàn)并不好,布局、圖片顯示、代碼等很多問題。所以建議到我Github上,下載Flutter編程指南 PDF合集。把所有Flutter文章總計(jì)三篇,都寫在這個(gè)PDF中,而且左側(cè)有目錄,方便閱讀。

下載地址:Flutter編程指南 PDF
麻煩各位大佬點(diǎn)個(gè)贊,謝謝!??