likes
comments
collection
share

Flutter 后台任务

作者站长头像
站长
· 阅读数 12

原文地址: Flutter Background Tasks


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 代码,需要执行几个步骤,在详细介绍代码前,我想用图表来展示它,然后解释它:

Flutter 后台任务

让我们来看看这个图表并解释每个部分,如您所见,有六个主要步骤:

  1. 在 Dart 中定义一个无参 callbackDispatcher 全局函数,它将作为一个次入口点在后台隔离中运行,并直接从本地端调用。
  2. 这部分也有三个步骤:
  • 当应用程序首次启动时,将callbackDispatcher函数通过一个 api 的参数传递给插件
  • 在插件中,使用 PluginUtils::toRawHandle 方法生成 callbackDispatcherRawHandle,并通过 MethodChannel 将其转发到插件的本地端(2')。

上述过程在 Dart 侧。

  • RawHandle 值(一个长整数)保存在本地端的持久存储中,以便将来能够使用 — 2’’

long 值可以理解成 Dart 中的回调函数的内存地址,传给了本地端。

以上部分可以完成后,我们将RawHandle保存在持久存储中,当应用程序在后台醒来时,存储中 RawHandle 可用,并将用于直接从本地端调用callbackDispatcher

  1. 当应用在后台唤醒时(例如:启动完成-后台进程初始化器),从持久化存储中获取 RawHandle。

  2. 在后台初始化FlutterEngineFlutterLoader

5.通过 RawHandle 获取FlutterCallbackInfo

  1. 使用DartExecutorcallbackInfo(来自第 5 步)调用executeDartCallback。这样就可以调用在 Dart 侧的callbackDispatcher函数了。

  2. callbackDispatcher 被调用时,你可以在插件中注册其他事件并在后台的 Dart 侧处理它们,或者使用其他插件!

原生插件中可以通过 Dart 侧函数句柄调用 Dart 侧代码,也可以通过句柄使用其他插件。

如上所述,callbackDispatcher 只是 Dart 后台隔离的入口点。

让我们将上面的步骤分解为代码示例:

在 main.dart 中创建 callbackDispatcher 回调分发器

Flutter 后台任务

在上面的代码片段中,在 main.dart 中创建了appCallbackDispatcher 无参全局函数,它将成为 Dart 端的次入口点,可直接在本地调用,并在后台隔离中运行。

理解:一个全局函数,运行在后台线程中。

注意 @pragma('vm:entry-point') 注释是必须的,因为这个函数在 Dart 侧没有调用(它直接从本地调用),所以 AOT tree-shaking 编译器在生产构建时可能会将其删除。这个注释可以防止编译器删除这个函数。

让我们转到插件侧看看它的样子:

在插件 Dart 代码中获取 RawHandle

Flutter 后台任务

在上面的代码示例中,我们可以看到一个经典的 Flutter 插件 Dart 端。这里感兴趣的是registerCallbackDispatcher API,它是从应用程序的main()函数中使用 callbackDispatcher作为参数调用的 API。然后,在第 13 到 15 行,使用PluginUtilities和 toRawHandle()方法获取其RawHandle

然后,在第 17 行,使用 methodChannel 将其转发到本地端。在图表中,这一部分对应于步骤 2 和 2'。

将 RawHandle 保存到持久性存储中(本地端)

让我们切换到插件本机端,看看它如何处理 registerCallbackDispatcher api

上面的代码示例分为两个部分:

Flutter 后台任务

Flutter 后台任务

  1. 在第一部分中,我们看到了 MyPlugin.kt 文件,使用 Kotlin 编写的本机插件。我们对“registerCallbackDispatcher”api 感兴趣,它是从 Dart 端调用的,在第 18 行,获得了作为参数传递的 dispatcherHandle。在第 21 行将其保存在一个 SharedPreference 持久存储中。
  2. 第二部分只是一个辅助类,用于保存和读取SharedPreferences中的数据。

这个解释是针对我们图表中的 2”。

从后台启动 Dart 引擎

这就是故事的核心部分,我们想从后台启动 Dart 引擎和 VM,但不启动主隔离和 UI 部分。 如图 3 中所示,它说的是后台进程初始化器。 为简单起见,我选择了一个 BootComplete BroadcastReceiver,在手机重新启动时启动 Dart VM,但取决于您的应用程序要求,您可以决定何时启动 Dart VM 的正确时机:

Flutter 后台任务

在上面的代码中,我们看到一个典型的 BroadcastReceiver,它在手机完成启动时调用。从 onReceive 中,我们开始并调用我们的 dart 回调分派器,分为两个主要步骤(图中的 4 和 5)。

  1. initializeFlutterEngine method:
  • 创建一个 FlutterLoader 对象并检查其是否已初始化
  • 在第 19-20 行开始并等待初始化完成
  • 获取应用程序的BundlePath,即应用程序的根路径
  1. 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 以从本地端发送和获取消息。

唯一的区别是一旦它在后台被调用,我们可以从回调调度程序与其交互。

让我们看一些代码,然后我会解释它

Flutter 后台任务

Flutter 后台任务

Flutter 后台任务

上面的代码分为三个部分:

  1. 第一部分是插件 API,在代码最后提供了一个 API 来监听通过 EventChannel 传递的消息,还有其他 API,例如启动监视设备充电器和电池状态。这些事件将通过 EventChannel 发送回来。
  2. 第二部分是插件本地端,在第 14 和 15 行,设置专门类的 StreamHandler。
  3. 最后是 PluginEventEmitter 类,这是将消息发送到 Dart 端的类。

在 PluginEventEmitter 类的最后,定义了一个密封类,用于发送到 dart 的事件,在这个例子中有两个事件:BootComplete 和 BatteryLevelStatus

PluginEventEmitter 还会缓存事件,直到 dart 侧在 EventChannel 上有监听。

看看如何在 callbackDispatcher 中使用它:

Flutter 后台任务

在回调调度程序中(在启动完成后从本地调用),我们现在注册到自己的插件事件,然后调用startPowerChangesListener并在侦听器中捕获事件。

所以,当我们重启手机时,callbackDispatcher 将被调用,并且所有这些将在后台运行!只要进程是活动的(这是另一篇文章的主题..),事件将继续在后台传递给监听器!

示例项目源代码

请参考我的github上的示例项目,其中包含完整的源代码!

这种方式有它的缺点,需要至少打开一次应用程序以注册 callbackRawHandle 回调函数。

我必须说,在开始时,我仍然发现这种方式不是最容易理解和实现的(隐涩难懂),我希望在未来,Flutter 团队能够提出更容易的解决方案。


太棒了!鼓励自己坚持到底。我希望我为你投入的时间增加了一些价值。

本文原创 听蝉,如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。