Flutter 和平台对接的考虑
由于新产品的需要, 技术调查时, 需要做的事情:
- 如何将三方 vendor 的 dylib 封装到 framework 中然后通过 ffi 调用
- 分解这个问题:
- dylib 封装到 framework, 以便使用, 另外可以用中间层对接口进行部分封装
- flutter 中利用 ffi 和 c 互操
找到的一些参考:
- https://www.polpiella.dev/embedding-a-dylib-in-a-swift-package/
- https://itecnote.com/tecnote/xcode-how-to-create-a-working-framework-with-dylib-files-in-xcode-4/
- https://stackoverflow.com/questions/70673624/dyld-library-not-loaded-libabc-dylib-image-not-found
- https://bpoplauschi.github.io/2021/10/25/Advanced-static-vs-dynamic-libraries-and-frameworks.html
- https://medium.com/walmartglobaltech/ios-library-bundle-and-frameworks-8fdc0aac6952
- dart 的 C 互操: https://dart.dev/guides/libraries/c-interop
- https://stackoverflow.com/questions/67460070/flutter-desktop-windows-how-to-call-native-code-via-method-channel-make-api-c
- 官方: https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin-ffi
- dart 的 ffi 文档: https://dart.dev/guides/libraries/c-interop
- dart 2.7 的更新介绍(其中有 ffi 的): https://medium.com/dartlang/dart-2-17-b216bfc80c5d
针对 unlocker 的 oc 或 c# 代码, 苹果和 windows 均使用 methodchannel 会更好. ffi 的话还是会先将 OC header 转 C, 再生成 dart 侧定义... 因此在平台上用 method channel 直接进行封装, dart 侧有一层对接层即可.
另外在代码组织上选择使用 plugin 还是直接在平台层对接?
更好的方式是 plugin, 底层和 UI 互相的耦合可以降到最低. 底层单独修改, UI 也单独修改, 二者基于 Plugin 的接口通信. 并且不去做 FFI Plugin, 而是使用 channel 通信即可.
目前可以使用 cocopods 提供的关联 dylib 的能力, 可以尝试一下先.
另外 plugin 有 Federated plugins, 以这个为起点去做, 可以对不同平台包抽象统一的对 Flutter 接口: https://docs.flutter.dev/development/packages-and-plugins/developing-packages#plugin
https://docs.flutter.dev/development/platform-integration/platform-channels
使用命令行传入的编译配置: https://github.com/flutter/flutter/issues/64088#issuecomment-878542960
:
flutter build apk --dart-define=app.flavor=paid
const String flavor = String.fromEnvironment('app.flavor');
void main() {
print(flavor);
}
关于 flavor, 也可以和 appflowy 类似的方式进行: https://sebastien-arbogast.com/2022/05/02/multi-environment-flutter-projects-with-flavors/#Isolating_Your_Apps_Configuration
上述两种方法结合起来可以比较完美解决问题.
做法
- 创建一个 plugin
- 在 macOS 上通过 cocoapods 集成 dylib
- 处理对 外接口抽象
资料的阅读
包和插件开发
插件 API支持混合插件, 它可以把不同平台实现隔离, 开发者可以指定插件支持的平台类型, 比如仅支持 windows 和 macOS. 并且老的插件 API 被废弃了, 意味着新的插件类型都是混合的了.
什么是包?
可以通过包来创建易于共享的模块化代码. 一个包最少都有如下文件:
- pubspec.yaml: 包配置
- lib 目录: 包的对外代码
包的类型
有如下包类型可选:
- Dart 包: Dart 编写的通用包, 比如 path 包. 有些可能包含 Flutter 的特定功能, 因此会依赖 Flutter Framework, 这样就限制了它们只能用于 Flutter 项目, 比如 fluro 包.
- 插件包: 特殊的 Dart 包, 包含 Dart 编写的 API 以及一个或多个特定平台的实现代码. 插件包支持任何 Flutter 支持的平台, 并且可以限定支持的平台. 一个典型的例子是 url_launcher 插件.
- FFI 插件包: 特殊的 Dart 包, 包含 Dart 编写的 API 以及一个或多 个平台的特定实现(使用 Dart FFI).
包开发
开发的总体步骤是:
- 创建 package:
flutter create --template=package hello
- 实现包
插件包开发
如果包需要调用平台上的特定 API, 则需要使用插件包.
在插件包中和平台沟通使用 platform channels 中的一种:
- 二进制消息 channel: 二进制消息通道
- 消息 channel: 可以使用不同的消息编解码方式
- method channel: 类似方法调用那样"调用->返回"的消息通道
- event channel: 消息以类似流的方式传递
混合插件在模板生成时, 默认使用的是 Method Channel.
混合插件
这种类型的插件是新的插件存在形式, 替换掉了老的插件类型. 它支持将不同平台的实现分离到不同的包, 因此可以在不同平台上使用自定义的包, 然后提供统一的对 Flutter APP 的接口.
在混合插件中, 需要如下的包:
- 面向 APP 的接口包: 对外提供统一的 API, 在 Flutter APP 中使用.
- 平台包: 一个或多个的平台实现包, 包含特定平台的功能实现代码. 面向 APP 的接口包会调用这里面的代码, 在编译时只有对应 平台的包才被包含到 APP 中.
- 平台接口包: 用于提供 APP 接口包和平台包之间的胶水层, 在其中定义一套所有平台均需要实现的接口, 以便支持 APP 接口包的调用. 在使用时, 具体平台包只要对应实现了平台接口包中定义的接口, 就可以被自由替换.
可以在 APP 接口包的 pubspec.yaml
中指定支持的平台, 当添加更多平台的支持时, 需要对应更新.
平台包的 pubspec.yaml
格式类似, 只是在其中指定实现的包名字和平台对应的入口.
在 APP 接口包中可以指定默认的平台实现包依赖.
混合插件的实现
首先需要创建插件工程, 使用 flutter 的模板创建即可:
# 通过模板创建插件, 然后指定对应平台的实现语言
flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -a kotlin hello
flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -a java hello
flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -i objc hello
flutter create --org com.example --template=plugin --platforms=android,ios,linux,macos,windows -i swift hello
创建后的文件结构类似如下:
lib/hello.dart
: 插件的 APIandroid/src/main/java/com/example/hello/HelloPlugin.kt
: 安卓平台特定的 API 对应实现ios/Classes/HelloPlugin.m
: iOS 平台的 API 对应实现example/
: 示例, 插件作者可以在其中演示如何使用这个插件
默认情况下, 插件默认使用 Swift 或 Kotlin 对应 iOS 和 Android.
由于插件中会包含多种平台的实现, 因此需要进行一些特定步骤以保证开发的顺畅:
- 定义 Dart API
- 在 macOS/Windows 开始开发前需要编译一次:
flutter build macos
,flutter build windows
. - 如果是 macOS: 用 Xcode 打开
example/macos/Runner.xcworkspace
, 然后在Pods/Development Pods/hello/../../example/macos/Flutter/ephemeral/.symlinks/plugins/hello/macos/Classes
中进行开发. - 如果是 Windows: 用 VS 打开
example/build/windows/xx.sln
, 然后在hello_plugin/Source Files
或hello_plugin/Header Files
找到源码即可开始开发.
最后通过符合需求的 platform channel 来沟通, 模板中生成的是 method channel 沟通.
官方 FFI 文档
Flutter 移动端和桌面端都可以使用 dart:ffi
库来调用原生的 C API. FFI 即外部函数接口的简称, 还可以称为 "native interface" 和 "language bindings".
在 Dart 库或程序使用 FFI 来绑定本地代码前, 必须保证本地代码被加载并且它们的符号能够被 Dart 访问. 本文主要关注如何在 Flutter APP 或插件中编译, 打包, 加载 macOS 本地代码.
下面演示如何将 C/C++ 源码打包到 Flutter 插件中, 然后使用 Dart FFI 库在 macOS 系统中绑定它们.
动态 vs 静态绑定
本地库可以通过动态或静态链接的方式链接到 app 中. 静态链接库会被内嵌到 app 的可执行文件中, 在 app 启动后被加载到内存. 在静态链接库中的符号可以通过 DynamicLibrary.executable
或 DynamicLibrary.process
进行加载.
动态链接库则是在 APP 包中的独立文件, 且可以按需加载. 在 macOS 系统上动态链接库是通过 .framework
文件夹的形式进行分发, 动态链接库可以通过 DynamicLibrary.open
加载到 Dart 中.
dylib 可以直接在平台工程中加入, 然后使用 dart 的 load 来 load.
实际操作
- 实际操作时, 可以先用 demo 中的 dylib 验证.
- 之后再用 rust 的 fsevent 项目实际封装一次并准备实现一个简单的跨平台小工具.