概述

尽管Flutter的初衷是用Flutter构建一个完整的APP而不是简单的页面,但在实际开发中不可避免得需要与原生代码进行通讯混编,而混编的两种方案,即Flutter混合原生和原生混合Flutter,也是实际开发中需要作出抉择的,本文也会做一定的探讨。
由于个人的技术局限性,我只对iOS部分做研究。

Flutter与原生交互的通道—Channel

Channel是Flutter与原生交互的一个重要对象,Flutter(或原生)可以创建创建一个指定名字的Channel对象,利用channel对象发送消息,而另一段即原生(或Flutter)也可以拿到同样名字的Channel对象,监听channel对象的回调方法,这样就可以实现远程flutter与原生的互调了。
Flutter中初始化MethodChannel:

  1. //初始化
  2. MethodChannel _methodChannel = MethodChannel('mine_page/method');
  3. File _avataFile;
  4. @override
  5. void initState() {
  6. super.initState();
  7. //监听MethodChannel的返回
  8. _methodChannel.setMethodCallHandler((call){
  9. if(call.method == "imagePath"){
  10. String imagePath = call.arguments.toString().substring(7);//去除路径中的前缀 file://
  11. setState(() {
  12. _avataFile = File(imagePath);
  13. });
  14. }
  15. }
  16. );
  17. }

Flutter中给原生iOS发消息:

  1. _methodChannel.invokeMethod("picture");

OC中,用Xcode打开ios工程

AppDelegate.h实现协议UINavigationControllerDelegate,UIImagePickerControllerDelegate

  1. @interface AppDelegate : FlutterAppDelegate<UINavigationControllerDelegate,UIImagePickerControllerDelegate>
  2. @end

AppDelegate.m

  1. #import "AppDelegate.h"
  2. #import "GeneratedPluginRegistrant.h"
  3. @interface AppDelegate()
  4. @property(nonatomic,strong)FlutterMethodChannel *methodChannel;
  5. @end
  6. @implementation AppDelegate
  7. - (BOOL)application:(UIApplication *)application
  8. didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  9. [GeneratedPluginRegistrant registerWithRegistry:self];
  10. // 创建FlutterViewController
  11. FlutterViewController *vc = [[FlutterViewController alloc] init];
  12. // 创建channel,name与Flutter中的Channel的name保持一致
  13. self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"mine_page/method" binaryMessenger:vc];
  14. // 设置channel的回调
  15. [self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
  16. if ([call.method isEqualToString:@"picture"]) {
  17. UIImagePickerController *imageVc = [[UIImagePickerController alloc] init];
  18. [vc presentViewController:imageVc animated:YES completion:nil];
  19. }
  20. }];
  21. return [super application:application didFinishLaunchingWithOptions:launchOptions];
  22. }
  23. -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info{
  24. [picker dismissViewControllerAnimated:YES completion:^{
  25. NSString *imagePath = [NSString stringWithFormat:@"%@",info[@"UIImagePickerControllerImageURL"]];
  26. [self.methodChannel invokeMethod:@"imagePath" arguments:imagePath];
  27. }];
  28. }
  29. @end

其中info的结构为:
image.png

Channel的分类

除了MethodChannel,还有BasicMessageChannel和EventChannel两种

  • MethodChannel:用于传递方法调用。
  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • EventChannel: 用于数据流的通信。

而BasicMessageChannel和EventChannel可以进行连续通讯,而MethodChannel是一次消息通讯。

Flutter混原生

谷歌是推荐开发使用这种模式进行混编,而调用原生页面可以使用channel,就可以调起原生页面。
在Flutter项目的目录中找到ios文件夹,这一部分便是Flutter项目的iOS部分。
在Appdelegate.m中

  1. - (BOOL)application:(UIApplication *)application
  2. didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  3. [GeneratedPluginRegistrant registerWithRegistry:self];
  4. FlutterViewController * vc = (FlutterViewController *)self.window.rootViewController;
  5. FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"method" binaryMessenger:vc];
  6. UIViewController * nativeVC = [[UIViewController alloc] init];
  7. [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
  8. if ([call.method isEqualToString:@"show"]) {
  9. [vc presentViewController:nativeVC animated:YES completion:nil];
  10. }
  11. }];
  12. return [super application:application didFinishLaunchingWithOptions:launchOptions];
  13. }

持有channel对象,同样通过channel给flutter发送消息,把iOS执行的结果返回给Flutter。

原生混Flutter

在一个原有的原生项目中嵌入Flutter页面,而Flutter也提供了相应的支持。

创建module

image.png

也可以使用命令行 flutter create -t module flutter_module

FLutter项目嵌入

首先创建Flutter Module,命名为flutter_module,将module项目和原生项目放到同级目录,然后在Podfile文件中:

  1. # Uncomment the next line to define a global platform for your project
  2. # platform :ios, '9.0'
  3. flutter_application_path = '../flutter_module'
  4. load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
  5. target 'NativeDemo' do
  6. install_all_flutter_pods(flutter_application_path)
  7. use_frameworks!
  8. # Pods for NativeDemo
  9. end

执行pod install命令
通过cocoapods,Flutter 模块就嵌入原生项目工程了。

Flutter页面调用

通过导入,就可以调用Flutter页面了。

  1. #import <Flutter/Flutter.h>
  2. FlutterViewController *vc = [[FlutterViewController alloc] init];
  3. vc.modalPresentationStyle = UIModalPresentationFullScreen;//全屏
  4. [self presentViewController:vc animated:YES completion:nil];

可以看到调起的页面是main.dart中的页面,如果要调起指定的flutter页面,可以通过

  1. FlutterViewController *vc = [[FlutterViewController alloc] init];
  2. vc.modalPresentationStyle = UIModalPresentationFullScreen;//全屏
  3. [vc setInitialRoute:@"one"];
  4. [self presentViewController:vc animated:YES completion:nil];


传递指定标示路由,然后在main.dart中通过window.defaultRouteName拿到InitialRoute现实对应的Flutter页面(通过StatefulWidget变化页面)

  1. import 'dart:ui';
  2. void main() => runApp(MyApp(
  3. pageIndex: window.defaultRouteName,
  4. ));
  5. class MyApp extends StatelessWidget {
  6. final String pageIndex;
  7. const MyApp({Key key, this.pageIndex}) : super(key: key);
  8. // This widget is the root of your application.
  9. @override
  10. Widget build(BuildContext context) {
  11. return MaterialApp(
  12. title: 'Flutter Demo',
  13. theme: ThemeData(
  14. highlightColor: Color.fromRGBO(1, 0, 0, 0.0), //点击高亮设置为透明
  15. splashColor: Color.fromRGBO(1, 0, 0, 0.0), //水波纹色设置为透明
  16. primarySwatch: Colors.blue,
  17. ),
  18. home: _rootPage(pageIndex),
  19. );
  20. }
  21. _rootPage(String pageIndex) {
  22. switch (pageIndex) {
  23. case 'one':
  24. return Scaffold(
  25. appBar: AppBar(
  26. title: Text(pageIndex),
  27. ),
  28. body: Center(
  29. child: Text(pageIndex),
  30. ),
  31. );
  32. case 'two':
  33. return Scaffold(
  34. appBar: AppBar(
  35. title: Text(pageIndex),
  36. ),
  37. body: Center(
  38. child: Text(pageIndex),
  39. ),
  40. );
  41. }
  42. }
  43. }

原生调Flutter性能优化

尽管上述代码已经实现了原生调起Flutter页面,当仍然存在以下问题:

  1. 卡顿问题
  2. 内存泄露问题
  3. 状态保持问题

原生拉起Flutter页面的时候会发现明显的卡顿,这是因为创建一个大内存占用的FlutterViewController是很消耗性能,所以我们可以考虑缓存的策略,用空间换时间的方法,解决卡顿的问题 —- 懒加载FlutterEngine。

ios原生端:

  1. #import "ViewController.h"
  2. #import <Flutter/Flutter.h>
  3. @interface ViewController ()
  4. @property(nonatomic, strong) FlutterEngine* flutterEngine;
  5. @property(nonatomic, strong) FlutterViewController* flutterVc;
  6. @property(nonatomic, strong) FlutterBasicMessageChannel * msgChannel;
  7. @end
  8. @implementation ViewController
  9. - (FlutterEngine *)flutterEngine{
  10. if (!_flutterEngine) {
  11. FlutterEngine* flutterEngine = [[FlutterEngine alloc] initWithName:@"engin"];
  12. if (flutterEngine.run) {
  13. _flutterEngine = flutterEngine;
  14. }
  15. }
  16. return _flutterEngine;
  17. }
  18. - (void)viewDidLoad {
  19. [super viewDidLoad];
  20. self.flutterVc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
  21. self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc];
  22. [self.msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
  23. NSLog(@"收到Flutter的%@",message);
  24. }];
  25. }
  26. -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  27. {
  28. static int a = 0;
  29. [self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
  30. }
  31. - (IBAction)pushFlutter:(id)sender {
  32. //告诉Flutter显示one_page
  33. FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc];
  34. [methodChannel invokeMethod:@"one" arguments:nil];
  35. self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
  36. //弹出Flutter页面!
  37. [self presentViewController:self.flutterVc animated:YES completion:nil];
  38. //监听Flutter页面的回调
  39. __weak typeof(self) weakSelf = self;
  40. [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
  41. //如果是要我退出
  42. if ([call.method isEqualToString:@"exit"]) {
  43. [weakSelf.flutterVc dismissViewControllerAnimated:YES completion:nil];
  44. }
  45. }];
  46. }
  47. - (IBAction)pushFlutterTow:(id)sender {
  48. //告诉Flutter显示one_page
  49. FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:@"tow_page" binaryMessenger:self.flutterVc];
  50. [methodChannel invokeMethod:@"tow" arguments:nil];
  51. self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
  52. //弹出Flutter页面!
  53. [self presentViewController:self.flutterVc animated:YES completion:nil];
  54. //监听Flutter页面的回调
  55. __weak typeof(self) weakSelf = self;
  56. [methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
  57. //如果是要我退出
  58. if ([call.method isEqualToString:@"exit"]) {
  59. [weakSelf.flutterVc dismissViewControllerAnimated:YES completion:nil];
  60. }
  61. }];
  62. }
  63. @end

我们可以通过一个单例持有Engine,通过Engine创建FlutterViewController,避免每次创建时消耗的大量性能,而持有Engine,也能保持Flutter模块状态,不需要保存到原生项目中,解决了状态保持的问题

另外FlutterViewController也存在着内存泄露(Flutter Bug,暂时无法解决),一个FlutterViewController从页面上消失后仍然会有2MB的内存泄露,虽然暂时无法解决,我们可以用一个单例持有一个FlutterViewController,通过channel来决定显示那个页面,避免每次创建一个新的FlutterViewController造成性的内存泄露。

原生混Flutter性能明显不如Flutter混原生,所以谷歌并不推荐这种方式。

Flutter端:

  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. void main() => runApp(MyApp());
  4. class MyApp extends StatefulWidget {
  5. @override
  6. _MyAppState createState() => _MyAppState();
  7. }
  8. class _MyAppState extends State<MyApp> {
  9. final MethodChannel _oneChannel = MethodChannel('one_page');
  10. final MethodChannel _towChannel = MethodChannel('tow_page');
  11. final BasicMessageChannel _messageChannel =
  12. BasicMessageChannel('messageChannel', StandardMessageCodec());
  13. String _pageIndex;
  14. @override
  15. void initState() {
  16. super.initState();
  17. _messageChannel.setMessageHandler((message) {
  18. print('收到来自iOS的:$message');
  19. return null;
  20. });
  21. _oneChannel.setMethodCallHandler((call) {
  22. setState(() {
  23. _pageIndex = call.method;
  24. });
  25. return null;
  26. });
  27. _towChannel.setMethodCallHandler((call) {
  28. setState(() {
  29. _pageIndex = call.method;
  30. });
  31. return null;
  32. });
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. return MaterialApp(
  37. title: 'Flutter Demo',
  38. theme: ThemeData(
  39. primarySwatch: Colors.blue,
  40. ),
  41. home: _rootPage(_pageIndex),
  42. );
  43. }
  44. Widget _rootPage(String pageIndex) {
  45. switch (pageIndex) {
  46. case 'one':
  47. return Scaffold(
  48. appBar: AppBar(
  49. title: Text(pageIndex),
  50. ),
  51. body: Column(
  52. mainAxisAlignment: MainAxisAlignment.center,
  53. children: <Widget>[
  54. RaisedButton(
  55. onPressed: () {
  56. _oneChannel.invokeMapMethod('exit');
  57. },
  58. child: Text(pageIndex),
  59. ),
  60. TextField(
  61. onChanged: (String str) {
  62. _messageChannel.send(str);
  63. },
  64. )
  65. ],
  66. ),
  67. );
  68. case 'tow':
  69. return Scaffold(
  70. appBar: AppBar(
  71. title: Text(pageIndex),
  72. ),
  73. body: Center(
  74. child: RaisedButton(
  75. onPressed: () {
  76. _towChannel.invokeMapMethod('exit');
  77. },
  78. child: Text(pageIndex),
  79. )),
  80. );
  81. default:
  82. return Scaffold(
  83. appBar: AppBar(
  84. title: Text("default"),
  85. ),
  86. body: Center(
  87. child: RaisedButton(
  88. onPressed: () {
  89. MethodChannel('default_page').invokeMapMethod('exit');
  90. },
  91. child: Text(pageIndex),
  92. )),
  93. );
  94. }
  95. }
  96. }