如何在应用进入后台后保持 streaming API 通信持续运行?
可以申请后台执行,主进程仍受系统限制
普通 AI 聊天应用在 Android 和 iOS 上都没有通用开关,能让应用切到后台后,所有 streaming、timers 和 UI 相关逻辑继续按前台方式执行。
两端可采用的方案如下:
| 平台 | 可行方式 | 限制 |
|---|---|---|
| Android | 使用前台服务(Foreground Service)管理用户明确发起的持续任务 | 需要通知、适当的服务类型,并受启动条件、运行时限及系统资源限制 |
| iOS | 使用 beginBackgroundTask 申请短时收尾;iOS 26 起可评估 BGContinuedProcessingTask | 都需要处理结束或中断,不能保证永久维持连接 |
现有流式处理代码可以尽量复用。Android 的服务默认与应用运行在同一进程;iOS 申请后台执行时间,也不要求把业务迁移到独立进程。不过,依赖页面、providers 和 UI 操作的代码仍需调整生命周期管理,两端都无法保证这些代码在不作调整的情况下始终运行。
为什么进程还在,流式响应却停了?
进程还驻留在内存中时,系统也可能暂停或限制它的执行。iOS 应用进入 suspended 状态后,应用代码就无法继续正常执行,async、socket 和 timers 都不能绕过进程挂起。Android 会根据组件状态、进程重要性和电源策略限制后台活动,行为与 iOS 不完全相同。Apple 后台执行说明、Android 进程与线程说明
这里需要区分几个概念:
- 主进程:操作系统管理的进程。
- 主线程:通常负责平台 UI 和生命周期回调。
- Flutter 主 isolate:运行 Dart UI 及相关状态逻辑的执行环境。
- 后台执行资格:操作系统是否允许应用在后台继续做某项工作。
同一进程可以包含多个线程和多个 Dart isolate。创建 isolate 本身不会获得后台执行资格,不同 isolate 也不能直接共享原有 providers 中的对象。Flutter isolate 文档
Android:使用同进程前台服务,复用流式处理模块
1. 为正在进行的任务启动前台服务
用户发送问题后,如果希望切换应用时仍能接收这一轮回答,可以考虑用前台服务管理本轮任务。
Service 默认与应用其他组件运行在同一进程中。只要没有通过 android:process 等配置将它放到其他进程,就无需另建进程或引入跨进程通信。Android 服务说明
基本流程是:
- 用户在前台发起生成。
- 启动服务,及时调用
startForeground()并提供通知。 - 由服务管理本轮任务,调用可复用的流式处理模块。
- 任务完成、失败或被用户取消后,停止前台服务。
尽量在用户操作时启动服务。对于以 Android 12 或更高版本为目标的应用,从后台启动前台服务通常受到限制,只有特定情况例外。前台服务启动规则
2. 声明符合用途的服务类型
以 dataSync 为例,它适用于数据获取、上传下载等操作。具体功能是否适用,还要结合实际工作内容和发布要求判断,仅凭使用了网络无法确定。前台服务类型说明
<!-- AndroidManifest.xml 中的相关声明 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application>
<service
android:name=".ChatStreamService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
从可见的 Activity 中启动:
ContextCompat.startForegroundService(
this,
Intent(this, ChatStreamService::class.java)
)
在服务中提升为前台服务:
// 放在 Service 的启动流程中。
// notification 需要提前创建;Android 8.0 起还需要通知渠道。
ServiceCompat.startForeground(
this,
1001,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
}
)
接入时还需要实现通知、任务取消、错误处理和服务停止逻辑。服务回调默认运行在平台主线程,不要在其中执行阻塞网络读取。前台服务启动示例
3. Flutter 需要额外管理 engine 和状态生命周期
即使原生前台服务仍在运行,现有 Flutter engine、主 isolate 或页面中的 provider 也可能已经不可用。
要继续使用主 isolate 中的逻辑,需要检查:
- Flutter engine 是否仍存活,是否绑定于可能被销毁的 Activity。
- 流式订阅是否在页面
dispose、provider 自动释放或生命周期回调中被取消。 - 后台执行期间是否仍调用导航、弹窗或依赖
BuildContext的操作。 - 所用插件是否创建了另一个 engine 或 isolate。
如果插件使用后台 isolate,它就无法直接访问主 isolate 中原来的状态对象。具体行为取决于插件实现,支持前台服务并不保证原页面代码会一直执行。
前台服务仍受电源和网络限制。锁屏、Doze、网络切换及资源不足都需要单独处理,WakeLock 也不能保证 socket 永不断开。Doze 与 App Standby 文档
对于以 Android 15 或更高版本为目标的应用,dataSync 前台服务还有后台累计运行时限,需要处理 Service.onTimeout(int, int)。它适合管理有结束条件的任务,无法保证连接全天常驻。前台服务超时规则
iOS:分别处理短时收尾和较长任务
1. beginBackgroundTask 可以延长现有代码的执行时间
如果这一轮回答接近完成,可以使用:
UIApplication.shared.beginBackgroundTask(
withName: "FinishChatResponse",
expirationHandler: {
// 停止本地工作,并及时结束后台执行申请
}
)
这个 API 为当前应用申请额外执行时间,不会创建独立 worker,也不要求把流式逻辑迁移到其他进程。
可以用下面的类管理单次执行申请:
import UIKit
@MainActor
final class BackgroundExecutionWindow {
private var identifier: UIBackgroundTaskIdentifier = .invalid
func begin(onExpiration: @escaping () -> Void) {
guard identifier == .invalid else { return }
identifier = UIApplication.shared.beginBackgroundTask(
withName: "FinishChatResponse"
) { [weak self] in
onExpiration()
self?.end()
}
}
func end() {
guard identifier != .invalid else { return }
UIApplication.shared.endBackgroundTask(identifier)
identifier = .invalid
}
}
让应用级对象持有这个实例,在需要保护的操作开始前调用 begin,正常完成或失败时调用 end。到期后应迅速取消工作并保存必要状态,不能继续等待流式请求慢慢结束。
实际可用时间由系统决定,申请也可能返回 .invalid。不要假定后台一定能运行固定秒数,也不要反复调用它来尝试无限续期。延长后台执行时间
2. iOS 26 起,可评估 BGContinuedProcessingTask
iOS 26 引入的 BGContinuedProcessingTask 适用于用户主动发起、需要较长时间完成的任务。任务可以从前台开始,在用户切换应用后继续执行,也允许使用网络。Apple 长时间任务文档
接入时需要:
- 配置允许的任务标识符并注册处理入口。
- 在用户明确发起操作时提交请求。
- 在任务入口中调用或接管现有业务逻辑,避免重复发起生成。
- 报告实际进度,处理取消和到期,并在完成时调用
setTaskCompleted(success:)。
它不强制创建独立进程,但要求任务有明确目标,并向系统报告进度。系统会展示任务状态,用户可以取消,资源紧张时任务也可能被终止。BGContinuedProcessingTask 文档
对于 AI 流式回答,需要评估一轮回答的生成是否满足这些要求。如果输出长度未知,或任务长时间处于等待状态、缺少可报告的进度,就需要仔细设计任务状态,不能虚构完成百分比。这个 API 不能直接用于无限期维持聊天 socket,也不适用于旧版 iOS。
3. 后台传输和后台模式各有用途
URLSessionConfiguration.background(withIdentifier:) 适合由系统接管的上传下载。实际传输可以在应用挂起时继续,但原有 SSE 解析回调、WebSocket 消费逻辑或 Dart providers 不会因此持续执行。后台文件传输文档
音频、定位、通话等持续后台活动需要对应能力,也必须有真实的业务用途。普通文本聊天无法仅靠勾选这些模式获得通用后台执行权限。BGAppRefreshTask、BGProcessingTask 等机制由系统调度,同样不能保证已有流式连接在切到后台后始终不中断。Apple 后台任务说明
如何减少对现有代码的改动?
可以保留流式解析、消息合并和业务状态更新,将它们的生命周期从页面中独立出来:
平台执行管理
调用 ChatSession
接收流式事件
更新消息状态
保存恢复位置
页面 / providers
订阅 ChatSession
展示当前状态
ChatSession 可以继续位于原进程中;执行环境允许时,也可以继续位于原 Dart isolate 中。主要需要调整以下几处:
- 由应用级会话管理 socket 和订阅,避免它们随页面销毁。
- 业务事件先更新数据,导航、弹窗等操作留到前台处理。
- 回到前台后,从当前会话状态或持久化结果恢复页面。
- 明确区分用户取消、客户端断连和服务端任务失败。
如果现有代码必须持有页面对象,或必须等待 UI 操作才能继续,就需要拆开这部分依赖。申请后台执行资格后,这些生命周期问题仍需处理。
需要可靠完成回答时,服务端必须支持恢复
即使获得了后台执行时间,连接仍可能因网络变化、进程退出或用户停止任务而中断。要在 Android 和 iOS 上稳定交付完整回答,建议由服务端分别管理生成任务和客户端连接:
- 创建生成任务,返回
taskId。 - 前台通过 SSE 或 WebSocket 接收增量结果。
- 客户端断连后,服务端按约定继续生成并保存结果。
- 客户端恢复时,按任务状态拉取完整结果,或从事件游标继续接收。
taskId 和游标协议需要后端提供。SSE 的 Last-Event-ID 只有在服务端保存事件并支持重放时,才能用于补发。若上游 API 在连接断开时就取消生成,还需要后端代为维持上游请求,或使用上游明确支持的异步任务能力。这样,回答能否完成就不再取决于手机进程是否始终获得执行时间,手机端也无需为此引入独立 worker。
备注:内容仅供参考。
