在遠端開發機上,常見的誤判是:檔案在 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 連線。價格與實際供應狀況以控制台即時顯示為準。