← 返回蜂巢洞察

移动设备在后台的执行机制:iOS的后台运行模式、Android的WorkManager以及Dart语言中的后台服务功能

每一位移动应用开发者最终都会遇到同样的问题:当用户正在使用应用程序时,它运行得非常正常。 但一旦用户按下主屏幕按钮,所有事情就会出问题。本应在后台完成的同步操作根本没有进行;应该发出的通知也没有出现;用户在应用程序中开始进行的文件上传,在他们离开应用程序的瞬间就无声无息地失败了。 在移动设备上实现后台执行功能,是整个移动开发领域中最容易被误解的话题之一。大多数开发者认为这只是一个简单的问题——只要让代码在后台持续运行即可。而操作系统则将这个问题视为一个直接影响电池寿命、性能以及设备整体运行的资源管理问题。 只有真正了解iOS和Android系统对后台任务的处理方式,再进一步理解Flutter是

每一位移动应用开发者最终都会遇到同样的问题:当用户正在使用应用程序时,它运行得非常正常。

但一旦用户按下主屏幕按钮,所有事情就会出问题。本应在后台完成的同步操作根本没有进行;应该发出的通知也没有出现;用户在应用程序中开始进行的文件上传,在他们离开应用程序的瞬间就无声无息地失败了。

在移动设备上实现后台执行功能,是整个移动开发领域中最容易被误解的话题之一。大多数开发者认为这只是一个简单的问题——只要让代码在后台持续运行即可。而操作系统则将这个问题视为一个直接影响电池寿命、性能以及设备整体运行的资源管理问题。

只有真正了解iOS和Android系统对后台任务的处理方式,再进一步理解Flutter是如何基于这两种系统进行开发的,才能区分那些与平台抗争的开发者与那些能够有效利用平台功能的开发者。

本文正是围绕这些内容展开的。你将了解到这两个操作系统上的原生后台执行机制,以及 Flutter框架是如何衔接这两种机制的,同时也会知道在什么情况下应该使用哪种方法。

目录

先决条件

本文是为那些正在为iOS、Android或Flutter平台开发或维护生产级移动应用的工程师们准备的。在阅读本文之前,你应当已经掌握以下基础知识:

  • 移动应用的基本生命周期:了解应用程序在前台、后台以及暂停状态之间如何切换

  • 移动开发的基本概念:你至少曾经在一个平台上使用过某种开发框架来开发或维护过移动应用

  • 使用任何一种开发框架编写移动应用的能力:无论是iOS原生应用、Android原生应用,还是Flutter、React Native等其他移动开发技术

  • 你所使用的编程语言中的多线程与并发处理机制:理解你的编程语言是如何处理在主线程之外运行的任务的,这是理解本文所有内容的基础

如果你完全是移动开发领域的新手,或者从未接触过多线程和并发执行的相关知识,那么请先学习这些基础知识,等到你准备好深入了解生产级后台执行功能时,再回来阅读这篇文章吧。

为什么后台执行如此困难

当你的应用程序处于前台时,平台会赋予它对CPU、网络和内存的完全访问权限。用户正在主动使用该应用,因此电池消耗在所难免,而这样的资源使用也是合理的。

但一旦应用程序进入后台,情况就会发生彻底的变化。设备上可能安装了数十款应用程序,如果它们都在后台自由运行,那么电池会在几小时内就被耗尽,CPU也会持续处于工作状态,内存也会被占用殆尽,最终导致设备变得发热且运行速度变慢。

iOS和Android早就明确了一点:后台执行是一种特权,而非一项权利。应用程序必须通过声明自身所需资源及使用原因,才能获得在后台运行的权限。随后,平台会决定如何、何时以及允许运行多长时间。

iOS和Android处理这一问题的方式有所不同,但随着时间的发展,它们逐渐趋向于采用相同的模式:即要求应用程序明确说明自己需要执行哪些后台任务,并对这些任务进行分类和限制,然后由平台来控制这些任务的执行过程。

Flutter是如何在后台运行的

在分别了解iOS和Android的处理方式之前,你需要先理解Flutter的工作原理。

Flutter会在一个名为“main isolate”的独立线程中执行Dart代码。这个线程与平台的主线程相连,负责处理所有的用户界面逻辑及业务逻辑。当应用程序进入后台时,这个“main isolate”线程可以随时被暂停。

当 Flutter需要在执行某些后台任务时,平台并不会唤醒“main isolate”线程,而是会启动一个独立的Dart线程来专门处理这些后台任务。这个后台线程与“main isolate”线程是完全隔离的。

实际意义是什么

这个后台线程无法访问应用程序的用户界面组件,也无法调用`setState`方法来更新用户界面,更不能使用任何UI相关的类或对象。它纯粹只是用于执行Dart代码的线程,没有任何与用户界面相关的功能。

此外,这个后台线程也不会与“main isolate”线程共享内存。任何需要在后台处理的数据都必须被保存到磁盘上(例如通过共享偏好设置、本地数据库等方式),然后由后台线程独立地读取这些数据。

最后,这个后台线程必须是一个顶级函数或静态方法,而不能是匿名函数或类实例上的方法。因为平台在唤醒应用程序时,需要能够通过名称来调用这个函数。

理解了这些原理后,你就会重新审视Flutter中的后台执行机制。实际上,你并不是在让整个应用程序持续运行,而是在注册一段独立的Dart代码,让平台能够在自己设定的时间、在自己的独立环境中来调用这段代码。

iOS的后台执行机制

iOS会对你的应用程序产生哪些影响

iOS是通过一系列明确定义的状态来管理你的应用程序的。

当用户按下主屏幕按钮时,你的应用会从前台切换到后台。iOS会给你大约5到10秒的时间来完成你正在进行的操作。之后,你的应用就会被暂停——这意味着它会在内存中被完全冻结,不会有任何Dart代码被执行,也不会有网络请求被发送。虽然应用仍然存在于RAM中,但实际上已经处于暂停状态。

如果iOS需要更多内存,它可以随时终止那些处于暂停状态的应用。当用户再次使用你的应用时,iOS会要么快速恢复它的运行,要么重新启动它。如果你的应用在暂停状态下被终止了,用户是不会察觉到的——应用会像正常一样重新启动。

要在iOS后台执行某些操作,你必须明确说明自己的意图。iOS列出了特定的后台功能,你的应用必须申请自己真正需要的那些功能。苹果会在应用提交到App Store之前审核这些申请。

BGTaskScheduler

BGTaskScheduler是iOS为调度后台任务而提供的现代API。它从iOS 13开始被引入,取代了那些老旧且不够可靠的旧方法。它提供了两种类型的任务:

BGAppRefreshTask用于执行周期性的后台任务。可以理解为iOS会让你应用短暂地“苏醒”,以便检查是否有新的内容并更新应用的状态。你大约有30秒的时间来执行这个任务。iOS会根据用户的使用习惯来决定何时执行它。例如,如果用户每天早上8点都会打开你的应用,iOS就会记住这个习惯,并尝试在8点之前执行刷新任务,这样用户打开应用时就能看到最新的内容。 BGProcessingTask则用于执行耗时较长、任务较重的操作,比如数据库迁移、处理大型文件或更新机器学习模型。你会有几分钟的时间来执行这些任务。不过,这些任务只有在设备连接电源且处于WiFi网络环境下才会被执行。虽然你有更多的时间,但无法保证任务具体会在什么时候开始运行。

iOS对这些规则的执行非常严格:

在应用发布之前,你必须将任务标识符声明在Info.plist文件中的BGTaskSchedulerPermittedIdentifiers字段里。如果没有进行这样的声明,无论你的代码如何编写,该任务都不会被执行。 你还必须在applicationDidFinishLaunching方法执行完毕之前注册你的任务处理程序。这个步骤甚至会在Flutter初始化之前完成。workmanager包会自动处理这部分工作,但了解其中的原因还是非常重要的。 每个任务在完成后都必须调用setTaskCompleted方法。如果你不执行这个操作,iOS就会将该任务标记为失败状态,并且以后会更加不愿意再安排类似的任务来执行它。 你总是应该为任务设置一个过期处理程序。如果iOS决定提前终止某个任务,它会先调用过期处理程序,这样你就会有短暂的时间来清理相关数据、保存应用状态,并将任务标记为未完成状态,以便后续重新安排任务执行。

iOS的后台运行模式

除了BGTaskScheduler之外,iOS还为某些类型的应用提供了专门的后台运行模式。这些模式需要在Info.plist文件中明确声明,这样才能让应用在特定情况下能够持续在后台运行。

只要应用程序正在播放音频,Audio和AirPlay功能就能确保该应用持续运行。用户在锁屏界面上可以看到当前正在播放的内容的控制按钮。播客应用、音乐应用以及具备语音导航功能的导航应用都会使用这一机制。iOS系统对这种模式非常支持,因为用户显然希望音频能够继续播放。

“位置更新”功能即使在应用程序处于后台状态时也能让设备持续获取GPS数据。这一功能分为两个等级:当用户的地理位置发生显著变化时,系统会利用基站数据来获取位置信息,这种方式对电池的消耗较小;而“连续位置更新”模式虽然能提供更精确的位置信息,但会消耗更多的电量。苹果公司在审核应用程序时会对这些后台定位功能进行严格审查,因此使用这些功能必须有正当理由。

“后台数据获取”是一种传统的机制,iOS系统会定期让应用程序在短时间内恢复运行以获取所需数据。与BGAppRefreshTask不同,这种机制使用的是较旧的API接口,因此大多数新应用应该优先选择BGTaskScheduler这一选项。

content-available标志支持的远程通知功能允许服务器触发应用程序在后台短暂恢复运行。当服务器发送无声推送通知时,iOS系统会唤醒应用程序来处理这些通知。正是通过这种方式,许多应用程序能够在不需要频繁轮询的情况下保持数据更新。

iOS系统的后台执行机制

iOS系统中,应用程序在后台的执行能力本质上取决于系统的信任。如果你明确说明了自己需要使用后台功能的目的,并且确实按照这些目的来使用这些功能,同时也遵守了相关的时间限制,那么苹果系统就会信任你的应用程序,允许其在后台运行。

如果超过了规定的时间限制,iOS系统会终止该应用程序的运行。如果你申请了实际上并不需要的后台功能,App Store在审核时肯定会指出这一点。如果你为了用户无法理解的目的在后台使用定位功能,你的应用程序也很可能会被拒绝审核。

苹果系统确实设有监控机制,会主动检测应用程序在后台的执行情况。那些运行时间过长、消耗过多CPU资源或行为异常的应用程序都会被终止。因此,在设计后台任务时,必须确保它们运行迅速、目标明确,并且不会占用过多的系统资源。

Android系统的后台执行机制

Android系统对应用程序的影响

Android系统通过层次结构来管理进程的优先级。处于前台的应用程序享有最高的优先级;那些正在运行服务的应用程序也会获得较高的优先级;而后台应用程序的优先级则较低;没有活跃组件的空进程或应用程序的优先级最低。

当系统需要释放内存时,会按照优先级的顺序终止进程,优先级最低的进程首先会被终止。因此,你的后台应用程序随时都可能被终止。不过,那些具有前台服务的应用程序要被终止就困难得多;实际上,处于前台状态的应用程序几乎永远不会被系统终止。

从历史上看,Android系统的政策比iOS系统更为宽松。早期的Android版本允许应用程序在后台无限期地运行服务,这种自由度曾被一些应用程序滥用——即使用户数周都没有与这些应用程序进行交互,它们仍然会持续运行,从而导致电池寿命大幅下降,因此Android系统后来不得不对此进行调整。

从Android 8.0 Oreo开始,谷歌开始限制应用程序在后台运行的功能。如果应用程序本身不是处于前台状态,那么它就无法启动后台服务。后续的每个Android版本都进一步收紧了这些限制措施。如今,Android系统正在逐渐向iOS系统的那种“明确声明、受到约束的后台工作模式”靠拢。

前台服务

在Android系统中,前台服务是实现后台运行的最可靠方式。这类服务会持续运行,并且必须显示持久性的通知信息。这种通知是必不可少的,因为它是Android向用户表明有某些操作正在进行的手段。用户可以看到这些通知,展开查看详细内容,也可以选择停止这些服务的运行。

音乐播放器会显示当前正在播放的歌曲以及相应的播放控制选项;导航应用会显示当前的行驶路线及预计到达时间;文件上传应用会展示进度条;健身应用则会显示已消耗的时间及当前的各项数据。

Android 14引入了多种类型的前台服务。现在,你必须明确说明自己正在运行哪种类型的前台服务。这些类型包括:媒体播放、位置信息处理、数据同步、摄像头操作、麦克风使用、电话通话、远程消息传递、短时服务、健康相关功能以及系统豁免类服务。Android之所以采取这种分类方式,其实是借鉴了iOS的操作模式。

当用户期望某些操作能够持续进行时,前台服务就是最佳选择。比如播放音乐、导航、上传文件等等——任何需要用户发起并希望其持续进行的操作,都适合使用前台服务。

WorkManager

WorkManager是谷歌推荐的用于处理可延迟且能得到保证的后台任务的解决方案。了解它的关键特性是非常重要的。

“能得到保证”意味着你的任务最终一定会被执行。即使应用程序退出了,或者用户重新启动设备,又或者系统终止了你的进程,WorkManager也会将任务保存在本地数据库中,并在条件允许时重新尝试执行它。这与那些一旦应用程序关闭就会消失的后台服务截然不同。

“可延迟”意味着你无法精确控制任务的具体执行时间。你需要设定一些约束条件,而WorkManager会等到这些条件得到满足后再执行任务。例如,这些约束条件可能包括需要网络连接、设备必须在充电中,或者电池电量不能过低等等。WorkManager会在这些约束条件下选择最合适的执行时机。

对于定期执行的任务,平台规定其最小间隔时间为15分钟。这是由操作系统强制规定的,并非WorkManager自行设定的。Android不允许应用程序设置更频繁的任务执行频率,以防止过度消耗电池电量。

在现代版本的Android系统中,WorkManager实际上是通过JobScheduler来管理这些任务的。它能够帮你处理与兼容性及各种约束条件相关的问题,从而简化任务管理的复杂性。

以下情况适合使用WorkManager:与服务器同步数据、上传日志或分析结果、处理下载下来的文件、清除旧的缓存文件、生成缩略图,或者发送排队的消息等等。

然而,对于那些需要立即执行、必须在特定时间完成,或者需要持续运行的任务来说,WorkManager就不是合适的选择。

深睡模式与应用待机状态

当设备未连接电源、处于静止状态,并且屏幕已经关闭了一段时间后,系统会进入深睡模式。在深睡模式下,Android会暂停网络连接,延迟执行WorkManager的任务,忽略唤醒锁的设置,也会推迟闹钟的触发。系统之所以采取这种机制,就是为了在设备明显没有被使用时节省电池电量。

系统会定期退出“Doze”状态,以便进入维护窗口,在这些窗口期间可以执行被延迟的任务。设备在“Doze”状态下停留的时间越长,这样的维护窗口出现的频率就会越低。

只有高优先级的Firebase Cloud Messaging通知才能突破“Doze”状态的限制。这就是为什么由服务器触发的后台更新功能如此有效:服务器会发送高优先级的FCM消息,即使设备处于“Doze”状态,Android系统也会唤醒应用程序来处理这些消息。

“应用待机分类”会根据用户最近使用该应用的频率来对应用进行分类。这些分类包括“活跃”、“常用”、“频繁使用”、“很少使用”以及“受限”。应用所属的分类会直接影响它被允许执行多少后台任务。

如果被归类为“活跃”,说明用户最近经常使用该应用,此时应用程序可以完全执行后台任务。

如果被归类为“常用”,说明用户会定期使用该应用,此时后台任务的执行频率会受到一定限制。

如果被归类为“频繁使用”,说明用户会偶尔使用该应用,但不是每天都会使用,因此后台任务的执行也会受到更多限制。

如果被归类为“很少使用”,说明用户几乎从不使用该应用,此时后台任务会被严格限制,WorkManager任务的执行也会被大幅延迟。

如果被归类为“受限”,说明该应用存在不良行为,或者用户几乎从不使用它,此时后台任务的执行速度会被严重降低。实际上,这是用户或系统在提醒你该应用存在问题。

如果你的应用被归类为“很少使用”或“受限”,那么后台同步功能就会变得不可靠。避免这种情况的方法很简单:开发一个真正能被用户经常使用的应用。

Flutter的实现方式

现在让我们来看看Flutter工程师是如何利用上述原生机制来实现后台任务的。

项目结构设置

在Flutter中,后台任务的执行需要Dart代码与原生平台之间的协同配合。虽然大多数相关工作都由相关包来处理,但在iOS和Android平台上仍有一些配置步骤需要完成,这样才能确保后台任务能够真正被执行。

workmanager

`workmanager`包是Flutter中用于处理可延迟后台任务的最常用解决方案。它封装了Android的WorkManager和iOS的BGTaskScheduler。

需要添加以下依赖项:

dependencies:
  workmanager: ^0.5.2

在Android平台上,除了添加这个依赖项外,不需要再进行其他配置。`WorkManager`是AndroidX的一部分,因此所有现代Android设备都支持它。

在iOS平台上,需要将你的任务标识符添加到Info.plist文件中:

<key>>BGTaskSchedulerPermittedIdentifiers<>/key>
<array>
  <>string>>com.yourapp.syncTask<>/string>
  com.yourappcleanupTask<>/string>
</array>

同时还需要在Xcode中启用“后台模式”功能,并开启“后台数据获取”和“后台处理”选项。

后台回调函数必须是一个顶级函数,不能放在类内部。

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    switch (taskName) {
      case 'syncUserData':
        await syncUserData(inputData);
        break;
      case 'cleanupOldFiles':
        await cleanupOldFiles();
        break;
      default:
        print('未知任务:$taskName');
    }
    return Future.value(true);
  });
}

@pragma('vm:entry-point')这一注解非常重要。如果没有它,Dart代码分析工具在生成最终版本时可能会删除这个函数,因为看起来这段代码并没有被Dart代码调用过。但实际上,平台是直接通过名称来调用这个函数的,并不是通过Dart代码,因此代码分析工具无法检测到这种调用关系。

如果任务返回true,则表示任务成功完成;而返回false则表示任务失败,需要重新尝试。

请在主函数中初始化Workmanager:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: kDebugMode,
  );

  runApp(const MyApp());
}

isInDebugMode: true时,系统会记录有关任务调度和执行的详细信息。在生产环境中,请关闭这个选项。

注册一个一次性任务:

Future〈void〉 scheduleDataSync() async {
  await Workmanager().registerOneOffTask(
    'syncUserData',
    initialDelay: const Duration(minutes: 5),
    constraints: Constraints(
      networkType: NetworkType.connected,
      requiresBatteryNotLow: true,
    ),
    inputData: {
      'userId': currentUser.id,
      'syncType': 'full',
    },
  );
}

这个任务只会执行一次,且至少要等待5分钟之后才会运行。只有当设备处于联网状态且电池电量充足时,这个任务才会被执行。inputData字典会作为参数传递给回调函数,在executeTask方法中可以通过inputData参数获取这些数据。

注册一个周期性任务:

Future〈void〉 schedulePeriodicSync() async {
  await Workmanager().registerPeriodicTask(
    'periodicSync',
    'syncUserData',
    frequency: const Duration(hours: 1),
    constraints: Constraints(
      networkType: NetworkType.connected,
    ),
  );
}

平台规定,周期性任务的最小执行间隔为15分钟。如果你设置的间隔时间小于15分钟,系统会自动将其调整为15分钟。在iOS系统中,BGTaskScheduler会根据设备的具体情况来决定实际的执行频率。

取消任务:

// 取消一个特定的任务
await Workmanager().cancelByUniqueName('periodicSync');

// 取消所有已注册的任务
await Workmanager().cancelAll();

flutter_background_service

虽然Workmanager非常适合用于那些可以延迟执行的任务,但有时候我们也需要一些能够持续运行的服务,比如健康监测工具、实时数据采集系统或者需要保持持久连接的程序。

为此,flutter-background_service提供了这样的功能。在Android系统中,这类服务会以前台服务的形式运行,并且会显示持久的通知;而在iOS系统中,则会利用后台模式来实现持续运行。

请添加相应的依赖项:

dependencies:
  flutter_background_service: ^5.0.5
  flutter_local_notifications: ^17.0.0

背景服务的入口点必须是一个顶级函数:

@pragma('vm:entry-point') void onStart(ServiceInstance service) async { DartPluginRegistrant.ensureInitialized(); if (service is AndroidServiceInstance) { service.on('setAsForeground').listen((event) { service.setAsForegroundService(); }); service.on('setAsBackground').listen((event) { service.setAsBackgroundService(); }); } service.on('stopService').listen((event) { service.stopSelf(); }); // 你的实际背景工作在这里执行 Timer-periodic(const Duration(seconds: 30), (timer) async { if (service is AndroidServiceInstance) { if (await service.isForegroundService()) { serviceforegroundNotificationInfo( title: '应用正在运行', content: '上次同步时间:${DateTime.now()}', ); } } // 执行实际任务 await performBackgroundSync(); // 如有需要,将数据发送到主线程 service.invoke('update', { 'lastSync': DateTime.now().toIso8601String(), }); }); }

初始化服务:

Future〈void〉 initializeBackgroundService() async { final service = FlutterBackgroundService(); const AndroidNotificationChannel channel = AndroidNotificationChannel( 'background_service', '背景服务', description: '此通道用于发送背景服务的通知', importance: Importance.low, ); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation〈AndroidFlutterLocalNotificationsPlugin〉>() ?.createNotificationChannel(channel); await service.configure( androidConfiguration: AndroidConfiguration( onStart: onStart, autoStart: true, isForegroundMode: true, notificationChannelId: 'background_service', initialNotificationTitle: '应用正在运行', initialNotificationContent: '背景同步功能已启用', foregroundServiceNotificationId: 888, ), iosConfiguration: IosConfiguration( autoStart: true, onForeground: onStart, onBackground: onIosBackground, ), ); await service.startService(); }

背景服务与用户界面之间的通信:

// 在你的 widget 中,监听来自背景服务的更新 class HomeScreen extends StatefulWidget { @override State〈HomeScreen〉 createState() => _HomeScreenState(); } class _HomeScreenState extends State〈HomeScreen〉 { String lastSync = '从未同步过'; @override void initState() { super.initState(); FlutterBackgroundService().on('update').listen((event) { setState(() { lastSynch = event?['lastSync'] ?? '未知'; }); }); } void stopService() { FlutterBackgroundService().invoke('stopService'); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Text('上次同步时间:$lastSynch'), ElevatedButton( onPressed: stopService, child: const Text('停止背景服务'), ), ], ), ); } }

invokeon方法在后台隔离进程与主隔离进程之间建立了双向通信通道。后台服务会通过这些方法触发带有数据的事件,而用户界面则会监听这些事件并据此进行相应的更新。

background_fetch

对于那些不需要使用WorkManager的复杂约束机制的简单周期性后台任务来说,background-fetch提供了更为简洁的API接口。

dependencies:
  backgroundFetch: ^1.2.1
void main() {
  runApp(const MyApp());
  BackgroundFetch.registerHeadlessTask/backgroundFetchHeadlessTask);
}

@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
  String taskId = task.taskId;
  bool isTimeout = task.timeout;

  if (isTimeout) {
    BackgroundFetch.finish(taskId);
    return;
  }

  await performQuickSync();
  BackgroundFetch.finish(taskId);
}

配置并启动使用方法如下:

Future<void> configureBackgroundFetch() async {
  await BackgroundFetch.configure(
    BackgroundFetchConfig(
      minimumFetchInterval: 15,
      stopOnTerminate: false,
      enableHeadless: true,
      requiresBatteryNotLow: false,
      requiresCharging: false,
      requiresStorageNotLow: false,
      requiresDeviceIdle: false,
      requiredNetworkType: NetworkType.ANY,
    ),
    (taskId) async {
      await performQuickSync();
      BackgroundFetch.finish(taskId);
    },
    (taskId) async {
      // 超时处理逻辑
      BackgroundFetchfinish(taskId);
    },
  );
}

必须调用BackgroundFetch.finish(taskId)这个方法。在iOS系统中,如果不执行这个操作,系统会认为该任务未能成功完成,这将会影响后续的任务调度;而在Android系统中,这个方法用于通知WorkManager任务已经完成。

在不同隔离进程之间持久化数据

由于后台隔离进程与主隔离进程不共享内存,因此数据必须被保存到磁盘上才能在两者之间进行传递。

对于简单的键值对数据,通常可以使用shared_preferences来存储;而对于结构化数据,则可以考虑使用sqflite或isar这样的本地数据库。

// 在后台隔离进程中写入数据
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    final prefs = await SharedPreferences.getInstance();

    // 从服务器获取新数据
    final newData = await fetchFromServer();

    // 将数据保存到本地,以便主隔离进程能够读取
    await prefs.setString('lastSyncData', jsonEncode(newData));
    await prefs.setString('lastSyncTime', DateTime.now().toIso8601String());

    return Future.value(true);
  });
}

// 在应用返回到前台时,在主隔离进程中读取数据
class HomeScreen extends StatefulWidget {
  @override
  State<HomeScreen>> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>> {
  @override
  void initState() {
    super.initState();
    loadLastSyncedData();
  }

  Future<void>> loadLastSyncedData() async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString('lastSyncData');
    final syncTime = prefs.getString('lastSyncTime');

    if (data != null) {
      setState(() {
        // 使用同步后的数据更新状态
      });
    }
  }
}

决策框架

针对特定的后台任务需求,以下是判断应采用哪种方法的具体依据:

如果用户希望某些功能持续运行,例如音乐播放、导航系统持续运行或文件上传,此时应使用`flutter_background_service`来创建前台服务。这种持久的通知机制不仅是一种必要功能,更是向用户清晰展示应用程序当前正在执行什么操作的有效方式。

如果某项任务需要在未来某个时刻完成,但不一定需要立即执行,比如数据同步、日志上传、文件处理或缓存清理等,那么可以使用`workmanager`。该工具能够确保任务得以执行,同时会遵守各种限制条件,并且即使在应用程序重启或设备重新启动后,这些任务也会继续进行。

如果某项任务的执行需要由服务器发送的信号来触发,此时应使用Firebase Cloud Messaging,并设置高优先级的静默通知。服务器发出信号后,iOS和Android系统会唤醒应用程序,然后后台服务会负责执行相应的任务。这种方式能够突破Android系统的“Doze模式”,同时也适用于iOS系统的静默推送机制。

如果某项任务只需要按照固定的时间间隔定期执行,并且对执行时间的要求并不严格,那么可以使用`background_fetch`这个较为简单的API来完成任务。当`workmanager`提供的复杂限制条件对你来说并非必需时,选择这个API会更加方便。

如果某项任务的执行时间需要被精确控制,那么你需要重新考虑是否真的有必要在后台执行这项任务。例如,如果用户设置了下午3点的提醒,此时使用本地通知才是正确的选择。无论应用程序是否处于后台状态,通知都会在约定的时间准确触发。

特别需要注意的是iOS系统:没有任何Flutter插件能够绕过苹果公司的限制规定。如果你注册了`BGAppRefreshTask`,那么由iOS系统来决定该任务何时执行;而如果你为`WorkManager`设置了各种限制条件,那么由Android系统来判断这些条件是否得到满足。平台拥有最终的决策权,你的职责就是清楚地说明自己的需求,在平台提供的时间窗口内正确地执行任务,并确保应用程序能够在后台任务执行时间晚于预期时依然能够正常运行。

结论

在移动设备上实现后台任务的执行,并不是Flutter或Dart语言本身的问题,而是操作系统层面的限制。Flutter只是运行在这些操作系统之上的一种开发框架而已。

iOS系统的设计本身就存在诸多限制。你需要明确指定自己需要执行的背景任务类型,苹果公司会评估这种使用场景是否合法;一旦被批准,平台才会为你提供有时间限制的执行窗口。如果超出了这个时间窗口,系统就会终止你的任务。

Android系统最初的设计较为开放,但随着每个版本的更新,其限制也越来越严格。前台服务能够确保任务的持续执行,并且会显示相应的通知信息;`WorkManager`则能保证任务在满足特定限制条件的情况下被延迟执行;而“Doze模式”和“应用待机机制”则会根据设备状态和用户行为来进一步限制各种后台任务的执行。

Flutter通过一系列与原生API对应的插件,解决了上述这些问题。`workmanager`能够确保两种平台上的任务都能被延迟执行;`flutter_background_service`适用于需要显示通知的前台任务;而`background_fetch`则提供了简单、高效的定期任务执行方式。

<那些在移动设备的后台任务处理方面取得成功的工程师,都是那些真正了解平台的工作原理,并根据这些限制来进行设计的专家。他们不会与平台对抗,而是明确自己需要什么功能,合理利用系统提供的资源,在不同的组件之间妥善保持状态的一致性,并确保自己的系统能够在后台任务被延迟或推迟执行时依然能够正常运行。

<这就是移动设备上后台任务实际执行的方式。

<在原生应用及混合应用开发中,了解后台进程的工作原理以及应用程序的生命周期,能够帮助你在选择适合特定任务的处理器时做出更加明智的设计决策。

<祝编码愉快!

相关文章

技术实践

如果你已经不再需要使用原有的定时任务调度工具了,应该该怎么办呢?

大多数开发者在开始自动化工作的过程中都会采取类似的方法。他们会编写一些脚本来完成某些有用的任务,比如从API中获取数据、调整一批图片的大小,或者发送报告邮件,然后将这些脚本安排在每天早上自动运行。 这样做之后,他们会在自己的crontab文件中添加相应的指令,这样就能感受到自己对自动化流程的控制力了。现在,在他们睡觉的时候,电脑会自动执行这些脚本。 有一段时间里,这样的安排确实足够用了。但后来就不再适用了。 也许某个备份脚本在凌晨3点悄悄地失败了,而你直到当天晚些时候需要使用该脚本时才发现了这个问题;又或者你编写了一个在终端环境中运行得非常顺利的脚本,但却发现在用crontab安排它执行时却会

阅读全文
技术实践

如何使用OpenTelemetry收集器将Prometheus提供的直方图数据转换为OTLP格式的数据

现代应用程序通常会通过 ` /metrics ` 端点以 Prometheus 格式公开各种指标数据。 在这些指标中,直方图尤其实用。它们能够显示数值落在不同区间的频率,比如 HTTP 请求的耗时、数据库查询的时间,或者队列处理的延迟等。 与简单的平均值不同,直方图能够呈现完整的图像:你可以清楚地看到有多少请求处理得很快,有多少请求处理得很慢,以及那些偶尔出现的异常值是如何影响用户体验的。 例如,在支付系统中,交易数量的突然增加可能会暴露出隐藏的瓶颈。虽然大多数请求会迅速完成,但那一小部分处理速度缓慢的交易却可能对整个系统产生连锁反应,导致重试次数增加、失败率上升,进而影响整体的吞吐量。直方图

阅读全文
技术实践

如何在SQL中使用子查询

每当你在SQL中看到一个查询嵌套在另一个查询内部时,这就是子查询。子查询也被称为内查询,而包含它的那个查询则被称为主查询或外查询。 子查询的作用是为主查询提供额外的数据,这些数据可以以派生列或派生表的形式出现,或者它们也可以用来过滤主查询返回的行。 对于刚开始学习SQL的初学者来说,子查询可能相当难以理解。本文将帮助大家简化这一概念,使其更易于理解。读完这篇文章后,你应该能够更加熟练地使用子查询来解决问题了。 目录 先决条件 子查询的工作原理 执行顺序 子查询的类型 非相关子查询 相关子查询 结论 先决条件: 子查询属于高级SQL概念,因此,必须牢固掌握SQL的基础知识,包括SELECT、FR

阅读全文
技术实践

npm 12版本已发布:由于注册表机制发生了变化,现在默认情况下会自动安装这些脚本。

npm 12带来了许多与安全性相关的变更,使得某些安装行为需要用户明确选择才会被执行。值得注意的是,默认情况下,脚本的执行是被禁用的,因此运行任何脚本——包括那些在构建过程中自动执行的脚本——都需要获得用户的明确许可。此次更新还限制了来自非官方注册源的代码的下载,从而解决了社区对于自动执行脚本可能带来的安全风险的担忧。 作者:丹尼尔·柯蒂斯

阅读全文