混合开发与Flutter引擎

原生嵌入Flutter

原生要想嵌入FlutterFlutter就不能是一个独立的App,新建工程的时候要选择Flutter Module类型创建,如下图所示

工程类型

下面新建flutter_module工程(纯Flutter工程),其代码还是在lib目录下编写

  • 如果想把App类型的工程代码迁移至Module类型的工程,只需要把lib目录下的.dart文件迁移过来即可;
  • Module类型的工程中也有iosandroid目录,是隐藏目录,官方不建议在这两个隐藏目录下添加任何代码,主要用于在两个平台下调试。

使用Xcode新建iOS工程NativeDemo,与flutter_module工程放入同一目录下。使用cocoapods使两个工程进行关联

  • cocoapods初始化
$ cd /Users/wn/Documents/Flutter学习/NativeDemo
$ pod init 
  • 配置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页面
// 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文件中再添加一个按钮,并关联点击事件

/*
 * 标记页面的方式
 * 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];
}


/**
 * 不建议使用的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中根据不同路由名称展示不同页面

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 

@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 {
  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 {
  // 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 *)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

Flutter引擎开源源码

默认是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

安装Homebrew

// 查看有没有安装 ant,列表中有的话就表示已经安装
$ brew list 
  • 下载Flutter引擎
$ cd Desktop
$ mkdir engine
$ cd engine
  • 创建gclien⽂件
$ touch .gclient
  • 然后配置⽂件。(尤其要确定CommitID保持⼀致)
solutions = [
    {
        "managed":  False,
        "name":  "src/flutter",
        // 6bc433c6b6b5b98dcf4cc11aff31cdee90849f32就是CommitID
        "url":  "[email protected]: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":  "[email protected]: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编译!
image.png
  • 构建
#构建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

你可能感兴趣的:(混合开发与Flutter引擎)