在云端 Mac 上验收 iOS 后台 URLSession 文件上传

在云端 Mac 上验收 iOS 后台 URLSession 文件上传

远程开发机上有个常见误判:文件在 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 连接。价格与实际可用性以控制台实时返回为准。

配置远程 Mac