Phase 1.14.c:LobbyZipUpgrader 加 0…1 progress 回调
- upgradeIfNeeded 新增 onProgress: @escaping @Sendable (Double) -> Void 参数, 默认 no-op 兼容仅做版本对比的烟雾测试 - 新增文件内私有 DownloadProgressDelegate(URLSessionDownloadDelegate + @unchecked Sendable),在 didWriteData 回调里把 totalBytesWritten / totalBytesExpectedToWrite 换算成 0…1 上报,clamp 到 [0,1] - 通过 session.download(from:delegate:) 注入;下载完成后兜底报 1.0 (部分 server 不发 Content-Length 或末尾片段晚到,避免进度条停在 99%) - RootViewController 烟雾测试用 ProgressTicker 把回调节流到 10% 阶梯打印, 避免几百行刷屏(NSLock 保护单 Int 状态,@unchecked Sendable) - BuildProject 通过 Plan 进度已勾选(§5 Phase 1.14.c + §8) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
ba34854133
commit
f3c66b670d
@@ -129,10 +129,19 @@ final class RootViewController: UIViewController {
|
||||
→ H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
|
||||
""")
|
||||
|
||||
// Phase 1.13 烟雾测试:触发 LobbyZipUpgrader(如果需要升级)
|
||||
// Phase 1.13 / 1.14.c 烟雾测试:触发 LobbyZipUpgrader(如果需要升级),
|
||||
// 并打印 progress 回调(每 10% 报一次,避免刷屏)
|
||||
let upgradeStart = Date()
|
||||
do {
|
||||
let outcome = try await LobbyZipUpgrader.shared.upgradeIfNeeded(resolved: resolved)
|
||||
let lastReported = ProgressTicker()
|
||||
let outcome = try await LobbyZipUpgrader.shared.upgradeIfNeeded(
|
||||
resolved: resolved,
|
||||
onProgress: { p in
|
||||
lastReported.tickIfPassed(p) { reported in
|
||||
print(String(format: "[LobbyZipUpgrader] 下载进度 %3d%%", Int(reported * 100)))
|
||||
}
|
||||
}
|
||||
)
|
||||
let elapsed = Date().timeIntervalSince(upgradeStart)
|
||||
switch outcome {
|
||||
case .noop:
|
||||
@@ -173,3 +182,21 @@ final class RootViewController: UIViewController {
|
||||
|
||||
override var prefersStatusBarHidden: Bool { false }
|
||||
}
|
||||
|
||||
/// 仅供 Phase 1.14.c 烟雾测试使用:按 10% 阶梯节流 progress 打印,避免几百行刷屏。
|
||||
/// `@unchecked Sendable`:状态仅一个 Int 阈值,用 NSLock 保护写入。
|
||||
private final class ProgressTicker: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var lastBucket: Int = -1
|
||||
|
||||
func tickIfPassed(_ progress: Double, action: (Double) -> Void) {
|
||||
let bucket = Int(progress * 10)
|
||||
lock.lock()
|
||||
let shouldFire = bucket > lastBucket
|
||||
if shouldFire { lastBucket = bucket }
|
||||
lock.unlock()
|
||||
if shouldFire {
|
||||
action(Double(bucket) / 10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,15 @@ public actor LobbyZipUpgrader {
|
||||
|
||||
/// 比较远端 game_version 与本地 version.xml。需要升级则下载 + 解压 + 原子 rename。
|
||||
///
|
||||
/// - Parameter resolved: VersionResolver.resolve(...) 的结果
|
||||
/// - Parameters:
|
||||
/// - resolved: VersionResolver.resolve(...) 的结果
|
||||
/// - onProgress: 下载进度回调 0.0…1.0,由 URLSession delegate 在后台线程派发;
|
||||
/// UI 层负责把闭包 hop 到 MainActor。默认 no-op 兼容仅做版本对比的烟雾测试。
|
||||
/// - Returns: `.noop` 已是最新;`.upgraded(from:to:)` 完成升级
|
||||
public func upgradeIfNeeded(resolved: ResolvedVersion) async throws -> Outcome {
|
||||
public func upgradeIfNeeded(
|
||||
resolved: ResolvedVersion,
|
||||
onProgress: @escaping @Sendable (Double) -> Void = { _ in }
|
||||
) async throws -> Outcome {
|
||||
let localGameVersion = LocalVersionReader.localGameVersion
|
||||
guard resolved.gameVersion > localGameVersion,
|
||||
let zipURLString = resolved.gameZip,
|
||||
@@ -55,11 +61,15 @@ public actor LobbyZipUpgrader {
|
||||
return .noop
|
||||
}
|
||||
|
||||
// 1. 下载到 tmp(URLSession 默认管理临时位置)
|
||||
// 1. 下载到 tmp(URLSession 默认管理临时位置),delegate 上报 0…1 进度
|
||||
let progressDelegate = DownloadProgressDelegate(onProgress: onProgress)
|
||||
let downloadedURL: URL
|
||||
let response: URLResponse
|
||||
do {
|
||||
(downloadedURL, response) = try await session.download(from: zipURL)
|
||||
(downloadedURL, response) = try await session.download(
|
||||
from: zipURL,
|
||||
delegate: progressDelegate
|
||||
)
|
||||
} catch {
|
||||
throw UpgradeError.downloadFailed(error)
|
||||
}
|
||||
@@ -67,6 +77,9 @@ public actor LobbyZipUpgrader {
|
||||
!(200..<300).contains(http.statusCode) {
|
||||
throw UpgradeError.httpStatus(http.statusCode)
|
||||
}
|
||||
// 下载完成兜底报 1.0:部分 server 不发 Content-Length 或末尾片段晚到,
|
||||
// 不显式补这一下,进度条可能停在 99% 直到解压结束才跳走
|
||||
onProgress(1.0)
|
||||
|
||||
// 2. 解压到隔离 staging 目录
|
||||
let staging = SandboxPaths.caches
|
||||
@@ -99,3 +112,37 @@ public actor LobbyZipUpgrader {
|
||||
return .upgraded(from: localGameVersion, to: resolved.gameVersion)
|
||||
}
|
||||
}
|
||||
|
||||
/// URLSession 下载进度桥接:把 `didWriteData` 的 byte 计数换算成 0…1,回调上层。
|
||||
///
|
||||
/// 之所以放在文件内私有:仅 LobbyZipUpgrader 用,不构成对外 API。
|
||||
/// `@unchecked Sendable`:URLSessionDownloadDelegate 协议本身不带 Sendable;
|
||||
/// 我们的状态仅是一个 `@Sendable` 闭包,无可变共享,标记安全。
|
||||
private final class DownloadProgressDelegate: NSObject,
|
||||
URLSessionDownloadDelegate,
|
||||
@unchecked Sendable {
|
||||
private let onProgress: @Sendable (Double) -> Void
|
||||
|
||||
init(onProgress: @escaping @Sendable (Double) -> Void) {
|
||||
self.onProgress = onProgress
|
||||
super.init()
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didWriteData bytesWritten: Int64,
|
||||
totalBytesWritten: Int64,
|
||||
totalBytesExpectedToWrite: Int64) {
|
||||
guard totalBytesExpectedToWrite > 0 else { return }
|
||||
let p = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
||||
onProgress(min(max(p, 0), 1))
|
||||
}
|
||||
|
||||
/// 必须实现的协议方法。async download API 内部已接管下载文件落地,
|
||||
/// 这里无需把文件搬到任何位置。
|
||||
func urlSession(_ session: URLSession,
|
||||
downloadTask: URLSessionDownloadTask,
|
||||
didFinishDownloadingTo location: URL) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user