混合开发与Flutter引擎
原生嵌入Flutter
原生要想嵌入Flutter
,Flutter
就不能是一个独立的App
,新建工程的时候要选择Flutter Module
类型创建,如下图所示
下面新建flutter_module
工程(纯Flutter工程
),其代码还是在lib
目录下编写
- 如果想把
App
类型的工程代码迁移至Module
类型的工程,只需要把lib
目录下的.dart
文件迁移过来即可; -
Module
类型的工程中也有ios
、android
目录,是隐藏目录,官方不建议在这两个隐藏目录下添加任何代码,主要用于在两个平台下调试。
使用Xcode新建iOS工程NativeDemo
,与flutter_module
工程放入同一目录下。使用cocoapods
使两个工程进行关联
-
cocoapods
初始化
$ cd /Users/wn/Documents/Flutter学习/NativeDemo
$ pod init
- 配置
Podfile
文件
<!-- Podfile文件内容 -->
// flutter_module工程的路径
flutter_application_path = '../flutter_module'
// 把flutter工程加载到iOS工程中
load File.join(flutter_application_path,'.iOS','Flutter','podhelper.rb')
platform :ios, '9.0'
target 'NativeDemo' do
// 把flutter依赖的一些库加载进来
install_all_flutter_pods(flutter_application_path)
use_frameworks!
# Pods for NativeDemo
end
-
cocoapods
安装
$ pod install
- 打开
NativeDemo
工程,在ViewController.m
文件中导入Flutter
头文件
// 如果能成功导入,就说明iOS工程NativeDemo与Flutter工程flutter_module已经进行了关联
#import <Flutter/Flutter.h>
- 原生加载
Flutter
页面
// Main.storyboard文件中添加按钮,并关联点击事件
- (IBAction)pushFlutter:(id)sender {
// 跳转flutter页面
FlutterViewController *vc = [[FlutterViewController alloc] init];
[self presentViewController:vc animated:true completion:nil];
}
运行NativeDemo工程跳转Flutter页面
注意:如果Flutter
代码有更新,直接打开iOS工程,是无法展示Flutter
最新代码的,解决方案如下:
- 方案一:用
Android Studio
重新打开,编译运行 - 方案二:
Xcode
清除缓存再运行
查看NativeDemo
工程内存以及App
包内容
打开Flutter页面内存暴增
NativeDemo包的体积增大
运行NativeDemo
发现的几个问题?
- 问题一:打开
Flutter
页面,内存暴增 - 问题二:返回关闭
Flutter
页面,使其销毁,内存依然使用了94.5MB
- 问题三:点击按钮再次跳转
Flutter
页面,这一次内存使用了154.7MB
后面针对以上问题进行优化......
显示对应的Flutter页面
原生跳转不同的Flutter
页面
-
Main.storyboard
文件中再添加一个按钮,并关联点击事件
<!-- ViewController.m文件 -->
/*
* 标记页面的方式
* 1. 使用路由标记
* 2. 使用FlutterMethodChannel通信标记
*/
- (IBAction)pushFlutter:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
// 使用路由标记页面
[vc setInitialRoute:@"one"];
[self presentViewController:vc animated:true completion:nil];
}
- (IBAction)pushFlutterTwo:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
// 使用路由标记页面
[vc setInitialRoute:@"two"];
[self presentViewController:vc animated:true completion:nil];
}
<!-- 查看setInitialRoute方法介绍 -->
/**
* 不建议使用的API
* Deprecated API to set initial route.
*
* Attempts to set the first route that the Flutter app shows if the Flutter
* 默认路由名称是 /
* runtime hasn't yet started. The default is "/".
* 初始化页面的时候,又创建了一个Flutter引擎
* effect when using `initWithEngine` if the `FlutterEngine` has already been
* run.
*
* @param route The name of the first route to show.
*/
- (void)setInitialRoute:(NSString*)route
FLUTTER_DEPRECATED("Use FlutterViewController initializer to specify initial route");
-
Flutter
中根据不同路由名称展示不同页面
<!-- main.dart文件 -->
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(MyApp(
// flutter获取原生传入的路由名称
pageIndex: window.defaultRouteName,
));
class MyApp extends StatelessWidget {
final String pageIndex;
const MyApp({Key? key, required this.pageIndex}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: _rootPage(pageIndex),
);
}
//根据pageIndex来返回页面!
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(child: Text(pageIndex)),
);
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(child: Text(pageIndex)),
);
default:
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(child: Text(pageIndex)),
);
}
}
}
按钮一跳转Flutter第一个页面
按钮二跳转Flutter第二个页面
设置跳转模式
- (IBAction)pushFlutter:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] init];
// 使用路由标记页面
[vc setInitialRoute:@"one"];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:vc animated:true completion:nil];
}
全屏模式跳转
我们发现跳转到Flutter
页面之后无法返回去了,这个时候就需要Flutter
添加事件告诉原生返回去。
退回原生页面
现在对Flutter
页面进行包装,使其能够返回原生页面
- 页面添加
ElevatedButton
按钮
//根据pageIndex来返回页面!
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
// 点击按钮的时候给原生发送消息,退出Flutter页面
const MethodChannel('one_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
const MethodChannel('two_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
default:
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
const MethodChannel('default_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
}
}
- 优化
Flutter
引擎,防止每次初始化页面都创建引擎
#import "ViewController.h"
#import <Flutter/Flutter.h>
@interface ViewController ()
@property(nonatomic, strong) FlutterEngine* flutterEngine;
@end
@implementation ViewController
// 懒加载创建Flutter引擎,不用每次初始化页面的时候都创建引擎,这样的话内存使用会降下来
-(FlutterEngine *)flutterEngine
{
if (!_flutterEngine) {
FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"hk"];
if (engine.run) {
// Flutter引擎运行起来的时候再赋值
_flutterEngine = engine;
}
}
return _flutterEngine;
}
- (IBAction)pushFlutter:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
// 直接用self.flutterEngine初始化vc,下面设置路由名称就会失效
[vc setInitialRoute:@"one"];
[self presentViewController:vc animated:true completion:nil];
}
- (IBAction)pushFlutterTwo:(id)sender {
FlutterViewController *vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
// 使用路由标记页面
[vc setInitialRoute:@"two"];
[self presentViewController:vc animated:true completion:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
// 调用一次引擎,使其run起来,防止第一次跳转flutter页面卡顿
self.flutterEngine;
}
@end
- 直接用
self.flutterEngine
初始化vc
,设置路由名称就会失效,这里先查看下Flutter
引擎优化后,内存的使用情况
直接运行NativeDemo
原生工程,内存就会增加到90MB
,原因是viewDidLoad
方法中加载了Flutter
引擎。接着跳转Flutter
页面,退出Flutter
页面再次进来,内存基本稳定在94.8MB
,成功解决了多次跳转Flutter
页面,内存成倍增加的问题。
- 使用
FlutterMethodChannel
通信标记页面
@interface ViewController ()
@property(nonatomic, strong) FlutterEngine* flutterEngine;
// 不用每次跳转Flutter页面都创建FlutterViewController
@property(nonatomic, strong) FlutterViewController* flutterVc;
@end
@implementation ViewController
-(FlutterEngine *)flutterEngine
{
if (!_flutterEngine) {
FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"hk"];
if (engine.run) {
_flutterEngine = engine;
}
}
return _flutterEngine;
}
- (IBAction)pushFlutter:(id)sender {
self.flutterVc.modalPresentationStyle = UIModalPresentationFullScreen;
//创建channel
FlutterMethodChannel * methodChannel = [FlutterMethodChannel methodChannelWithName:@"one_page" binaryMessenger:self.flutterVc.binaryMessenger];
//告诉Flutter对应的页面
[methodChannel invokeMethod:@"one" arguments:nil];
//弹出页面
[self presentViewController:self.flutterVc animated:YES completion:nil];
//监听退出
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
//如果是exit我就退出页面!
if ([call.method isEqualToString:@"exit"]) {
[self.flutterVc dismissViewControllerAnimated:YES completion:nil];
}
}];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.flutterVc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
}
这个时候跳转Flutter
页面,点击Flutter
页面中间按钮,就能成功返回原生页面。
下面把Flutter
页面中的MyApp
改成有状态的小部件
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final MethodChannel _oneChannel = const MethodChannel('one_page');
final MethodChannel _twoChannel = const MethodChannel('two_page');
String pageIndex = 'one';
@override
void initState() {
super.initState();
// flutter设置监听
_oneChannel.setMethodCallHandler((call) {
pageIndex = call.method;
print(call.method);
setState(() {});
return Future(() {});
});
_twoChannel.setMethodCallHandler((call) {
pageIndex = call.method;
print(call.method);
setState(() {});
return Future(() {});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: _rootPage(pageIndex),
);
}
//根据pageIndex来返回页面!
Widget _rootPage(String pageIndex) {
switch (pageIndex) {
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
case 'two':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
_twoChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
default:
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Center(
child: ElevatedButton(
onPressed: () {
const MethodChannel('default_page').invokeMapMethod('exit');
},
child: Text(pageIndex),
),
),
);
}
}
}
返回原生页面
点击Flutter
页面中间按钮,成功返回原生页面。
Flutter和原生通信
Flutter
与原生通信的Channel
类型
-
MethodChannel
:传递方法的调用;单次通信 -
BasicMessageChannel
:传递字符串和半结构化信息;持续通信,收到消息之后还可以回复消息; -
EventChannel
:传递数据流。
下面来练习下BasicMessageChannel
通信
-
Flutter
给原生发送消息以及接收原生发送的消息
// 初始化BasicMessageChannel
class _MyAppState extends State<MyApp> {
// BasicMessageChannel有一个编解码器
final BasicMessageChannel _messageChannel =
const BasicMessageChannel('messageChannel', StandardMessageCodec());
......
// 接收原生发送的消息
@override
void initState() {
super.initState();
_messageChannel.setMessageHandler((message) {
print('收到来自iOS的$message');
return Future(() {});
});
......
// 给原生发送消息
case 'one':
return Scaffold(
appBar: AppBar(title: Text(pageIndex)),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_oneChannel.invokeMapMethod('exit');
},
child: Text(pageIndex),
),
TextField(
onChanged: (String str) {
// 给原生发送消息
_messageChannel.send(str);
},
)
],
),
);
- 原生接收
Flutter
消息以及给Flutter发送消息
// 声明FlutterBasicMessageChannel
@interface ViewController ()
@property(nonatomic, strong) FlutterBasicMessageChannel * msgChannel;
@end
// 接收Flutter发送的消息
- (void)viewDidLoad {
[super viewDidLoad];
self.msgChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messageChannel" binaryMessenger:self.flutterVc.binaryMessenger];
[self.msgChannel setMessageHandler:^(id _Nullable message, FlutterReply _Nonnull callback) {
NSLog(@"收到Flutter的:%@",message);
}];
}
// 给Flutter发送消息
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int a = 0;
// 给Flutter发送消息
[self.msgChannel sendMessage:[NSString stringWithFormat:@"%d",a++]];
}
下载Flutter引擎源码
查看Flutter引擎
版本号
$ flutter doctor -v
查看Flutter引擎版本
查看Flutter Channel
版本
$ flutter channel
Flutter channels:
master //主分支,一般是github上面的分支
dev //开发分支,正在开发还没有完成的分支
beta //新特性分支
* stable //channel稳定分支,一般用的是这个分支
// 切换channel版本,切换到master版本
$ flutter channel master
默认是mian分支
查看Flutter引擎
完整版本号
$ cat $FLUTTER/bin/internal/engine.version
241c87ad800beeab545ab867354d4683d5bfb6ce
下载引擎代码
⼯具准备
-
Chromium
提供的部署⼯具depot_tools
,下载完成之后推荐放在用户根目录下
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
- 配置⼯具的环境变量
$ vi ~/.zshrc
配置环境变量
- 安装最后⼀个⼯具
$ brew install ant
// 查看有没有安装 ant,列表中有的话就表示已经安装
$ brew list
- 下载
Flutter引擎
$ cd Desktop
$ mkdir engine
$ cd engine
- 创建gclien⽂件
$ touch .gclient
- 然后配置⽂件。(尤其要确定CommitID保持⼀致)
solutions = [
{
"managed": False,
"name": "src/flutter",
// 6bc433c6b6b5b98dcf4cc11aff31cdee90849f32就是CommitID
"url": "git@github.com:flutter/engine.git@6bc433c6b6b5b98dcf4cc11aff31cdee90849f32",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
CommitID
在~/flutter/bin/internal/engine.version
文件中
- 执⾏
gclient sync
进行下载(这个操作将会fetch Flutter所有的依赖。这⾥有15G⽂件,需要点时间,请保持⽹络!该操作需要翻墙)
下载完成大概是15.46G
$ gclient sync
Flutter引擎目录
注意:引擎下载是断点续传
的,如果网络断掉,重新执行该命令即可。
引擎升级相关
当我们升级了Flutter的SDK,我们想要升级引擎代码。直接更新.gclient
⽂件。
$ cat $FLUTTER/bin/internal/engine.version
241c87ad800beeab545ab867354d4683d5bfb6ce
- 然后将这个修改到
.gclient
⽂件中
solutions = [
{
"managed": False,
"name": "src/flutter",
// 更新CommitID
"url": "git@github.com:flutter/engine.git@241c87ad800beeab545ab867354d4683d5bfb6ce",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
- 然后进⼊
src/flutter⽬录
$ git pull
$ git reset --hard commitID
- 回到
engine
⽬录,也就是.gclient
⽂件所在的⽬录
$ gclient sync --with_branch_heads --with_tags --verbose
编译引擎源码
- 我们先要使⽤
GN
:这是⼀个⽣成Ninja
构建⽂件的元构建系统,最后我们还是⽤Ninja
编译!
- 构建
#构建iOS设备使⽤的引擎
#真机debug版本
$ ./gn --ios --unoptimized
#真机release版本(⽇常开发使⽤,如果我们要⾃定义引擎)
$ ./gn --ios --unoptimized --runtime-mode=release
#模拟器版本
$ ./gn --ios --simulator --unoptimized
#主机端(Mac)构建
$ ./gn --unoptimized
构建完成会有四个Xcode⼯程
image.png- 使⽤ninja编译⼯程(这⾥也是⼀个耗时操作)
$ ninja -C host_debug_unopt && ninja -C ios_debug_sim_unopt && ninja -C ios_debug_unopt && ninja -C ios_release_unopt
编译完成之后文件大概是26.79G