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:
@@ -12,7 +12,7 @@ import Foundation
|
||||
///
|
||||
/// 多渠道分发:构建后用 `plutil` 修改 `.app/ChannelConfig.plist` + 重签,
|
||||
/// 不需要重新 Xcode build。详见 ADR-007。
|
||||
public final class BundleConfig: Sendable {
|
||||
nonisolated public final class BundleConfig: Sendable {
|
||||
|
||||
/// App 启动期默认读 main bundle 的 `ChannelConfig.plist`,业务代码统一通过此单例访问。
|
||||
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: - 基础沙盒目录
|
||||
|
||||
/// `Library/Caches/`(系统空间不足时可能被清,需可重建)
|
||||
public static var caches: URL {
|
||||
nonisolated public static var caches: URL {
|
||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
/// `Documents/`(用户级持久化,iCloud 备份)
|
||||
public static var documents: URL {
|
||||
nonisolated public static var documents: URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
/// `<App>.app/`
|
||||
public static var bundle: URL {
|
||||
nonisolated public static var bundle: URL {
|
||||
Bundle.main.bundleURL
|
||||
}
|
||||
|
||||
// MARK: - 大厅 H5 路径(基于 BundleConfig.shared 的当前渠道)
|
||||
|
||||
/// 大厅 H5 解压根目录:`{Caches}/{gamedir}/`
|
||||
public static var lobbyRoot: URL {
|
||||
nonisolated public static var lobbyRoot: URL {
|
||||
caches.appendingPathComponent(BundleConfig.shared.gameDir)
|
||||
}
|
||||
|
||||
/// 大厅 H5 入口 HTML:`{Caches}/{gamedir}/{gamestart}/index.html`
|
||||
public static var lobbyIndex: URL {
|
||||
nonisolated public static var lobbyIndex: URL {
|
||||
let cfg = BundleConfig.shared
|
||||
return caches
|
||||
.appendingPathComponent(cfg.gameDir)
|
||||
@@ -47,7 +47,7 @@ public enum SandboxPaths {
|
||||
|
||||
/// 大厅 H5 版本标记文件:`{Caches}/{gamedir}/{gamestart}/version.xml`
|
||||
/// 由 ResourceUnzipper 判断是否需要解压初始 gamehall.zip。
|
||||
public static var lobbyVersionXML: URL {
|
||||
nonisolated public static var lobbyVersionXML: URL {
|
||||
let cfg = BundleConfig.shared
|
||||
return caches
|
||||
.appendingPathComponent(cfg.gameDir)
|
||||
@@ -58,12 +58,12 @@ public enum SandboxPaths {
|
||||
// MARK: - 子游戏 H5 路径(参数化,由 SwitchOverGameData 传入)
|
||||
|
||||
/// 子游戏 H5 解压根目录:`{Caches}/{dir}/`
|
||||
public static func subGameRoot(_ dir: String) -> URL {
|
||||
nonisolated public static func subGameRoot(_ dir: String) -> URL {
|
||||
caches.appendingPathComponent(dir)
|
||||
}
|
||||
|
||||
/// 子游戏 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)
|
||||
.appendingPathComponent(start)
|
||||
.appendingPathComponent("index.html")
|
||||
|
||||
Reference in New Issue
Block a user