PAYATHON 2026

如何在应用进入后台后保持 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 服务说明

基本流程是:

  1. 用户在前台发起生成。
  2. 启动服务,及时调用 startForeground() 并提供通知。
  3. 由服务管理本轮任务,调用可复用的流式处理模块。
  4. 任务完成、失败或被用户取消后,停止前台服务。

尽量在用户操作时启动服务。对于以 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 长时间任务文档

接入时需要:

  1. 配置允许的任务标识符并注册处理入口。
  2. 在用户明确发起操作时提交请求。
  3. 在任务入口中调用或接管现有业务逻辑,避免重复发起生成。
  4. 报告实际进度,处理取消和到期,并在完成时调用 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 上稳定交付完整回答,建议由服务端分别管理生成任务和客户端连接:

  1. 创建生成任务,返回 taskId。
  2. 前台通过 SSE 或 WebSocket 接收增量结果。
  3. 客户端断连后,服务端按约定继续生成并保存结果。
  4. 客户端恢复时,按任务状态拉取完整结果,或从事件游标继续接收。

taskId 和游标协议需要后端提供。SSE 的 Last-Event-ID 只有在服务端保存事件并支持重放时,才能用于补发。若上游 API 在连接断开时就取消生成,还需要后端代为维持上游请求,或使用上游明确支持的异步任务能力。这样,回答能否完成就不再取决于手机进程是否始终获得执行时间,手机端也无需为此引入独立 worker。

备注:内容仅供参考。