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.
236 lines
7.9 KiB
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);
|
||
|
|
}
|
||
|
|
}
|