You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/mx/module/res/BundleLifecycle.ts

236 lines
7.9 KiB

3 weeks ago
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<string> = 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<string, MembershipSet<string>>();
/** 同名 loadBundle 合并;超时后可重试 */
private _loadWait: Record<string, Promise<AssetManager.Bundle | null> | undefined> = {};
/**
* 下载分包:同名只发起一次原生 loadBundle。
* 超时只结束本次等待,不拆掉进行中的请求(新用户首下很慢,拆掉会二次并发且回调丢失)。
* @param bundleName 分包名
*/
ensureLoaded(bundleName: string): Promise<AssetManager.Bundle | null> {
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<AssetManager.Bundle | null>): Promise<AssetManager.Bundle | null> {
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<AssetManager.Bundle | null> {
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);
}
}