Phase 1.5:ResourceUnzipper 解压 gamehall.zip 到 Caches 全链路打通

启动期完成「ChannelConfig.plist 读 → SandboxPaths 算路径 →
gamehall.zip 解压」三步,lobbyIndex 真实存在于沙盒 Caches,
为 Phase 1.10 WebContainer file:// 加载就绪。

- 新增 ylgamehall/Source/Resource/ResourceUnzipper.swift
  - public actor,全局单例 ResourceUnzipper.shared
  - ensureReady() async throws:检查 lobbyVersionXML 不存在则调
    FileManager.unzipItem (ZIPFoundation 扩展) 解压 Bundle 内
    gamehall.zip 到 SandboxPaths.lobbyRoot
  - UnzipError 类型化错误(bundleZipNotFound / unzipFailed)
  - 幂等:已就绪直接 return,并发调用安全(actor 串行)
- 新增 ylgamehall/Resources/gamehall.zip:从 docs/res 拷一份入
  Bundle(11.5 MB,2023-12 旧版;上线前由 H5 团队替换最新版,
  ADR-002 已记录)
- Swift 6 严格并发适配:
  - SandboxPaths:所有 static 成员标 nonisolated(无状态命名空间,
    从 actor / 非 MainActor 上下文都能直接读 caches/documents/bundle
    及 lobby 路径计算)
  - BundleConfig:class 整体 nonisolated(Sendable + all-let 属性,
    跨 actor 边界安全),允许 ResourceUnzipper actor 读 channel 配置
- RootViewController 补 ResourceUnzipper 烟雾测试 Task:
  await ensureReady() 后打印耗时 + lobbyIndex 是否真实存在
- 真机验证(iOS Simulator):首次 1.266s 解压成功,二次启动 0s 跳过
This commit is contained in:
joywayer
2026-06-22 00:01:08 +08:00
parent b866ee212c
commit 463f0fc6fe
5 changed files with 76 additions and 9 deletions
Binary file not shown.
+16
View File
@@ -40,6 +40,22 @@ final class RootViewController: UIViewController {
lobbyIndex = \(SandboxPaths.lobbyIndex.path) lobbyIndex = \(SandboxPaths.lobbyIndex.path)
lobbyVersionXML = \(SandboxPaths.lobbyVersionXML.path) lobbyVersionXML = \(SandboxPaths.lobbyVersionXML.path)
""") """)
// Phase 1.5 ResourceUnzipper lobby index.html
Task {
let start = Date()
do {
try await ResourceUnzipper.shared.ensureReady()
let elapsed = Date().timeIntervalSince(start)
let exists = FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path)
print("""
[ResourceUnzipper] ensureReady 完成(耗时 \(String(format: "%.3f", elapsed))s
lobbyIndex 存在: \(exists)
""")
} catch {
print("[ResourceUnzipper] ERROR: \(error)")
}
}
} }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
@@ -12,7 +12,7 @@ import Foundation
/// ///
/// `plutil` `.app/ChannelConfig.plist` + /// `plutil` `.app/ChannelConfig.plist` +
/// Xcode build ADR-007 /// Xcode build ADR-007
public final class BundleConfig: Sendable { nonisolated public final class BundleConfig: Sendable {
/// App main bundle `ChannelConfig.plist`访 /// App main bundle `ChannelConfig.plist`访
public static let shared = BundleConfig() public static let shared = BundleConfig()
@@ -0,0 +1,51 @@
//
// ResourceUnzipper.swift
// ylgamehall
//
// Bundle gamehall.zip Library/Caches/{gamedir}/
// docs/H5-Native-Implementation-Design.md §1.1 / §7
//
import Foundation
import ZIPFoundation
/// H5 actor `ensureReady()`
///
/// `{Caches}/{gamedir}/{gamestart}/version.xml`
/// / Caches 0
public actor ResourceUnzipper {
public static let shared = ResourceUnzipper()
public enum UnzipError: Error {
case bundleZipNotFound
case unzipFailed(underlying: Error)
}
private init() {}
/// H5 Caches
///
/// - version.xml
/// - `{Caches}/{gamedir}/` `Bundle/gamehall.zip`
public func ensureReady() async throws {
if FileManager.default.fileExists(atPath: SandboxPaths.lobbyVersionXML.path) {
return
}
guard let zipURL = Bundle.main.url(forResource: "gamehall", withExtension: "zip") else {
throw UnzipError.bundleZipNotFound
}
let destination = SandboxPaths.lobbyRoot
let fm = FileManager.default
do {
try fm.createDirectory(at: destination,
withIntermediateDirectories: true)
try fm.unzipItem(at: zipURL, to: destination)
} catch {
throw UnzipError.unzipFailed(underlying: error)
}
}
}
@@ -15,29 +15,29 @@ public enum SandboxPaths {
// MARK: - // MARK: -
/// `Library/Caches/` /// `Library/Caches/`
public static var caches: URL { nonisolated public static var caches: URL {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
} }
/// `Documents/`iCloud /// `Documents/`iCloud
public static var documents: URL { nonisolated public static var documents: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
} }
/// `<App>.app/` /// `<App>.app/`
public static var bundle: URL { nonisolated public static var bundle: URL {
Bundle.main.bundleURL Bundle.main.bundleURL
} }
// MARK: - H5 BundleConfig.shared // MARK: - H5 BundleConfig.shared
/// H5 `{Caches}/{gamedir}/` /// H5 `{Caches}/{gamedir}/`
public static var lobbyRoot: URL { nonisolated public static var lobbyRoot: URL {
caches.appendingPathComponent(BundleConfig.shared.gameDir) caches.appendingPathComponent(BundleConfig.shared.gameDir)
} }
/// H5 HTML`{Caches}/{gamedir}/{gamestart}/index.html` /// H5 HTML`{Caches}/{gamedir}/{gamestart}/index.html`
public static var lobbyIndex: URL { nonisolated public static var lobbyIndex: URL {
let cfg = BundleConfig.shared let cfg = BundleConfig.shared
return caches return caches
.appendingPathComponent(cfg.gameDir) .appendingPathComponent(cfg.gameDir)
@@ -47,7 +47,7 @@ public enum SandboxPaths {
/// H5 `{Caches}/{gamedir}/{gamestart}/version.xml` /// H5 `{Caches}/{gamedir}/{gamestart}/version.xml`
/// ResourceUnzipper gamehall.zip /// ResourceUnzipper gamehall.zip
public static var lobbyVersionXML: URL { nonisolated public static var lobbyVersionXML: URL {
let cfg = BundleConfig.shared let cfg = BundleConfig.shared
return caches return caches
.appendingPathComponent(cfg.gameDir) .appendingPathComponent(cfg.gameDir)
@@ -58,12 +58,12 @@ public enum SandboxPaths {
// MARK: - H5 SwitchOverGameData // MARK: - H5 SwitchOverGameData
/// H5 `{Caches}/{dir}/` /// H5 `{Caches}/{dir}/`
public static func subGameRoot(_ dir: String) -> URL { nonisolated public static func subGameRoot(_ dir: String) -> URL {
caches.appendingPathComponent(dir) caches.appendingPathComponent(dir)
} }
/// H5 HTML`{Caches}/{dir}/{start}/index.html` /// H5 HTML`{Caches}/{dir}/{start}/index.html`
public static func subGameIndex(_ dir: String, _ start: String) -> URL { nonisolated public static func subGameIndex(_ dir: String, _ start: String) -> URL {
caches.appendingPathComponent(dir) caches.appendingPathComponent(dir)
.appendingPathComponent(start) .appendingPathComponent(start)
.appendingPathComponent("index.html") .appendingPathComponent("index.html")