import { assetManager, type AssetManager } from 'cc'; import { Singleton } from '../../tools/Singleton'; import { LoadRecoveryConfig } from './LoadRecovery'; import { createMembershipSet, membershipAdd, membershipFromArray, mapKeysToArray, type MembershipSet, type ReadonlyMembershipSet, } from '../../tools/MembershipSet'; /** 战斗阶段专用 bundle:退战斗且 UI 销毁后可 unload(与 GBundle 字符串一致) */ const BATTLE_PHASE_BUNDLES: readonly string[] = [ 'battleMap', 'battleGameMonster', 'battleHero', 'popBattle', 'battleGame', ]; /** 主 Tab bundle:进战斗前可 unload(openPanel 会按需重载) */ const TAB_BUNDLES: readonly string[] = [ 'shop', 'role', 'weapon', 'replica', 'chapterGift', 'diamondShop', ]; export function isTabScopedBundle(bundleName: string): boolean { return TAB_BUNDLES.includes(bundleName); } const PERMANENT_BUNDLES: ReadonlyMembershipSet = membershipFromArray([ 'first', 'comm', 'popCommon', 'home', 'icons', 'audio', 'heros', ]); /** 闲时静默下载的非常驻分包(新产品磁盘实况;不含 lang_* 多语言包) */ const IDLE_DOWNLOAD_BUNDLES: readonly string[] = [ 'rank', 'shenjiang', 'battleGame', 'battleMap', 'battleGameMonster', 'battleHero', 'popBattle', ]; /** * Bundle 生命周期:登记 owner,按阶段 unload 非常驻包。 */ export class BundleLifecycle extends Singleton { private _owners = new Map>(); /** 同名 loadBundle 合并;超时后可重试 */ private _loadWait: Record | undefined> = {}; /** * 下载分包:同名只发起一次原生 loadBundle。 * 超时只结束本次等待,不拆掉进行中的请求(新用户首下很慢,拆掉会二次并发且回调丢失)。 * @param bundleName 分包名 */ ensureLoaded(bundleName: string): Promise { if (!bundleName) return Promise.resolve(null); const hit = assetManager.getBundle(bundleName); if (hit) return Promise.resolve(hit); if (!this._loadWait[bundleName]) { this._loadWait[bundleName] = this._loadBundleNative(bundleName); } const native = this._loadWait[bundleName]; if (!native) return Promise.resolve(null); return this._awaitLoaded(bundleName, native); } /** * 只点火 loadBundle,不等待。点开始后 user load 会合并到同一 native。 * @param bundleName 分包名 */ kickLoad(bundleName: string): void { if (!bundleName || assetManager.getBundle(bundleName)) return; if (!this._loadWait[bundleName]) { this._loadWait[bundleName] = this._loadBundleNative(bundleName); } } /** * 原生 loadBundle 是否仍在飞(超时后仍可能落地)。 * @param bundleName 分包名 */ isLoadInflight(bundleName: string): boolean { return !!this._loadWait[bundleName]; } /** * 闲时点火所有尚未落地的业务分包 zip。 * 只 kickLoad(合并同名、不 acquire、不 load 资源);用户加载时不再新发。 * @returns 已发起 kick 的分包名 */ kickAllPendingIdleBundles(): string[] { const kicked: string[] = []; for (const name of IDLE_DOWNLOAD_BUNDLES) { if (PERMANENT_BUNDLES.has(name)) continue; if (assetManager.getBundle(name) || this._loadWait[name]) continue; this.kickLoad(name); kicked.push(name); } return kicked; } /** * 等原生结果;超时只结束本次等待,不 clear 原生 inflight。 * 拆掉 inflight 会在弱网二次并发 loadBundle,小游戏上第二回调可能永不回来。 * @param bundleName 分包名 * @param native 原生 Promise */ private _awaitLoaded(bundleName: string, native: Promise): Promise { return new Promise((resolve) => { let done = false; const timer = setTimeout(() => { if (done) return; done = true; const late = assetManager.getBundle(bundleName); if (!late) { console.error('[BundleLifecycle] loadBundle timeout (native still inflight)', bundleName); } resolve(late); }, LoadRecoveryConfig.bundleTimeoutMs); native.then((bundle) => { if (done) return; done = true; clearTimeout(timer); resolve(bundle); }); }); } /** * 单次原生 loadBundle,完成后才清 inflight。 * @param bundleName 分包名 */ private _loadBundleNative(bundleName: string): Promise { return new Promise((resolve) => { assetManager.loadBundle(bundleName, (err, bundle) => { this._loadWait[bundleName] = undefined; if (err || !bundle) { console.error('[BundleLifecycle] loadBundle fail', bundleName, String(err)); resolve(null); return; } resolve(bundle); }); }); } acquire(bundleName: string, owner: string): void { if (!bundleName || !owner || PERMANENT_BUNDLES.has(bundleName)) return; let set = this._owners.get(bundleName); if (!set) { set = createMembershipSet(); this._owners.set(bundleName, set); } membershipAdd(set, owner); } releaseOwner(owner: string): void { if (!owner) return; for (const set of this._owners.values()) { set.delete(owner); } } releaseBattlePhase(): void { for (const name of BATTLE_PHASE_BUNDLES) { this._tryRemoveBundle(name, 'battle'); } this.releaseOwner('battle'); } /** 进战斗前卸 Tab 包,降低战斗峰值 */ releaseTabBundles(): void { for (const name of TAB_BUNDLES) { this._tryRemoveBundle(name, 'tab'); } this.releaseOwner('tab'); } /** 切 Tab 时卸除非当前 Tab 的 bundle(保留 keepBundles 中的包) */ releaseTabBundlesExcept(keepBundles: readonly string[]): void { const keep = membershipFromArray(keepBundles); for (const name of TAB_BUNDLES) { if (keep.has(name)) continue; this._tryRemoveBundle(name, 'tab'); } } /** 面板 getResPkg 清理时对称释放 bundle 的 res 所有权 */ releaseBundleOwner(bundleName: string, owner: string): void { if (!bundleName || !owner || PERMANENT_BUNDLES.has(bundleName)) return; const owners = this._owners.get(bundleName); if (!owners) return; owners.delete(owner); if (owners.size === 0) { this._owners.delete(bundleName); this._removeBundle(bundleName); } } private _tryRemoveBundle(bundleName: string, releasingOwner: string): void { if (PERMANENT_BUNDLES.has(bundleName)) return; const owners = this._owners.get(bundleName); if (owners && owners.size > 0) { const others = mapKeysToArray(owners).filter((o) => o !== releasingOwner); if (others.length > 0) return; } this._removeBundle(bundleName); this._owners.delete(bundleName); } private _removeBundle(bundleName: string): void { if (this._loadWait[bundleName]) { console.warn('[BundleLifecycle] skip removeBundle while loading', bundleName); return; } const bundle = assetManager.getBundle(bundleName); if (!bundle) return; assetManager.removeBundle(bundle); } }