本文先介紹一下現(xiàn)有工程如何集成 Flutter 實(shí)現(xiàn)混合開發(fā),以及混合項(xiàng)目如何打包,再探索下如何降低原生和 Flutter 之間的依賴,使 Flutter 開發(fā)對(duì)原生開發(fā)的影響盡量降低,以及一些我在嘗試中遇到的問題及解決。
介紹 Flutter
Flutter 是 Google 發(fā)布的一個(gè)用于創(chuàng)建跨平臺(tái)、高性能移動(dòng)應(yīng)用的框架。Flutter 和 QT mobile 一樣,都沒有使用原生控件,相反都實(shí)現(xiàn)了一個(gè)自繪引擎,使用自身的布局、繪制系統(tǒng)。開發(fā)者可以通過 Dart 語言開發(fā) App,一套代碼同時(shí)運(yùn)行在 iOS 和 Android平臺(tái)。Flutter 提供了豐富的組件、接口,開發(fā)者可以很快地為 Flutter 添加 Native 擴(kuò)展。
前提工作
開發(fā)者需要安裝好 Flutter 的環(huán)境,執(zhí)行flutter doctor -v驗(yàn)證。

驗(yàn)證通過后即可開始集成 Flutter。
現(xiàn)有原生工程集成 Flutter
最官方的教程應(yīng)該是Add Flutter to existing apps了,按照教程如下一步步操作:
1.創(chuàng)建 flutter module
使用flutter create xxx指令創(chuàng)建的 Flutter 項(xiàng)目包括用于 Flutter/Dart 代碼的非常簡單的工程。你可以修改 main.dart 的內(nèi)容,以滿足你的需要,并在此基礎(chǔ)上進(jìn)行構(gòu)建。
假設(shè)你有一個(gè)已經(jīng)存在 iOS 工程(以 flutterHybridDemo 為例)在some/path/flutterHybridDemo,那么你新建的 flutter_module 和 iOS 工程應(yīng)該在同一目錄下(即都在 path 下)。
$ cd some/path/
$ flutter create -t module flutter_module

通過
shift+command+.顯示/隱藏隱藏文件夾
- lib/main.dart:存放的是 Dart 語言編寫的代碼,這里是核心代碼;
- pubspec.yaml:配置依賴項(xiàng)的文件,比如配置遠(yuǎn)程 pub 倉庫的依賴庫,或者指定本地資源(圖片、字體、音頻、視頻等);
- .ios/:iOS 部分代碼;
- .android/:Android 部分代碼;
- build/:存儲(chǔ) iOS 和 Android 構(gòu)建文件;
- test/:測試代碼。
2.將 flutter module 作為依賴添加到工程
假設(shè)文件夾結(jié)構(gòu)如下:
some/path/
flutter_module/
lib/main.dart
.ios/
...
flutterHybridDemo/
flutterHybridDemo.xcodeproj
flutterHybridDemo/
AppDelegate.h
AppDelegate.m
...
集成 Flutter 框架需要使用CocoaPods,這是因?yàn)?Flutter 框架還需要對(duì) flutter_module 中可能包含的任何 Flutter 插件可用。
- 如果需要,請(qǐng)參考cocoapods.org了解如何在您的電腦上安裝 CocoaPods。
創(chuàng)建 Podfile:
$ cd some/path/flutterHybridDemo
$ pod init
此時(shí)工程中會(huì)出現(xiàn)一個(gè) Podfile 文件,添加項(xiàng)目依賴的第三方庫就在這個(gè)文件中配置,編輯 Podfile 文件添加最后兩行代碼:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'TestOne' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for TestOne
target 'TestOneTests' do
inherit! :search_paths
# Pods for testing
end
target 'TestOneUITests' do
inherit! :search_paths
# Pods for testing
end
end
#新添加的代碼
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
- 如果你的工程(flutterHybridDemo)已經(jīng)在使用 Cocoapods ,你只需要做以下幾件事來整合你的 flutter_module 應(yīng)用程序:
(1)添加如下內(nèi)容到 Podfile:
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
(2)執(zhí)行pod install
當(dāng)你在some/path/flutter_module/pubspec.yaml中修改 Flutter 插件依賴時(shí),需要先執(zhí)行flutter packages get通過 podhelper.rb 腳本來刷新插件列表,然后再從some/path/flutterHybridDemo執(zhí)行一次pod install。
podhelper.rb 腳本將確保你的插件和 Flutter 框架被添加到你的工程中,以及 bitcode 被禁用。
(3)禁用 bitcode
因?yàn)?Flutter 現(xiàn)在不支持 bitcode。需要設(shè)置 Build Settings->Build Options->Enable Bitcode 為 NO。

3.為編譯 Dart 代碼配置 build phase
打開 iOS 工程,選中項(xiàng)目的 Build Phases 選項(xiàng),點(diǎn)擊左上角+號(hào)按鈕,選擇 New Run Script Phase。

將下面的 shell 腳本添加到輸入框中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
最后,確保 Run Script 這一行在 "Target dependencies" 或者 "Check Pods Manifest.lock" 后面。

至此,你可以編譯一下工程確保無誤:?B。
4.在 iOS 工程中使用 FlutterViewController
首先聲明你的 AppDelegate 是 FlutterAppDelegate 的子類。然后定義一個(gè) FlutterEngine 屬性,它可以幫助你注冊(cè)一個(gè)沒有 FlutterViewController 實(shí)例的插件。
在 AppDelegate.h:
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在AppDelegate.m,修改didFinishLaunchingWithOptions方法如下:
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#include "AppDelegate.h"
@implementation AppDelegate
// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
如果 AppDelegate 已經(jīng)繼承于別的類的時(shí)候,可以通過讓你的 delegate 實(shí)現(xiàn)FlutterAppLifeCycleProvider協(xié)議:
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end
然后生命周期方法應(yīng)該由 FlutterPluginAppLifeCycleDelegate 來代理:
@implementation AppDelegate
{
FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)viewController;
}
return nil;
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesBegan:touches withEvent:event];
// Pass status bar taps to key window Flutter rootViewController.
if (self.rootFlutterViewController != nil) {
[self.rootFlutterViewController handleStatusBarTouches:event];
}
}
- (void)applicationDidEnterBackground:(UIApplication*)application {
[_lifeCycleDelegate applicationDidEnterBackground:application];
}
- (void)applicationWillEnterForeground:(UIApplication*)application {
[_lifeCycleDelegate applicationWillEnterForeground:application];
}
- (void)applicationWillResignActive:(UIApplication*)application {
[_lifeCycleDelegate applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication*)application {
[_lifeCycleDelegate applicationDidBecomeActive:application];
}
- (void)applicationWillTerminate:(UIApplication*)application {
[_lifeCycleDelegate applicationWillTerminate:application];
}
- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
[_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
[_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application
didReceiveRemoteNotification:userInfo
fetchCompletionHandler:completionHandler];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [_lifeCycleDelegate application:application openURL:url options:options];
}
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
return [_lifeCycleDelegate application:application handleOpenURL:url];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
annotation:(id)annotation {
return [_lifeCycleDelegate application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
[_lifeCycleDelegate application:application
performActionForShortcutItem:shortcutItem
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
completionHandler:(nonnull void (^)(void))completionHandler {
[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
在 ViewController 中添加跳轉(zhuǎn)到 FlutterViewController 的測試代碼即可:
#import "ViewController.h"
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self
action:@selector(handleButtonAction)
forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"Jump to flutterViewController" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor grayColor]];
button.frame = CGRectMake(80.0, 210.0, 300.0, 40.0);
button.center = self.view.center;
[self.view addSubview:button];
}
- (void)handleButtonAction {
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
FlutterEngine *flutterEngine = delegate.flutterEngine;
FlutterViewController *flutterVC = [[FlutterViewController alloc]initWithEngine:flutterEngine nibName:nil bundle:nil];
[self presentViewController:flutterVC animated:YES completion:nil];
}
@end
5.使用熱重載的方式調(diào)試 Dart 代碼
熱重載指的是不用重新啟動(dòng)就看到修改后的效果,類似 web 網(wǎng)頁開發(fā)時(shí)保存就看到效果的方式。
進(jìn)入 flutter module,在終端執(zhí)行命令:
$ cd some/path/flutter_module
$ flutter run

并且你能在控制臺(tái)中看下如下內(nèi)容:
?? To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".
你可以在 flutter_module 中編輯 Dart code,然后在終端輸入 r 來使用熱重載。你也可以在瀏覽器中輸入上面的 URL 來查看斷點(diǎn)、分析內(nèi)存和其他的調(diào)試任務(wù)。
集成 Flutter 后工程打包
1. flutter build ios
執(zhí)行flutter build ios以創(chuàng)建 release 版本(flutter build 默認(rèn)為--release,如需創(chuàng)建 debug 版本執(zhí)行flutter build ios —debug)。
2.成功后修改 Xcode 為 release 模式配置
3.最后選擇 Product > Archive 以生成構(gòu)建版本即可

混合工程改造優(yōu)化
Flutter 的工程結(jié)構(gòu)比較特殊,由 Flutter 目錄、Native 工程的目錄(即 iOS 和 Android 兩個(gè)目錄)組成。默認(rèn)情況下,引入了 Flutter 的 Native 工程無法脫離父目錄進(jìn)行獨(dú)立構(gòu)建和運(yùn)行,因?yàn)樗鼤?huì)反向依賴于 Flutter 相關(guān)的庫和資源。
實(shí)際上,在真實(shí)的開發(fā)情況下,開發(fā)者很少會(huì)創(chuàng)建一個(gè)完全 Flutter 的工程重寫項(xiàng)目,更多的情況是原生工程集成 Flutter。
1.問題
這樣就帶來了一系列問題:
(1)構(gòu)建打包問題:引入 Flutter 后,Native 工程因?qū)ζ溆辛艘蕾嚭婉詈?,從而無法獨(dú)立編譯構(gòu)建。在 Flutter 環(huán)境下,工程的構(gòu)建是從 Flutter 的構(gòu)建命令開始,執(zhí)行過程中包含了 Native 工程的構(gòu)建,開發(fā)者要配置完整的 Flutter 運(yùn)行環(huán)境才能走通整個(gè)流程;
(2)混合編譯帶來的開發(fā)效率的降低:在轉(zhuǎn)型 Flutter 的過程中必然有許多業(yè)務(wù)仍使用 Native 進(jìn)行開發(fā),工程結(jié)構(gòu)的改動(dòng)會(huì)使開發(fā)無法在純 Native 環(huán)境下進(jìn)行,而適配到 Flutter 工程結(jié)構(gòu)對(duì)純 Native 開發(fā)來說又會(huì)造成不必要的構(gòu)建步驟,造成開發(fā)效率的降低。
2.目標(biāo)
希望能將 Flutter 依賴抽取出來,作為一個(gè) Flutter 依賴庫,供純 Native 工程引用,無需配置完整的 Flutter 環(huán)境。
3.Flutter 產(chǎn)物
iOS 工程對(duì) Flutter 有如下依賴:
Flutter.framework:Flutter 庫和引擎
App.framework:dart 業(yè)務(wù)源碼相關(guān)文件
flutter_assets:Flutter依賴的靜態(tài)資源,如字體,圖片等
Flutter Plugin:編譯出來的各種 plugin 的 framework
把以上依賴的編譯結(jié)果抽取出來,即是 Flutter 相關(guān)代碼的最終產(chǎn)物。
那么我們只需要將這些打包成一個(gè) SDK 依賴的形式提供給 Native 工程,就可以解除 Native 工程對(duì) Flutter 工程的直接依賴。
產(chǎn)物的產(chǎn)生:
對(duì) flutter 工程執(zhí)行 flutter build 命令后,生成在.ios/Flutter目錄下,直接手動(dòng)拷貝 framework 到主工程即可。
注意事項(xiàng):
framework 選擇 Create groups 加入文件夾,flutter_assets 選擇 Create folder references 加入文件夾。

加入完成后的結(jié)構(gòu):

framework 加入后,記住一定要確認(rèn) framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。

最后改造 APPDelegate 即可:
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate>
@property (strong, nonatomic) FlutterEngine *flutterEngine;
@end
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc]initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
return YES;
}
4. 優(yōu)化
為了更方便管理 framework,可以將這些文件上傳到遠(yuǎn)程倉庫,通過 CocoaPods 導(dǎo)入,Native 項(xiàng)目只需及時(shí)更新 pod 依賴即可。
我遇到過的一些問題及解決
1.在 Android Studio 上跑設(shè)備
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

提示你當(dāng)前有兩個(gè)模擬器設(shè)備,跑設(shè)備的時(shí)候要選擇運(yùn)行在哪個(gè)設(shè)備上,flutter run后面拼接上“-d <deviceId>”,deviceId 是第二列的內(nèi)容。
flutter run -d emulator-5554
flutter run -d C517D2D4-EAFA-42CA-B260-A18FA0ABFF60
電腦連著真機(jī)也同理,改成真機(jī)的 deviceId 即可。
2.flutter build ios 報(bào)錯(cuò)
build 時(shí)可能遇到的錯(cuò)誤:
It appears that your application still contains the default signing identifier.Try replacing 'com.example' with your signing id in Xcode:
open ios/Runner.xcworkspace

解決方法:
修改some/flutter_module/.ios/下 Runner 工程的 Bundle Identifier 和原生工程的一致,再次運(yùn)行flutter build ios即可。
3.開發(fā)時(shí)打包產(chǎn)物編譯失敗
當(dāng)你用flutter build ios的產(chǎn)物添加到原生工程中,跳轉(zhuǎn)到 Flutter 界面會(huì)黑屏并報(bào)出如下錯(cuò)誤:

Failed to find snapshot: …/Library/Developer/CoreSimulator/Devices/…/data/Containers/Bundle/Application/…/FlutterMixDemo.app/Frameworks/App.framework/flutter_assets/kernel_blob.bin
如何解決:
調(diào)試模式下用flutter build ios —debug的產(chǎn)物,再次拖入工程即可。
原因:
首先我們對(duì)比下,執(zhí)行flutter build ios和執(zhí)行flutter build ios --debug后 .ios/Flutter/App.framework/flutter_assets的文件內(nèi)容:


可以發(fā)現(xiàn),差別是在于三個(gè)文件:isolate_snapshot_data、kernel_blob.bin、vm_snapshot_data。
這里涉及 Flutter 的編譯模式知識(shí),具體可以參閱Flutter 的兩種編譯模式。
Flutter 開發(fā)階段的編譯模式:使用了 Kernel Snapshot 模式編譯,打包產(chǎn)物中,可以發(fā)現(xiàn)幾樣?xùn)|西:
isolate_snapshot_data:用于加速 isolate 啟動(dòng),業(yè)務(wù)無關(guān)代碼,固定,僅和 flutter engine 版本有關(guān);
platform.dill:和 Dart VM 相關(guān)的 kernel 代碼,僅和 Dart 版本以及 engine 編譯版本有關(guān)。固定,業(yè)務(wù)無關(guān)代碼;
vm_snapshot_data:用于加速 Dart VM 啟動(dòng)的產(chǎn)物,業(yè)務(wù)無關(guān)代碼,僅和 flutter engine 版本有關(guān);
kernel_blob.bin:業(yè)務(wù)代碼產(chǎn)物 。
Flutter 生產(chǎn)階段的編譯模式:選擇了 AOT 打包。
4.集成后 Native 工程報(bào)錯(cuò)
Shell Script Invocation Error
line 2:/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory

解決方法:
修改 TARGETS -> Build Setting -> FLUTTER_ROOT 為電腦安裝的 Flutter 環(huán)境的路徑即可。

5.如何在 iOS 工程 Debug 模式下使用 release 模式的 flutter
只需要將 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 修改為 release,F(xiàn)LUTTER_FRAMEWORK_DIR 修改為 release 對(duì)應(yīng)的路徑即可。
其他
1.說明:
本文僅供用于學(xué)習(xí)參考,請(qǐng)勿用于商業(yè)用途。如需轉(zhuǎn)載,請(qǐng)標(biāo)明出處,謝謝合作。
本文系參考網(wǎng)絡(luò)公開 Flutter 學(xué)習(xí)資料以及個(gè)人學(xué)習(xí)體會(huì)總結(jié)所得,部分內(nèi)容為網(wǎng)絡(luò)公開學(xué)習(xí)資料,如有侵權(quán)請(qǐng)聯(lián)系作者刪除。
2.參考資料:
Flutter 中文網(wǎng):https://flutterchina.club
咸魚技術(shù)-flutter:https://www.yuque.com/xytech/flutter
iOS Native混編Flutter交互實(shí)踐:https://juejin.im/post/5bb033515188255c5e66f500#heading-3
Flutter混編之路——開發(fā)集成(iOS篇):http://www.itdecent.cn/p/48a9083ebe89
作者簡介
就職于甜橙金融(翼支付)信息技術(shù)部,負(fù)責(zé) iOS 客戶端開發(fā)。