Flutter 后台任务
Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。
移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 BootComplete、电池和充电,搜索 BT 或 WiFi 网络等。
在 Android 中,我们可以在应用程序实际关闭时运行一些后台任务!
首先定义一个 BootComplete 广播接收器,当手机启动后立即执行,然后使用 WorkManager 或 AlarmManager 调度后台任务,使用 Service 在后台执行代码。
当然,后台任务中有些需要用户权限,可能会在通知栏显示一个通知表明此应用程序在后台运行。只要用户知道并同意,这些任务就可以在后台运行。
在 iOS 中,后台任务有更严格的限制,但仍然有一些方法可以运行一些后台任务。
说到 Flutter 应用程序及后台任务需要澄清的是他们的执行是在对端平台!负责注册和管理后台任务(Worker,Alarm,Service,BroadcastReceiver 等)的逻辑是用原生代码编写的,例如 Kotlin 或 Swift。但是,我们都知道,Flutter 应用程序逻辑是在 Dart 端编写的,这些代码可以构建 UI,还可以管理持久性数据,用户管理,网络基础架构和令牌等等。
如果我们想在 Dart 和原生端之间共享数据,可以使用 Flutter 的 MethodChannel 和 EventChannel。
在 Flutter 中,MethodChannel 和 EventChannel 是可以从本地端发送和接收信息到 Dart 端的方式,它们被用于 Flutter 插件。
假设我们对 BootComplete、电池状态感兴趣,想在后台用 Dart 处理这些事件呢。
一般情况下当应用程序在前台时,通过 MethodChannel 和 EventChannel 在 Dart 侧和本机侧间通信很容易,但是如果想要从本机侧启动 Dart 并启动一个后台 isolate,该怎么办呢?
让我们找出来吧!
在继续下面文章之前,我强烈建议您熟悉 Flutter 插件及其创建方法,因为示例将基于 Flutter 插件实现,详见文档。
启动 Dart 引擎(来自后台)
当应用启动时,Flutter 的 main isolate(入口点)在主(main)函数中启动。幸运的是,似乎也可以从本地启动 Dart VM,并在后台 isolate(次入口点)中调用全局函数。
Dart VM 启动不仅可以从 main 入口启动,也可以是其他入口,比如后台 isolate 的全局函数
关键在于应用程序后台唤醒时,在本机端持有可用的该入口点(全局函数)引用标识符 — callbackRawHandle 。
ChatGPT 关于 Dart CallbackRawHandle 说法
在 Dart 中,“callback raw handle”是对 Dart 函数基本实现的引用,可以传递给原生平台的 API。
callbackRawHandle 允许您绕过 Dart VM 的一般的类型检查,直接从本地代码调用函数。当您需要将 Dart 函数作为回调传递给本地库时,这非常有用。callbackRawHandle 使用的场景是应用程序本地端调用 Dart 代码。
为了从本地后台运行 Dart 代码,需要执行几个步骤,在详细介绍代码前,我想用图表来展示它,然后解释它:
让我们来看看这个图表并解释每个部分,如您所见,有六个主要步骤:
- 在 Dart 中定义一个无参 callbackDispatcher 全局函数,它将作为一个次入口点在后台隔离中运行,并直接从本地端调用。
- 这部分也有三个步骤:
- 当应用程序首次启动时,将callbackDispatcher函数通过一个 api 的参数传递给插件
- 在插件中,使用 PluginUtils::toRawHandle 方法生成 callbackDispatcher的RawHandle,并通过 MethodChannel 将其转发到插件的本地端(2')。
上述过程在 Dart 侧。
- 将 RawHandle 值(一个长整数)保存在本地端的持久存储中,以便将来能够使用 — 2’’
long 值可以理解成 Dart 中的回调函数的内存地址,传给了本地端。
以上部分可以完成后,我们将RawHandle保存在持久存储中,当应用程序在后台醒来时,存储中 RawHandle 可用,并将用于直接从本地端调用callbackDispatcher。
当应用在后台唤醒时(例如:启动完成-后台进程初始化器),从持久化存储中获取 RawHandle。
在后台初始化FlutterEngine和FlutterLoader
5.通过 RawHandle 获取FlutterCallbackInfo
使用DartExecutor和callbackInfo(来自第 5 步)调用executeDartCallback。这样就可以调用在 Dart 侧的callbackDispatcher函数了。
当 callbackDispatcher 被调用时,你可以在插件中注册其他事件并在后台的 Dart 侧处理它们,或者使用其他插件!
原生插件中可以通过 Dart 侧函数句柄调用 Dart 侧代码,也可以通过句柄使用其他插件。
如上所述,callbackDispatcher 只是 Dart 后台隔离的入口点。
让我们将上面的步骤分解为代码示例:
在 main.dart 中创建 callbackDispatcher 回调分发器
在上面的代码片段中,在 main.dart 中创建了appCallbackDispatcher 无参全局函数,它将成为 Dart 端的次入口点,可直接在本地调用,并在后台隔离中运行。
理解:一个全局函数,运行在后台线程中。
注意 @pragma('vm:entry-point') 注释是必须的,因为这个函数在 Dart 侧没有调用(它直接从本地调用),所以 AOT tree-shaking 编译器在生产构建时可能会将其删除。这个注释可以防止编译器删除这个函数。
让我们转到插件侧看看它的样子:
在插件 Dart 代码中获取 RawHandle
在上面的代码示例中,我们可以看到一个经典的 Flutter 插件 Dart 端。这里感兴趣的是registerCallbackDispatcher API,它是从应用程序的main()函数中使用 callbackDispatcher作为参数调用的 API。然后,在第 13 到 15 行,使用PluginUtilities和 toRawHandle()方法获取其RawHandle。
然后,在第 17 行,使用 methodChannel 将其转发到本地端。在图表中,这一部分对应于步骤 2 和 2'。
将 RawHandle 保存到持久性存储中(本地端)
让我们切换到插件本机端,看看它如何处理 registerCallbackDispatcher api
上面的代码示例分为两个部分:
- 在第一部分中,我们看到了 MyPlugin.kt 文件,使用 Kotlin 编写的本机插件。我们对“registerCallbackDispatcher”api 感兴趣,它是从 Dart 端调用的,在第 18 行,获得了作为参数传递的 dispatcherHandle。在第 21 行将其保存在一个 SharedPreference 持久存储中。
- 第二部分只是一个辅助类,用于保存和读取SharedPreferences中的数据。
这个解释是针对我们图表中的 2”。
从后台启动 Dart 引擎
这就是故事的核心部分,我们想从后台启动 Dart 引擎和 VM,但不启动主隔离和 UI 部分。 如图 3 中所示,它说的是后台进程初始化器。 为简单起见,我选择了一个 BootComplete BroadcastReceiver,在手机重新启动时启动 Dart VM,但取决于您的应用程序要求,您可以决定何时启动 Dart VM 的正确时机:
在上面的代码中,我们看到一个典型的 BroadcastReceiver,它在手机完成启动时调用。从 onReceive 中,我们开始并调用我们的 dart 回调分派器,分为两个主要步骤(图中的 4 和 5)。
- initializeFlutterEngine method:
- 创建一个 FlutterLoader 对象并检查其是否已初始化
- 在第 19-20 行开始并等待初始化完成
- 获取应用程序的BundlePath,即应用程序的根路径
- executeDartCallback:
- 在第 30 行创建 FlutterEngine 对象
- 接下来在第 31 行,获取我们之前在 SharedPreferences 中保存的**callbackDispatcher**句柄。检查句柄是否有效,然后使用 RawHandle 作为参数获取CallbackInfo(第 34 行)
- 一旦我们有了callbackInfo,我们就使用 DartEngine.dartExecutor 在 Dart 端调用 callbackDispatcher 回调函数!图中的第 5 部分。
这将直接从本地代码在后台调用 Dart 侧的callbackDispatcher!
总之,一旦手机重新启动,它将在后台启动 Dart 引擎。
如前所述,callbackDispatcher只是类似于 main()函数的辅助入口。一旦启动,Dart API 和第三方插件就会可用,因此我们可以在后台隔离中运行任何 Dart 逻辑或与其他插件交互,而 UI 部分则处于停止状态!
例如,我们自己的插件可以提供一个 EventChannel,为我们选择的任何事件提供事件流,此事件流可以在 callbackDispatcher 中被监听,并在 Dart 端后台获取事件。
需要说明的是,以下部分与上述背景隔离理论无关,这只是一个普通的插件功能,提供 Dart API 以从本地端发送和获取消息。
唯一的区别是一旦它在后台被调用,我们可以从回调调度程序与其交互。
让我们看一些代码,然后我会解释它
上面的代码分为三个部分:
- 第一部分是插件 API,在代码最后提供了一个 API 来监听通过 EventChannel 传递的消息,还有其他 API,例如启动监视设备充电器和电池状态。这些事件将通过 EventChannel 发送回来。
- 第二部分是插件本地端,在第 14 和 15 行,设置专门类的 StreamHandler。
- 最后是 PluginEventEmitter 类,这是将消息发送到 Dart 端的类。
在 PluginEventEmitter 类的最后,定义了一个密封类,用于发送到 dart 的事件,在这个例子中有两个事件:BootComplete 和 BatteryLevelStatus
PluginEventEmitter 还会缓存事件,直到 dart 侧在 EventChannel 上有监听。
看看如何在 callbackDispatcher 中使用它:
在回调调度程序中(在启动完成后从本地调用),我们现在注册到自己的插件事件,然后调用startPowerChangesListener并在侦听器中捕获事件。
所以,当我们重启手机时,callbackDispatcher 将被调用,并且所有这些将在后台运行!只要进程是活动的(这是另一篇文章的主题..),事件将继续在后台传递给监听器!
示例项目源代码
请参考我的github上的示例项目,其中包含完整的源代码!
这种方式有它的缺点,需要至少打开一次应用程序以注册 callbackRawHandle 回调函数。
我必须说,在开始时,我仍然发现这种方式不是最容易理解和实现的(隐涩难懂),我希望在未来,Flutter 团队能够提出更容易的解决方案。
链接:https://juejin.cn/post/7223775785207414821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。