远程开发机上有个常见误判:文件在 iOS 模拟器前台上传成功,就认定用户切走应用后也不会丢任务。后台上传涉及磁盘文件、系统接管的传输任务和应用恢复时的回调,不能只看请求有没有发出去。下面用云端 Mac 上的本机接收端做第一轮验收,再列出必须留给真机的检查项。
先划定验收边界
这次测试的是 URLSessionConfiguration.background 创建的文件上传任务,不是把 Data 留在内存里等待网络恢复。准备一个体积可辨认、内容不含真实凭据的样本文件,记下字节数与 SHA-256。服务端至少要能记录请求方法、接收字节数和返回状态;客户端则分别记录任务标识、传输错误与 HTTP 状态码。
didCompleteWithError的错误为nil,只表示传输层没有报告错误;服务端仍可能返回 4xx 或 5xx。验收必须同时检查 HTTP 响应。
模拟器适合验证这条基本链路。它不能代替真机证明系统会在特定条件下重新唤起应用,也不能替代真实移动网络切换测试。
在 Mac 上启动最小接收端
将以下脚本保存为 receiver.py,在运行模拟器的同一台 Mac 上执行。它只监听回环地址,限制单次请求大小,并把收到的内容写到固定测试路径;不要把它当成对公网开放的上传服务。
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
LIMIT = 10 * 1024 * 1024
OUTPUT = Path("/tmp/ios-upload-received.bin")
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
if self.path != "/upload" or not 0 < length <= LIMIT:
self.send_error(400)
return
body = self.rfile.read(length)
if len(body) != length:
self.send_error(400)
return
OUTPUT.write_bytes(body)
self.send_response(201)
self.end_headers()
HTTPServer(("127.0.0.1", 8765), Receiver).serve_forever()
启动命令是 python3 receiver.py。测试应用访问 http://127.0.0.1:8765/upload 时,仅在测试构建中为 localhost 或所用回环主机配置限定范围的 App Transport Security 例外;发布构建仍应使用 HTTPS。若模拟器连不上,先确认服务进程仍在运行、端口一致,再检查例外配置,不要一上来修改上传逻辑。
从磁盘文件创建后台任务
下例展示任务建立的关键部分。调用者负责传入已经写完、当前进程可读的文件 URL;endpoint 由测试配置提供,不要把本机测试地址写死进发布代码。后台会话标识应稳定且属于这条上传流程,避免每次启动都生成新标识。
import Foundation
final class UploadCoordinator: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(
withIdentifier: "com.example.upload-check"
)
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func upload(file: URL, endpoint: URL) {
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
session.uploadTask(with: request, fromFile: file).resume()
}
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
let status = (task.response as? HTTPURLResponse)?.statusCode
let accepted = error == nil && status == 201
recordResult(taskID: task.taskIdentifier, accepted: accepted)
}
func urlSessionDidFinishEvents(
forBackgroundURLSession session: URLSession
) {
finishPendingBackgroundEvents()
}
private func recordResult(taskID: Int, accepted: Bool) {
// 将结果交给应用自己的持久化状态记录器
}
private func finishPendingBackgroundEvents() {
// 在应用完成状态更新后调用保存的系统完成处理器
}
}
示例中的两个私有方法是应用集成点,不是上传已完成的实现。实际接入时,持久化结果应保留任务与业务文件的对应关系;应用代理收到后台会话事件后,用同一个标识重建会话并接回代理,把系统给的完成处理器按标识保存。待事件处理及界面状态更新完毕,再调用该处理器。不要仅凭一条成功回调就立即删除尚未核实的业务文件。
分阶段验证,不用强制退出冒充后台
先让应用保持前台,提交一次样本文件。接收端返回 201 后,比较 /tmp/ios-upload-received.bin 与原文件的 SHA-256;两端一致,才说明本轮收到的是完整预期内容。然后换一个样本,提交任务后通过模拟器界面回到主屏幕,等待回调并再次核对文件。不要用命令强制终止进程来模拟普通的“切到后台”:用户主动强制退出与系统接管后的恢复不是同一种情形。
| 检查点 | 应记录的证据 |
|---|---|
| 创建任务 | 会话标识、任务标识、源文件路径与字节数 |
| 服务端接收 | HTTP 状态、实际字节数、文件 SHA-256 |
| 客户端完成 | 传输错误与 HTTP 状态分别记录 |
| 应用恢复 | 会话重建、回调到达和完成处理器调用顺序 |
真机验收还要单独覆盖锁屏、网络中断后恢复、网络类型切换,以及应用被系统终止后重新唤起。用户主动强制退出应另列测试,不要把它的结果当作普通后台行为。远程 Mac 上没有连接到身边设备的能力时,先把模拟器证据和待测清单交给持有真机的同事,不要在报告里写“真机已通过”。
常见失败与收尾检查
如果服务端没有收到请求,先查文件 URL 是否指向实际存在的普通文件,再查测试地址、端口和传输安全配置。如果文件到了但客户端标记失败,核对响应状态:示例只接受 201,不能把“没有传输错误”偷换成“服务端已接收”。若应用恢复后没有回调,检查是否用原标识重建后台会话、代理对象是否仍被持有,以及完成处理器是否过早调用。
最后把测试接收端关掉,删除 /tmp/ios-upload-received.bin,清理只用于测试的源文件和网络例外。正式流程还应定义失败重试上限、重复提交时的服务端处理方式,并在确认结果持久化之后清理文件。这样交接给下一位工程师的不是一句“后台上传能用”,而是可重跑的步骤、可核对的证据,以及尚未完成的真机项目。
常见问题
后台 URLSession 能直接上传内存中的 Data 吗?
这套后台上传流程应使用磁盘文件,通过 uploadTask(with:fromFile:) 创建任务;先把待传内容写入文件,并保证任务建立时文件可读。
模拟器上传成功,是否就证明应用退出后也能自动恢复?
不能。模拟器适合检查请求、响应和基础回调;系统重新唤起应用、网络切换及用户主动强制退出后的行为,还要用真机分别验证。
给下一次远程开发准备一台 Mac
按用途选机型与节点,使用屏幕共享、VNC 或 SSH 连接。价格与实际可用性以控制台实时返回为准。