feat(mobile): ios background upload bridge (compile-gated at G3)
Swift 侧(协议与 Android UploadPlugin.kt 逐字一致,编译/真机留 G3): - UploadBridge.swift:通道/方法/事件/失败码常量 + registerTask 参数解析 (镜像 UploadProtocol.kt) - UploadEventBus.swift:单调 seq + 终态 ack 缓存重放(镜像 UploadEventBus.kt),iOS 增强——未 ack 终态落盘(系统后台拉起投递的 回调在进程重启后同样重放,技术方案 §6.2 持久化原生回调) - UploadTaskStore.swift:taskId→URLSession taskIdentifier 映射 plist 持久化 (Application Support;对应 Android WorkManager 自持映射) - UploadPlugin.swift:URLSession background (com.photolive.photo_live.uploads)+ delegate 失败分类(2xx→completed+etag / 403→needsRenew / 4xx→HTTP_4XX / 网络→重试上限后 NETWORK,镜像 UploadPutExecutor/UploadWorker)、原生重试(5 次上限、10s 指数退避)、 进度 500ms 节流、queryTask/cancelTask/ackEvents、静默取消 - AppDelegate.swift:implicit engine 通道注册 + 启动重建会话 + handleEventsForBackgroundURLSession completionHandler 暂存 - Info.plist:UIBackgroundModes 有意不声明(background URLSession 无需; 计划措辞按技术方案 §6.2 修正,见 README-ios.md) - project.pbxproj 登记新 Swift 文件;README-ios.md(Xcode 步骤 + G3 验证 清单:实际文件上传/持久化回调窗口/服务端补偿/重启重联校准/强制退出/ 低电量延迟/100 张批量) Dart:NativeTransport 选择矩阵并入 iOS(Platform.isAndroid || isIOS, FORCE_DART_TRANSPORT 覆盖不变)——协议两侧一致,无新通道代码。 flutter analyze 0 issue;flutter test 219 全绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
import Flutter
|
||||
import Foundation
|
||||
|
||||
/// 上传桥接插件(P3 计划 Task 8;协议与 Android `UploadPlugin.kt` 逐字对齐)。
|
||||
///
|
||||
/// - MethodChannel `photolive/uploads`:registerTask(入队后台 URLSession
|
||||
/// upload task)/ cancelTask / queryTask / ackEvents;
|
||||
/// - EventChannel `photolive/uploads/events`:[UploadEventBus] 终态/进度
|
||||
/// 事件(终态未 ack 缓存重放 + **跨进程落盘**,见总线文档);
|
||||
/// - 实现 = URLSession background(identifier `com.photolive.photo_live.uploads`):
|
||||
/// 传输由系统调度,App 退后台/锁屏后继续;完成后系统在后台拉起 App 投递
|
||||
/// 回调(`handleEventsForBackgroundURLSession` → 重建会话重挂 delegate)。
|
||||
///
|
||||
/// **进程死亡语义**:iOS 对后台会话事件会在需要时重新拉起 App(区别于
|
||||
/// Android WorkManager 自持重跑)——本插件启动即重建会话(`ensureSession`),
|
||||
/// delegate 回调照常入总线;终态已落盘,Dart 引擎下次 onListen 时重放,
|
||||
/// 镜像 Android 的缓存重放语义。用户强制退出后系统不再代跑,重新启动时
|
||||
/// 由重联 + 重放校准(技术方案 §6.2)。
|
||||
///
|
||||
/// 失败分类(didCompleteWithError,镜像 Kotlin `UploadPutExecutor`):
|
||||
/// 2xx → completed(带 ETag);403 → needsRenew;其余 4xx → failed
|
||||
/// HTTP_4XX;5xx / NSURLErrorDomain 网络错误 → 原生侧重试(上限
|
||||
/// [maxWorkAttempts],指数退避初始 10s——镜像 WorkManager 语义;App 挂起
|
||||
/// 时退避定时器暂停,唤醒后续跑),耗尽后 failed SERVER_ERROR / NETWORK;
|
||||
/// 源文件缺失(注册时预检)→ failed SOURCE_MISSING。
|
||||
final class UploadPlugin: NSObject {
|
||||
|
||||
static let shared = UploadPlugin()
|
||||
|
||||
/// 传输尝试上限(含首发;对齐 Kotlin UploadWorker.MAX_WORK_ATTEMPTS)。
|
||||
static let maxWorkAttempts = 5
|
||||
|
||||
/// 重试退避初始间隔(秒;指数翻倍,对齐 Android WorkManager 10s 初始)。
|
||||
static let retryBackoffBaseSeconds: Double = 10
|
||||
|
||||
/// 进度事件节流(秒;Android 侧每 64KB 块发事件、通知 500ms 节流——
|
||||
/// iOS 统一在事件层 500ms 节流,终值(sent==total)必发)。
|
||||
static let progressThrottleSeconds: Double = 0.5
|
||||
|
||||
private let lock = NSLock()
|
||||
private let bus = UploadEventBus.shared
|
||||
let store = UploadTaskStore()
|
||||
|
||||
private var session: URLSession?
|
||||
private var sessionDelegate: UploadSessionDelegate?
|
||||
|
||||
/// 运行期内存态(锁内):活动系统任务 / 进度快照 / 静默取消标记 /
|
||||
/// 进度节流时间戳 / 系统后台回调。
|
||||
private var activeTasks: [Int: URLSessionTask] = [:]
|
||||
private var lastProgress: [String: (sent: Int64, total: Int64)] = [:]
|
||||
private var quietCancelIdentifiers: Set<Int> = []
|
||||
private var progressEmittedAt: [String: Double] = [:]
|
||||
private var backgroundCompletionHandler: (() -> Void)?
|
||||
|
||||
// ── 通道注册(AppDelegate 调用) ─────────────────────────────────
|
||||
|
||||
static func register(with messenger: FlutterBinaryMessenger) {
|
||||
FlutterMethodChannel(
|
||||
name: UploadBridge.methodChannel,
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
.setMethodCallHandler { call, result in
|
||||
UploadPlugin.shared.handleMethodCall(call, result: result)
|
||||
}
|
||||
FlutterEventChannel(
|
||||
name: UploadBridge.eventChannel,
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
.setStreamHandler(UploadEventStreamHandler())
|
||||
}
|
||||
|
||||
/// 后台会话重建(启动 / 系统后台拉起时调用;幂等)。
|
||||
func ensureSession() {
|
||||
lock.lock()
|
||||
guard session == nil else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
let config = URLSessionConfiguration.background(
|
||||
withIdentifier: UploadBridge.sessionIdentifier
|
||||
)
|
||||
config.isDiscretionary = false
|
||||
config.sessionSendsLaunchEvents = true
|
||||
// 无网时挂起等待而非立即失败(镜像 Android NetworkType.CONNECTED 约束)。
|
||||
config.waitsForConnectivity = true
|
||||
let delegate = UploadSessionDelegate(plugin: self)
|
||||
sessionDelegate = delegate
|
||||
session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
/// 系统后台拉起回调暂存(urlSessionDidFinishEvents 时消费)。
|
||||
func storeBackgroundCompletionHandler(_ handler: @escaping () -> Void) {
|
||||
lock.lock()
|
||||
backgroundCompletionHandler = handler
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func finishBackgroundEvents() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.lock.lock()
|
||||
let handler = self.backgroundCompletionHandler
|
||||
self.backgroundCompletionHandler = nil
|
||||
self.lock.unlock()
|
||||
handler?()
|
||||
}
|
||||
}
|
||||
|
||||
// ── MethodChannel(镜像 Kotlin handleMethodCall) ────────────────
|
||||
|
||||
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case UploadBridge.methodRegister:
|
||||
handleRegister(call, result: result)
|
||||
case UploadBridge.methodCancel:
|
||||
handleCancel(call, result: result)
|
||||
case UploadBridge.methodQuery:
|
||||
handleQuery(call, result: result)
|
||||
case UploadBridge.methodAck:
|
||||
handleAck(call, result: result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRegister(_ call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let raw = call.arguments as? [String: Any] else {
|
||||
result(FlutterError(code: "bad_args", message: "arguments must be a Map", details: nil))
|
||||
return
|
||||
}
|
||||
do {
|
||||
let spec = try parseRegisterSpec(raw)
|
||||
// 重注册(renew 换 URL / 崩溃恢复重驱):作废旧缓存终态 +
|
||||
// REPLACE 旧系统任务(防同任务双跑;镜像 Kotlin unique work REPLACE)。
|
||||
bus.purgeTask(taskId: spec.taskId)
|
||||
try startTask(spec: spec)
|
||||
result(true)
|
||||
} catch let error as UploadBridgeBadArgsError {
|
||||
result(FlutterError(code: "bad_args", message: error.message, details: nil))
|
||||
} catch {
|
||||
result(FlutterError(code: "enqueue_failed", message: "\(error)", details: nil))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCancel(_ call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let taskId = taskChannelArg(call, key: "taskId") as? String else {
|
||||
result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil))
|
||||
return
|
||||
}
|
||||
cancelQuietly(taskId: taskId)
|
||||
bus.purgeTask(taskId: taskId)
|
||||
result(true)
|
||||
}
|
||||
|
||||
private func handleQuery(_ call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let taskId = taskChannelArg(call, key: "taskId") as? String else {
|
||||
result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil))
|
||||
return
|
||||
}
|
||||
guard let entry = store.entry(taskId: taskId) else {
|
||||
// 无映射(未注册 / 已终态清理):镜像 Kotlin 无 WorkInfo 的 null。
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
let task = activeTasks[entry.sessionTaskIdentifier]
|
||||
let progress = lastProgress[taskId]
|
||||
lock.unlock()
|
||||
let state: String
|
||||
if let task = task {
|
||||
switch task.state {
|
||||
case .running: state = "RUNNING"
|
||||
case .suspended: state = "ENQUEUED"
|
||||
case .canceling: state = "CANCELLED"
|
||||
case .completed: state = "SUCCEEDED"
|
||||
@unknown default: state = "ENQUEUED"
|
||||
}
|
||||
} else {
|
||||
// 未重挂(进程重启后):系统仍持有任务,等同等待系统调度。
|
||||
state = "ENQUEUED"
|
||||
}
|
||||
result([
|
||||
"state": state,
|
||||
"attempt": entry.executions,
|
||||
"progress": progress.map { ["sent": $0.sent, "total": $0.total] },
|
||||
])
|
||||
}
|
||||
|
||||
private func handleAck(_ call: FlutterMethodCall, result: FlutterResult) {
|
||||
let seq: Int?
|
||||
if let v = taskChannelArg(call, key: "seq") as? NSNumber {
|
||||
seq = v.intValue
|
||||
} else if let v = taskChannelArg(call, key: "seq") as? Int {
|
||||
seq = v
|
||||
} else {
|
||||
seq = nil
|
||||
}
|
||||
guard let seq = seq else {
|
||||
result(FlutterError(code: "bad_args", message: "missing or invalid 'seq'", details: nil))
|
||||
return
|
||||
}
|
||||
bus.ack(seq: seq)
|
||||
result(true)
|
||||
}
|
||||
|
||||
/// cancelTask/queryTask/ackEvents 的字符串参数取值。
|
||||
private func taskChannelArg(_ call: FlutterMethodCall, key: String) -> Any? {
|
||||
(call.arguments as? [String: Any])?[key]
|
||||
}
|
||||
|
||||
// ── 任务生命周期 ─────────────────────────────────────────────────
|
||||
|
||||
/// 创建并启动后台 upload task(registerTask / 原生重试共用;
|
||||
/// [executions] 为已执行计数——重试路径沿用,注册路径自 1 起)。
|
||||
private func startTask(spec: UploadRegisterSpec, executions: Int = 1) throws {
|
||||
ensureSession()
|
||||
guard let session = session else {
|
||||
throw NSError(
|
||||
domain: "com.photolive.photo_live.uploads", code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "session unavailable"]
|
||||
)
|
||||
}
|
||||
// REPLACE:作废旧系统任务(静默取消,不产生终态事件)。
|
||||
cancelQuietly(taskId: spec.taskId)
|
||||
guard let url = URL(string: spec.url) else {
|
||||
throw UploadBridgeBadArgsError(message: "'url' invalid")
|
||||
}
|
||||
// 源文件缺失预检(镜像 Kotlin UploadPutExecutor 的 File.exists 分类;
|
||||
// 交由 URLSession 才失败会被误归 NETWORK)。
|
||||
guard FileManager.default.fileExists(atPath: spec.filePath) else {
|
||||
bus.emitFailed(taskId: spec.taskId, code: UploadBridge.failSourceMissing, status: nil)
|
||||
store.remove(taskId: spec.taskId)
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod =
|
||||
spec.method.isEmpty ? "PUT" : spec.method.uppercased()
|
||||
for (name, value) in spec.headers {
|
||||
// Content-Length 由 uploadTask(fromFile:) 按文件长度自带;
|
||||
// Content-Type URLSession 不会自动设置,原样透传(与 Kotlin
|
||||
// 经 RequestBody.contentType 表达等价)。
|
||||
if name.lowercased() == "content-length" {
|
||||
continue
|
||||
}
|
||||
request.setValue(value, forHTTPHeaderField: name)
|
||||
}
|
||||
let task = session.uploadTask(
|
||||
with: request,
|
||||
fromFile: URL(fileURLWithPath: spec.filePath)
|
||||
)
|
||||
store.upsert(
|
||||
taskId: spec.taskId,
|
||||
entry: UploadTaskStore.Entry(
|
||||
url: spec.url,
|
||||
method: request.httpMethod ?? "PUT",
|
||||
headers: spec.headers,
|
||||
filePath: spec.filePath,
|
||||
uploadId: spec.uploadId,
|
||||
attempt: spec.attempt,
|
||||
executions: executions,
|
||||
sessionTaskIdentifier: task.taskIdentifier
|
||||
)
|
||||
)
|
||||
lock.lock()
|
||||
activeTasks[task.taskIdentifier] = task
|
||||
lastProgress[spec.taskId] = nil
|
||||
progressEmittedAt[spec.taskId] = nil
|
||||
lock.unlock()
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/// 静默取消(用户取消 / 重注册替换):无终态事件(镜像 Kotlin 取消语义
|
||||
/// ——Dart 引擎先行置 cancelled 终态,本侧不发事件)。
|
||||
private func cancelQuietly(taskId: String) {
|
||||
let identifier = store.entry(taskId: taskId)?.sessionTaskIdentifier ?? -1
|
||||
if identifier >= 0 {
|
||||
lock.lock()
|
||||
quietCancelIdentifiers.insert(identifier)
|
||||
if let task = activeTasks[identifier] {
|
||||
task.cancel()
|
||||
}
|
||||
lock.unlock()
|
||||
// 未重挂的任务(进程重启后)经系统任务表兜底取消。
|
||||
session?.getAllTasks { [weak self] tasks in
|
||||
for task in tasks where task.taskIdentifier == identifier {
|
||||
task.cancel()
|
||||
}
|
||||
self?.lock.lock()
|
||||
self?.quietCancelIdentifiers.remove(identifier)
|
||||
self?.lock.unlock()
|
||||
}
|
||||
}
|
||||
store.remove(taskId: taskId)
|
||||
lock.lock()
|
||||
lastProgress[taskId] = nil
|
||||
progressEmittedAt[taskId] = nil
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
/// 终态/静默结束后的运行期清理(store 条目由调用方决定去留)。
|
||||
private func taskRuntimeCleanup(taskId: String, identifier: Int) {
|
||||
lock.lock()
|
||||
activeTasks[identifier] = nil
|
||||
quietCancelIdentifiers.remove(identifier)
|
||||
lastProgress[taskId] = nil
|
||||
progressEmittedAt[taskId] = nil
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
// ── delegate 回调入口(UploadSessionDelegate 转发) ──────────────
|
||||
|
||||
fileprivate func taskId(forSessionTask identifier: Int) -> String? {
|
||||
store.taskId(sessionTaskIdentifier: identifier)
|
||||
}
|
||||
|
||||
/// 进度节流(500ms;终值必发)。返回是否应发事件。
|
||||
fileprivate func shouldEmitProgress(taskId: String, sent: Int64, total: Int64) -> Bool {
|
||||
lock.lock()
|
||||
lastProgress[taskId] = (sent, total)
|
||||
let now = Date().timeIntervalSince1970
|
||||
let last = progressEmittedAt[taskId] ?? 0
|
||||
let terminalValue = total > 0 && sent >= total
|
||||
if terminalValue || now - last >= UploadPlugin.progressThrottleSeconds {
|
||||
progressEmittedAt[taskId] = now
|
||||
lock.unlock()
|
||||
return true
|
||||
}
|
||||
lock.unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
/// 静默取消判定(用户取消 / 替换;系统 NSURLErrorCancelled 归同处理)。
|
||||
fileprivate func isQuietlyCancelled(identifier: Int) -> Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return quietCancelIdentifiers.contains(identifier)
|
||||
}
|
||||
|
||||
/// 传输结束分类(didCompleteWithError;分类表见类注释)。
|
||||
fileprivate func handleTaskCompletion(
|
||||
taskId: String,
|
||||
identifier: Int,
|
||||
response: NSHTTPURLResponse?,
|
||||
error: Error?
|
||||
) {
|
||||
if isQuietlyCancelled(identifier: identifier) {
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
return
|
||||
}
|
||||
if let nsError = error as NSError? {
|
||||
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
return
|
||||
}
|
||||
// 网络层错误(镜像 Kotlin IOException → 可重试网络类)。
|
||||
handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil)
|
||||
return
|
||||
}
|
||||
guard let status = response?.statusCode else {
|
||||
handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil)
|
||||
return
|
||||
}
|
||||
switch status {
|
||||
case 200...299:
|
||||
bus.emitCompleted(taskId: taskId, etag: response?.value(forHTTPHeaderField: "ETag"))
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
store.remove(taskId: taskId)
|
||||
case 403:
|
||||
bus.emitNeedsRenew(taskId: taskId)
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
store.remove(taskId: taskId)
|
||||
case 400...499:
|
||||
bus.emitFailed(taskId: taskId, code: UploadBridge.failHttp4xx, status: status)
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
store.remove(taskId: taskId)
|
||||
default:
|
||||
handleRetryable(taskId: taskId, identifier: identifier, serverError: true, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可重试结局:未达尝试上限 → 指数退避后重建任务;达上限 → 终态事件
|
||||
/// (SERVER_ERROR / NETWORK;status 语义对齐 Kotlin——5xx 带 status,
|
||||
/// 网络类不带)。退避定时器经主队列 dispatchAfter——App 挂起期间暂停、
|
||||
/// 唤醒后补跑(系统已按需后台拉起,G3 真机校准点)。
|
||||
private func handleRetryable(
|
||||
taskId: String,
|
||||
identifier: Int,
|
||||
serverError: Bool,
|
||||
status: Int?
|
||||
) {
|
||||
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
||||
guard let entry = store.entry(taskId: taskId) else { return }
|
||||
if entry.executions >= UploadPlugin.maxWorkAttempts {
|
||||
bus.emitFailed(
|
||||
taskId: taskId,
|
||||
code: serverError ? UploadBridge.failServerError : UploadBridge.failNetwork,
|
||||
status: status
|
||||
)
|
||||
store.remove(taskId: taskId)
|
||||
return
|
||||
}
|
||||
var next = entry
|
||||
next.executions += 1
|
||||
store.upsert(taskId: taskId, entry: next)
|
||||
let delay =
|
||||
UploadPlugin.retryBackoffBaseSeconds * pow(2, Double(entry.executions - 1))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
self?.recreate(taskId: taskId)
|
||||
}
|
||||
}
|
||||
|
||||
/// 重试重建(退避到期;期间被取消/重注册则 entry 已变——按 URL 等字段重建)。
|
||||
private func recreate(taskId: String) {
|
||||
guard let entry = store.entry(taskId: taskId) else { return }
|
||||
let spec = UploadRegisterSpec(
|
||||
taskId: taskId,
|
||||
url: entry.url,
|
||||
method: entry.method,
|
||||
headers: entry.headers,
|
||||
filePath: entry.filePath,
|
||||
uploadId: entry.uploadId,
|
||||
attempt: entry.attempt
|
||||
)
|
||||
do {
|
||||
try startTask(spec: spec, executions: entry.executions)
|
||||
} catch {
|
||||
bus.emitFailed(taskId: taskId, code: UploadBridge.failNetwork, status: nil)
|
||||
store.remove(taskId: taskId)
|
||||
}
|
||||
}
|
||||
|
||||
/// delegate 持有任务引用(重挂/取消用)。
|
||||
fileprivate func trackActiveTask(_ task: URLSessionTask) {
|
||||
guard let taskId = store.taskId(sessionTaskIdentifier: task.taskIdentifier) else { return }
|
||||
store.mutate(taskId: taskId) { entry in
|
||||
entry.sessionTaskIdentifier = task.taskIdentifier
|
||||
}
|
||||
lock.lock()
|
||||
activeTasks[task.taskIdentifier] = task
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ── EventChannel 流(终态缓存重放见 UploadEventBus) ────────────────────
|
||||
|
||||
/// EventChannel `photolive/uploads/events`:onListen 接管总线 sink(事件
|
||||
/// 统一主线程投递,镜像 Kotlin mainHandler.post);onCancel 摘除。
|
||||
private final class UploadEventStreamHandler: NSObject, FlutterStreamHandler {
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
||||
-> FlutterError?
|
||||
{
|
||||
UploadEventBus.shared.attach { event in
|
||||
DispatchQueue.main.async {
|
||||
events(event)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
UploadEventBus.shared.detach()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ── URLSession delegate(系统回调 → 插件分类) ──────────────────────────
|
||||
|
||||
/// 后台会话 delegate(独立对象 + weak 回指,避免 session↔plugin 环)。
|
||||
/// 回调在系统串行队列;所有插件入口自带锁。
|
||||
private final class UploadSessionDelegate: NSObject, URLSessionDataDelegate {
|
||||
weak var plugin: UploadPlugin?
|
||||
|
||||
init(plugin: UploadPlugin) {
|
||||
self.plugin = plugin
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didSendBodyData bytesSent: Int64,
|
||||
totalBytesSent: Int64,
|
||||
totalBytesExpectedToSend: Int64
|
||||
) {
|
||||
guard let plugin = plugin,
|
||||
let taskId = plugin.taskId(forSessionTask: task.taskIdentifier)
|
||||
else { return }
|
||||
plugin.trackActiveTask(task)
|
||||
if plugin.shouldEmitProgress(
|
||||
taskId: taskId,
|
||||
sent: totalBytesSent,
|
||||
total: totalBytesExpectedToSend
|
||||
) {
|
||||
UploadEventBus.shared.emitProgress(
|
||||
taskId: taskId,
|
||||
sent: totalBytesSent,
|
||||
total: totalBytesExpectedToSend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传任务本质是 data task:响应体(MinIO XML/空)读取后丢弃,防连接滞留。
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didReceive data: Data,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
||||
) {
|
||||
completionHandler(.discard)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didCompleteWithError error: Error?
|
||||
) {
|
||||
guard let plugin = plugin,
|
||||
let taskId = plugin.taskId(forSessionTask: task.taskIdentifier)
|
||||
else { return }
|
||||
plugin.handleTaskCompletion(
|
||||
taskId: taskId,
|
||||
identifier: task.taskIdentifier,
|
||||
response: task.response as? NSHTTPURLResponse,
|
||||
error: error
|
||||
)
|
||||
}
|
||||
|
||||
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
||||
plugin?.finishBackgroundEvents()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user