在现有工程中引入 Flutter 页面

本文最后更新于 2021年4月4日 晚上

参考官方文档, 实践在原有 APP 中引入 Flutter 页面, 期间遇到了一些坑和问题, 故记录下来.

在现有工程中引入 Flutter 的基本流程探索

按照官方文档的流程走了一遍.

假设现有工程路径在 some/path/AddToApp.

下面就来引入一个 Flutter 页面.

不过遇到的问题是释放 Flutter VC 后内存没有被释放的问题, 在网上已经有人提到解决方案, 但没有被合并到 Flutter 的代码库(截止目前 1.1.8)中还.

新建 Flutter 模块

在工程文件夹的上层新建一个 Flutter 模块:

1
2
$ cd some/path/
$ flutter create -t module my_flutter

建立好后的文件夹结构如下所示:

1
2
3
4
5
6
7
8
9
some/path/
my_flutter/
lib/main.dart
.ios/
AddToApp/
AddToApp/
AppDelegate.h
AppDelegate.m (or swift)
:

引入 Flutter 模块

需要使用 CocoaPod 将 flutter 模块引入到目前的工程中, 故如果没有使用 cocoa pod 的工程, 还需要:

1
pod init

初始化 pod, 然后编辑 Podfile 文件:

1
2
flutter_application_path = 'path/to/flutter_app/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

由于我们这里的相对路径是知道的, 故可以写成如下的方式:

1
2
flutter_application_path = '../my_flutter/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

然后执行 pod install.

不出意外就建立好工程结构了.

(可以尝试将 Flutter 工程放到方便管理的地方, 不是必须放到同级, 这里演示目的故放到同级.)

每次修改 Flutter 的依赖, 都需要在 flutter 文件夹执行 flutter packages get, 且需要在原来工程的文件夹下执行 pod install.

新建一个 build phase 以构建 Dart 代码

在需要引入 Flutter 的 Target 中需要添加如下脚本来编译 Dart 代码:

1
2
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

保证这个脚本放到 Target Dependencies phase 之后就可以了.

之后就可以尝试进行编译运行了.

内部流程解析

详细的需要看文档, 这里只将粗略的步骤:

  1. Pod 将 Flutter.framework, 也就是引擎, 嵌入到原生 APP 工程中
  2. Pod 将 App.framework, 也就是用户的 Flutter 端代码, 嵌入到原生 APP 工程中
  3. flutter_assets 作为资源被引入到原生工程中.
  4. 任何 Flutter 端使用的插件都作为 Pod 被引入.
  5. 所有的 Target 的 bitcode 编译选项都被禁用, 保证可以链接 Flutter 引擎.
  6. 自动生成了一些包含和 Flutter 相关的环境变量的 xcconfig (包括 debug 和 release).

在原生代码中使用 Flutter

前面已经把 Flutter 端嵌入到原生 APP 中了, 下面就看如何在原生代码中使用:

  1. 修改 AppDelegate(没有继承其他类的情况下):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import Flutter
    import FlutterPluginRegistrant // 如果在 Flutter 端使用了插件, 则需要引入这个库.

    class AppDelegate: FlutterAppDelegate {

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 这个应该是插件功能相关的, 需要看源码明确内部的运作.
    GeneratedPluginRegistrant.register(with: self)

    return super.application(
    application,
    didFinishLaunchingWithOptions: launchOptions)
    }
    }

    如果运行的话会有如下 log 提示:

    1
    2
    3
    4

    2019-01-09 12:14:17.043743+0800 AddToApp[17990:123669] You've implemented -[<UIApplicationDelegate> application:performFetchWithCompletionHandler:], but you still need to add "fetch" to the list of your supported UIBackgroundModes in your Info.plist.
    2019-01-09 12:14:17.043952+0800 AddToApp[17990:123669] You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.
    2019-01-09 12:14:17.048696+0800 AddToApp[17990:123669] [VERBOSE-1:callback_cache.cc(132)] Could not parse callback cache, aborting restore

    针对每条进行处理即可.

    如果 AppDelegate 继承自其他类, 此时就无法继承 FlutterAppDelegate 了, 故可以使用第二种办法, 让 AppDelegate 实现 FlutterAppLifeCycleProvider 协议:

    1
       

    此外, 需要把 AppDelegate 中的所需生命期事件都交给代理去处理:

    1
       

    上述的类的实现或协议的内容都可以在 github Flutter 的 engine 工程中找到.

  2. 使用 FlutterViewController:

    假设设置一个按钮来弹出, 则在按钮动作上可以这样写:

    1
    2
    3
    4
    5
    6
    7
    @objc private func btnClickced() {
    let flutterVC = FlutterViewController()
    // 设置对应加载的初始 route, 这里指定的是 home.
    flutterVC.setInitialRoute("/")
    // 由于在 Flutter 中更多时候是内部提供导航栏, 故可以将这里修改为
    navigationController?.pushViewController(flutterVC, animated: true)
    }

    如果要在 flutter 代码中把这个 VC 移除, 则可以使用:

    1
    SystemNavigator.pop()	// 需要引入 'package:flutter/services.dart';

    这个方法实际是通过消息通道调用平台端的代码, 在平台端判断 window 的根控制器是导航控制器的话就用 pop, 如果根控制器不是导航控制器, 且根控制器不是 Flutter 视图控制器, 则调用 flutter 视图控制器的 dismiss.

调试

通过上面的步骤, 就可以开始调试了, 调试的时候可以使用 attach 来自动连接到有 flutter 视图控制器的界面上.

具体的命令行操作可以看文档.

如果在真机上调试的话, 需要将 APP 对应 Target 的 bitcode 也关闭了.

在移除的时候果然遇到内存不降低的情况呢?

切换到 release 配置再尝试一下:

切换了配置发现出现 plugin 模块找不到的错误, 需要重新执行: flutter packages getpod install.

内存果然没有降低.

在掘金上有篇文章, 就是在说这个问题, 并且目前的解决办法就只能是自己编译 engine 然后替换掉有 bug 的 engine.

另外闲鱼这篇文章有说这个问题的.

另外就是 SystemNavigator.pop 不工作的问题, 看了源码, 发现是如果 window 的根控制器是导航控制器的话, 就只会进行 pop 操作, 而不是去 dismiss 了.

故如果 window 的根控制器是导航控制器的话, 就只能将 Flutter VC push 上去. 如果是一般控制器的话, 就是 present 即可.

如果要在原生工程中内嵌 Flutter 并且使用其中的插件的话, 需要对这些插件注册, 注册的地方就是在 Pod :

Development Pods 内的 GeneratedPluginRegistrant.m 文件内将flutter工程中用到的插件都集中注册到原生, 可以这样写:

1
2
3
4
5
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FlutterWebviewPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterWebviewPlugin"]];
[FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]];
[FLTSharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTSharedPreferencesPlugin"]];
}

其中类似 FlutterWebviewPlugin 等都是在 Flutter 工程中引入的插件, 而且可以在这个 Pod 工程中访问到的, 因为脚本已经把这些内容都集中到了 Pod 里面.

将 Flutter 升级到 1.1.7 后, 但引擎不知道是否同步升级了? 如果是的话, 就看循环引用问题解决了没有?

目前还没有解决, 仍然是那样, 估计最终还需要引擎端的问题解决了才能搞定这个问题.


在现有工程中引入 Flutter 页面
https://blog.rayy.top/2019/01/12/2019-36-flutter-embedded/
作者
貘鸣
发布于
2019年1月12日
许可协议