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.
865 lines
33 KiB
865 lines
33 KiB
import { _decorator, Asset, AssetManager, assetManager, Component, director, instantiate, Node, ParticleSystem2D, Prefab, sp, SpriteFrame } from 'cc';
|
|
import { DEV } from 'cc/env';
|
|
import { Singleton } from '../../tools/Singleton';
|
|
import { ResPkgName } from './ResPkgName';
|
|
import { BundleLifecycle } from './BundleLifecycle';
|
|
import { formatLoadKey, LoadRecoveryConfig, type LoadResult } from './LoadRecovery';
|
|
import {
|
|
createMembershipSet,
|
|
membershipAdd,
|
|
membershipToArray,
|
|
mapKeysToArray,
|
|
type MembershipSet,
|
|
} from '../../tools/MembershipSet';
|
|
const { ccclass, property } = _decorator;
|
|
|
|
/**资源信息 */
|
|
export interface ResInfo {
|
|
/**资源名称 */
|
|
name: string,
|
|
/**资源所在bundle */
|
|
bundle: string,
|
|
/**资源路径 */
|
|
path: string,
|
|
/**资源类型 */
|
|
type: typeof Asset
|
|
/**资源实例 */
|
|
res?: Asset
|
|
}
|
|
|
|
/** 目录批量加载(bundle.loadDir,避免数十条并行单资源 load 在预览/弱机失败) */
|
|
export interface ResDirInfo {
|
|
/** bundle 名 */
|
|
bundle: string;
|
|
/** 目录路径(如 prefab/hero) */
|
|
path: string;
|
|
/** 资源类型 */
|
|
type: typeof Asset;
|
|
}
|
|
|
|
/**资源包 */
|
|
export interface ResPakage {
|
|
/**资源包名称 */
|
|
name: string,
|
|
/**是否可释放 */
|
|
isReleaseable: boolean,
|
|
/**资源列表 */
|
|
ress: ResInfo[],
|
|
/** 可选:整目录加载条目(与 ress 并列计入进度) */
|
|
dirs?: ResDirInfo[],
|
|
}
|
|
|
|
@ccclass('ResourcesManager')
|
|
export class ResourcesManager extends Singleton {
|
|
|
|
/**节点池 */
|
|
private _nodePool: Map<string, Node[]> = new Map();
|
|
/**资源缓存 */
|
|
private _resCache: Map<string, ResPakage> = new Map();
|
|
/**预加载登记(preLoad 命中后 ensure 时晋升) */
|
|
private _preloadedKeys: MembershipSet<string> = createMembershipSet();
|
|
/** 同路径 bundle.load 合并,超时不拆原生请求 */
|
|
private _assetLoadWait: Record<string, Promise<Asset | null> | undefined> = {};
|
|
/** 同目录 bundle.loadDir 合并 */
|
|
private _dirLoadWait: Record<string, Promise<{ ok: boolean; assets?: Asset[] }> | undefined> = {};
|
|
/** 同名资源包加载代数:clearResPkg/重复 load 时递增,旧链 finishOne 不得再写 cache */
|
|
private _pkgLoadGen: Record<string, number> = {};
|
|
|
|
assetKey(name: string, path: string, bundle: string, type: typeof Asset): string {
|
|
const n = type === SpriteFrame ? name + "/spriteframe" : name;
|
|
return `${bundle}|${path}|${n}|${type.name}`;
|
|
}
|
|
|
|
isPreloaded(name: string, path: string, bundle: string, type: typeof Asset): boolean {
|
|
return this._preloadedKeys.has(this.assetKey(name, path, bundle, type));
|
|
}
|
|
|
|
/**获取资源
|
|
* @param path 资源路径
|
|
* @param bundle 资源所在bundle
|
|
* @returns 资源
|
|
*/
|
|
getRes<T>(name: string, path: string, bundle: string, type: typeof Asset): T extends Asset ? T : null {
|
|
if (type == SpriteFrame) {
|
|
name = name + "/spriteframe";
|
|
}
|
|
for (let resArr of this._resCache.values()) {
|
|
for (let i = 0; i < resArr.ress.length; i++) {
|
|
let resInfo = resArr.ress[i];
|
|
if (resInfo.name == name && resInfo.path == path && resInfo.bundle == bundle && resInfo.type == type) {
|
|
if (resInfo.res?.isValid) return resInfo.res as T extends Asset ? T : null;
|
|
resInfo.res = null;
|
|
}
|
|
}
|
|
}
|
|
let b = assetManager.getBundle(bundle);
|
|
if (b) {
|
|
const assetPath = path + name;
|
|
const res = this._validBundleAsset(b, assetPath, type);
|
|
if (res) {
|
|
return res as T extends Asset ? T : null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**获取节点(从节点池获取,没有就从预制件缓存实例化) */
|
|
getNode(path: string, name: string, bundle: string) {
|
|
let key = bundle + path + name;
|
|
try {
|
|
let pool = this._nodePool.get(key);
|
|
if (!pool) {
|
|
pool = [];
|
|
this._nodePool.set(key, pool);
|
|
}
|
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
while (pool.length > 0 && !pool[0]?.isValid) {
|
|
pool.shift();
|
|
}
|
|
if (pool.length <= 0) {
|
|
let p = this.getRes(name, path, bundle, Prefab) as Prefab;
|
|
if (p?.isValid) {
|
|
let n = instantiate(p);
|
|
n["resKey"] = key;
|
|
this._markPoolNodeBaseline(n);
|
|
pool.push(n);
|
|
}
|
|
}
|
|
if (pool.length <= 0) return null;
|
|
const node = pool.shift();
|
|
if (!node?.isValid) continue;
|
|
// 仍挂在场景上的池条目:曾 reparent 回场景却未出池,出池会把在用字块摘走
|
|
if (node.parent?.isValid) {
|
|
continue;
|
|
}
|
|
node.active = true;
|
|
this._restorePoolNodeBaseline(node, name, path, bundle);
|
|
node.setPosition(0, 0, 0);
|
|
node.angle = 0;
|
|
for (const sk of node.getComponentsInChildren(sp.Skeleton)) {
|
|
if (!sk?.isValid) continue;
|
|
sk.enabled = true;
|
|
this._resetPooledSpine(sk);
|
|
}
|
|
return node;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error("UIBase getNode error", key, String(error));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 节点已重新挂回场景时,从池数组摘掉,避免下次 getNode 偷走在用节点。
|
|
* @param node 可能仍留在池里的场景节点
|
|
*/
|
|
detachFromPool(node: Node | null | undefined): void {
|
|
if (!node?.isValid) return;
|
|
const key = node['resKey'] as string | undefined;
|
|
if (!key) return;
|
|
const pool = this._nodePool.get(key);
|
|
if (!pool?.length) return;
|
|
for (let i = pool.length - 1; i >= 0; i--) {
|
|
if (pool[i] === node) {
|
|
pool.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 出池/回池重置 Spine。缓存模式不能 clearTracks,须先退回 REALTIME。
|
|
* @param sk Spine 组件
|
|
*/
|
|
private _resetPooledSpine(sk: sp.Skeleton): void {
|
|
try {
|
|
if (typeof sk.isAnimationCached === 'function' && sk.isAnimationCached()) {
|
|
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
|
|
}
|
|
sk.clearTracks();
|
|
sk.setCompleteListener(null);
|
|
sk.setToSetupPose?.();
|
|
sk.setSlotsToSetupPose?.();
|
|
} catch (_) { /* bundle 已卸载 */ }
|
|
}
|
|
|
|
/** 记录预制体根节点设计 scale(如睡觉=0.5);出池时恢复,禁止统一洗成 1 */
|
|
private _markPoolNodeBaseline(node: Node): void {
|
|
if (node["poolBaseScaleX"] == null) {
|
|
node["poolBaseScaleX"] = node.scale.x;
|
|
node["poolBaseScaleY"] = node.scale.y;
|
|
}
|
|
}
|
|
|
|
private _readPrefabRootScale(name: string, path: string, bundle: string): { x: number; y: number } | null {
|
|
const p = this.getRes(name, path, bundle, Prefab) as Prefab | null;
|
|
if (!p?.isValid || !p.data?.isValid) return null;
|
|
const x = p.data.scale.x;
|
|
const y = p.data.scale.y;
|
|
if (Math.abs(x - 1) < 0.01 && Math.abs(y - 1) < 0.01) return null;
|
|
return { x, y };
|
|
}
|
|
|
|
/** 出池恢复预制体根 scale;优先读 Prefab 设计值,纠正旧池污染为 1 的节点 */
|
|
private _restorePoolNodeBaseline(node: Node, prefabName: string, path: string, bundle: string): void {
|
|
const design = this._readPrefabRootScale(prefabName, path, bundle);
|
|
const storedX = node["poolBaseScaleX"] as number | undefined;
|
|
if (design) {
|
|
if (
|
|
storedX == null
|
|
|| (Math.abs(storedX - 1) < 0.01 && Math.abs(design.x - 1) > 0.01)
|
|
) {
|
|
node["poolBaseScaleX"] = design.x;
|
|
node["poolBaseScaleY"] = design.y;
|
|
}
|
|
} else if (storedX == null) {
|
|
this._markPoolNodeBaseline(node);
|
|
}
|
|
const sx = node["poolBaseScaleX"] ?? 1;
|
|
const sy = node["poolBaseScaleY"] ?? 1;
|
|
node.setScale(sx, sy, 1);
|
|
}
|
|
|
|
/**回收节点 */
|
|
putNode(node: Node) {
|
|
let psss = node.getComponentsInChildren(ParticleSystem2D);
|
|
for (let ps of psss) {
|
|
ps.stopSystem();
|
|
}
|
|
const sks = node.getComponentsInChildren(sp.Skeleton);
|
|
for (const sk of sks) {
|
|
if (!sk?.isValid) continue;
|
|
sk.enabled = false;
|
|
this._resetPooledSpine(sk);
|
|
}
|
|
let pool = this._nodePool.get(node["resKey"]);
|
|
if (!pool) {
|
|
node.destroy();
|
|
} else {
|
|
// 回池前必须失活:否则挂回场景时会以脏 Sprite 状态触发 onEnable / 合批刷新
|
|
node.active = false;
|
|
node.removeFromParent();
|
|
// 避免同一节点重复入池(曾挂回场景却未 detachFromPool)
|
|
for (let i = pool.length - 1; i >= 0; i--) {
|
|
if (pool[i] === node) pool.splice(i, 1);
|
|
}
|
|
pool.push(node);
|
|
}
|
|
}
|
|
|
|
/**加载资源包
|
|
* @param resPkg 资源数组
|
|
* @param onProgress 进度回调
|
|
* @param onComplete 完成回调
|
|
* @param fresh true 时跳过 bundle 内已缓存资源,强制 bundle.load(退战斗/预载 invalidate 后进战斗须用)
|
|
*/
|
|
loadResPakg(
|
|
resPkg: ResPakage,
|
|
onProgress: Function = null,
|
|
onComplete: ((result: LoadResult) => void) | null = null,
|
|
fresh = false,
|
|
) {
|
|
// 同名资源包重复加载前先释放旧引用,避免 _resCache 覆盖导致引用丢失。
|
|
this.clearResPkg(resPkg.name);
|
|
if (fresh) {
|
|
this.evictPackageFromBundles(resPkg);
|
|
}
|
|
const gen = ((this._pkgLoadGen[resPkg.name] ?? 0) + 1);
|
|
this._pkgLoadGen[resPkg.name] = gen;
|
|
this._resCache.set(resPkg.name, resPkg);
|
|
const dirList = resPkg.dirs ?? [];
|
|
const totalCount = resPkg.ress.length + dirList.length;
|
|
let loadCount = 0;
|
|
const failed: string[] = [];
|
|
let staleCompleted = false;
|
|
const isStale = (): boolean => this._pkgLoadGen[resPkg.name] !== gen;
|
|
/** 旧包被 clear/重载:必须立刻 settle,否则调用方 Promise 永挂 */
|
|
const completeIfStale = (): void => {
|
|
if (!isStale() || staleCompleted) return;
|
|
staleCompleted = true;
|
|
if (onComplete) onComplete({ ok: false, failed: ['pkg_replaced'] });
|
|
};
|
|
if (totalCount <= 0) {
|
|
if (onComplete) onComplete({ ok: true, failed: [] });
|
|
return;
|
|
}
|
|
const finishOne = () => {
|
|
completeIfStale();
|
|
if (staleCompleted) return;
|
|
loadCount++;
|
|
if (onProgress) onProgress(loadCount, totalCount);
|
|
if (loadCount >= totalCount) {
|
|
const result: LoadResult = { ok: failed.length === 0, failed };
|
|
if (!result.ok) {
|
|
console.error('[ResourcesManager] loadResPakg partial fail', resPkg.name, failed);
|
|
}
|
|
if (onComplete) onComplete(result);
|
|
}
|
|
};
|
|
for (let i = 0; i < resPkg.ress.length; i++) {
|
|
const conf = resPkg.ress[i];
|
|
if (!conf) {
|
|
finishOne();
|
|
continue;
|
|
}
|
|
if (typeof conf.name !== 'string' || !conf.name) {
|
|
console.error('[ResourcesManager] skip invalid res name', conf.bundle, conf.path, conf.name);
|
|
failed.push(formatLoadKey(conf.bundle ?? '', conf.path ?? '', String(conf.name)));
|
|
finishOne();
|
|
continue;
|
|
}
|
|
const path = conf.path + conf.name;
|
|
let res = this.getRes(conf.name, conf.path, conf.bundle, conf.type) as Asset | null;
|
|
if (res && !res.isValid) res = null;
|
|
if (res) {
|
|
const cachedRes = res as Asset;
|
|
cachedRes.addRef();
|
|
conf.res = cachedRes;
|
|
finishOne();
|
|
continue;
|
|
}
|
|
this.loadBundle(conf.bundle, (b) => {
|
|
completeIfStale();
|
|
if (staleCompleted) return;
|
|
if (!b) {
|
|
failed.push(formatLoadKey(conf.bundle, conf.path, conf.name));
|
|
finishOne();
|
|
return;
|
|
}
|
|
this.loadRes(b, path, (loaded) => {
|
|
completeIfStale();
|
|
if (staleCompleted) {
|
|
// 旧包已被 clear:丢弃本条,避免 decRef 后仍挂在 conf.res
|
|
if (loaded) loaded.decRef();
|
|
return;
|
|
}
|
|
if (loaded) {
|
|
loaded.addRef();
|
|
conf.res = loaded;
|
|
} else {
|
|
failed.push(formatLoadKey(conf.bundle, conf.path, conf.name));
|
|
}
|
|
finishOne();
|
|
}, conf.type, fresh);
|
|
});
|
|
}
|
|
for (const dir of dirList) {
|
|
this.loadBundle(dir.bundle, (b) => {
|
|
completeIfStale();
|
|
if (staleCompleted) return;
|
|
if (!b) {
|
|
failed.push(formatLoadKey(dir.bundle, dir.path, '*'));
|
|
finishOne();
|
|
return;
|
|
}
|
|
this._loadDirAssets(b, dir.path, dir.type, fresh, (ok) => {
|
|
completeIfStale();
|
|
if (staleCompleted) return;
|
|
if (!ok) {
|
|
failed.push(formatLoadKey(dir.bundle, dir.path, '*'));
|
|
}
|
|
finishOne();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从 bundle 缓存剔除资源包内条目(clearResPkg / invalidate 后、fresh load 前)。
|
|
* 避免 loadResPakg 命中 bundle.get 返回已 release 的 SpriteFrame → getGFXTexture 空引用。
|
|
* 局外常驻包(isReleaseable=false,如 HerosRes)持有的资源不释放,否则 getRes 仍返回脏 Prefab,instantiate 报 nil。
|
|
*/
|
|
evictPackageFromBundles(resPkg: ResPakage): void {
|
|
const residentKeys = this._residentAssetKeys();
|
|
const released = createMembershipSet<Asset>();
|
|
for (const conf of resPkg.ress) {
|
|
if (typeof conf.name !== 'string' || !conf.name) continue;
|
|
const key = this.assetKey(conf.name, conf.path, conf.bundle, conf.type);
|
|
if (residentKeys.has(key)) continue;
|
|
const bundle = assetManager.getBundle(conf.bundle);
|
|
if (!bundle) continue;
|
|
let assetPath = conf.path + conf.name;
|
|
if (conf.type === SpriteFrame) {
|
|
assetPath = conf.path + conf.name + '/spriteframe';
|
|
}
|
|
const asset = bundle.get(assetPath, conf.type) as Asset | null;
|
|
if (asset && !released.has(asset)) {
|
|
membershipAdd(released, asset);
|
|
assetManager.releaseAsset(asset);
|
|
}
|
|
try {
|
|
bundle.release(assetPath, conf.type);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] bundle.release failed', conf.bundle, assetPath, String(e));
|
|
}
|
|
this._dropCachedAsset(conf.name, conf.path, conf.bundle, conf.type);
|
|
this._preloadedKeys.delete(key);
|
|
}
|
|
for (const dir of resPkg.dirs ?? []) {
|
|
const bundle = assetManager.getBundle(dir.bundle);
|
|
if (!bundle) continue;
|
|
try {
|
|
bundle.release(dir.path, dir.type);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] bundle.release dir failed', dir.bundle, dir.path, String(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 不可释放资源包里已登记的资源键(大厅 heros 字形等) */
|
|
private _residentAssetKeys(): MembershipSet<string> {
|
|
const keys = createMembershipSet<string>();
|
|
for (const pkg of this._resCache.values()) {
|
|
if (pkg.isReleaseable !== false) continue;
|
|
for (const conf of pkg.ress) {
|
|
if (typeof conf.name !== 'string' || !conf.name) continue;
|
|
membershipAdd(keys, this.assetKey(conf.name, conf.path, conf.bundle, conf.type));
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/** 释放后清掉其它包里指向同一资源的缓存,避免 getRes 拿到已 release 的 Prefab */
|
|
private _dropCachedAsset(name: string, path: string, bundle: string, type: typeof Asset): void {
|
|
for (const pkg of this._resCache.values()) {
|
|
if (pkg.isReleaseable === false) continue;
|
|
for (const conf of pkg.ress) {
|
|
if (conf.name === name && conf.path === path && conf.bundle === bundle && conf.type === type) {
|
|
conf.res = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 单条资源预载(供战斗后台分帧队列使用,每次只发起一项 IO) */
|
|
preLoadOne(conf: ResInfo): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
if (typeof conf.name !== 'string' || !conf.name) {
|
|
console.error('[ResourcesManager] skip invalid preload res name', conf.bundle, conf.path, conf.name);
|
|
resolve();
|
|
return;
|
|
}
|
|
const path = conf.path + conf.name;
|
|
const res = this.getRes(conf.name, conf.path, conf.bundle, conf.type);
|
|
if (res) {
|
|
resolve();
|
|
return;
|
|
}
|
|
this.loadBundle(conf.bundle, (b) => {
|
|
if (!b) {
|
|
resolve();
|
|
return;
|
|
}
|
|
this.preLoadRes(b, path, conf.type, (loaded) => {
|
|
if (loaded) {
|
|
loaded.addRef();
|
|
membershipAdd(this._preloadedKeys, this.assetKey(conf.name, conf.path, conf.bundle, conf.type));
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**预加载资源包 */
|
|
preLoadResPakg(resPkg: ResPakage, onProgress: Function = null, onComplete: ((result: LoadResult) => void) | null = null) {
|
|
const dirList = resPkg.dirs ?? [];
|
|
let totalCount = resPkg.ress.length + dirList.length;
|
|
let loadCount = 0;
|
|
let allCount = totalCount;
|
|
const failed: string[] = [];
|
|
if (totalCount <= 0) {
|
|
if (onComplete) onComplete({ ok: true, failed: [] });
|
|
return;
|
|
}
|
|
let fun = () => {
|
|
loadCount++;
|
|
if (onProgress) onProgress(loadCount, allCount);
|
|
if (loadCount >= allCount) {
|
|
if (onComplete) onComplete({ ok: failed.length === 0, failed });
|
|
}
|
|
}
|
|
for (let i = 0; i < resPkg.ress.length; i++) {
|
|
let conf = resPkg.ress[i];
|
|
if (!conf) {
|
|
fun();
|
|
continue;
|
|
}
|
|
if (typeof conf.name !== 'string' || !conf.name) {
|
|
console.error('[ResourcesManager] skip invalid preload res name', conf.bundle, conf.path, conf.name);
|
|
failed.push(formatLoadKey(conf.bundle ?? '', conf.path ?? '', String(conf.name)));
|
|
fun();
|
|
continue;
|
|
}
|
|
let path = conf.path + conf.name;
|
|
let res = this.getRes(conf.name, conf.path, conf.bundle, conf.type);
|
|
if (res) {
|
|
fun();
|
|
continue;
|
|
}
|
|
this.loadBundle(conf.bundle, (b) => {
|
|
if (!b) {
|
|
failed.push(formatLoadKey(conf.bundle, conf.path, conf.name));
|
|
fun();
|
|
return;
|
|
}
|
|
this.preLoadRes(b, path, conf.type, (loaded) => {
|
|
if (loaded) {
|
|
loaded.addRef();
|
|
membershipAdd(this._preloadedKeys, this.assetKey(conf.name, conf.path, conf.bundle, conf.type));
|
|
} else {
|
|
failed.push(formatLoadKey(conf.bundle, conf.path, conf.name));
|
|
}
|
|
fun();
|
|
});
|
|
});
|
|
}
|
|
for (const dir of dirList) {
|
|
this.loadBundle(dir.bundle, (b) => {
|
|
if (!b) {
|
|
failed.push(formatLoadKey(dir.bundle, dir.path, '*'));
|
|
fun();
|
|
return;
|
|
}
|
|
this._loadDirAssets(b, dir.path, dir.type, false, (ok) => {
|
|
if (!ok) {
|
|
failed.push(formatLoadKey(dir.bundle, dir.path, '*'));
|
|
}
|
|
fun();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**清理资源 */
|
|
clearResPkg(respkgName: ResPkgName | string) {
|
|
let respkg = this._resCache.get(respkgName);
|
|
if (!respkg) return;
|
|
if (!respkg.isReleaseable) {
|
|
return;
|
|
}
|
|
const bundleNames = createMembershipSet<string>();
|
|
for (const res of respkg.ress) {
|
|
if (typeof res.bundle === 'string' && res.bundle) {
|
|
membershipAdd(bundleNames, res.bundle);
|
|
}
|
|
}
|
|
for (const dir of respkg.dirs ?? []) {
|
|
if (typeof dir.bundle === 'string' && dir.bundle) {
|
|
membershipAdd(bundleNames, dir.bundle);
|
|
}
|
|
}
|
|
// bundle 即将 decRef/unload:先销毁池中对应实例,避免复用已失效 SpriteFrame
|
|
this.purgeNodePoolForBundles(membershipToArray(bundleNames));
|
|
for (let res of respkg.ress) {
|
|
res.res?.decRef();
|
|
if (res.res && res.res.refCount <= 0) {
|
|
res.res = null;
|
|
}
|
|
}
|
|
const names = membershipToArray(bundleNames);
|
|
for (let i = 0; i < names.length; i++) {
|
|
BundleLifecycle.getInstance().releaseBundleOwner(names[i], 'res');
|
|
}
|
|
this._resCache.delete(respkg.name);
|
|
}
|
|
|
|
/** 销毁指定 bundle 下节点池中的实例(bundle unload 前调用) */
|
|
purgeNodePoolForBundles(bundleNames: readonly string[]): void {
|
|
const prefixes: string[] = [];
|
|
for (let i = 0; i < bundleNames.length; i++) {
|
|
if (bundleNames[i]) prefixes.push(bundleNames[i]);
|
|
}
|
|
prefixes.sort((a, b) => b.length - a.length);
|
|
if (prefixes.length === 0) return;
|
|
const poolKeys = mapKeysToArray(this._nodePool);
|
|
for (let i = 0; i < poolKeys.length; i++) {
|
|
const key = poolKeys[i];
|
|
const pool = this._nodePool.get(key);
|
|
if (!pool) continue;
|
|
const owner = prefixes.find((prefix) => key.startsWith(prefix));
|
|
if (!owner) continue;
|
|
for (let n = 0; n < pool.length; n++) {
|
|
const node = pool[n];
|
|
if (node?.isValid) node.destroy();
|
|
}
|
|
this._nodePool.delete(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 释放资源包预热占用:丢掉 preload addRef,无人持有则剔除 bundle 缓存。
|
|
* 打开中的面板仍持有引用时不释放。
|
|
*/
|
|
invalidatePreloadForPackage(resPkg: ResPakage): void {
|
|
for (const conf of resPkg.ress) {
|
|
if (typeof conf.name !== 'string' || !conf.name) continue;
|
|
const key = this.assetKey(conf.name, conf.path, conf.bundle, conf.type);
|
|
const hadPreload = this._preloadedKeys.delete(key);
|
|
const bundle = assetManager.getBundle(conf.bundle);
|
|
if (!bundle) continue;
|
|
const assetPath = conf.path + conf.name;
|
|
const asset = bundle.get(assetPath, conf.type) as Asset | null;
|
|
if (!asset) continue;
|
|
if (!asset.isValid) {
|
|
this._evictInvalidAsset(bundle, assetPath, conf.type, asset);
|
|
continue;
|
|
}
|
|
if (hadPreload) {
|
|
asset.decRef();
|
|
if (asset.refCount > 0) asset.decRef();
|
|
}
|
|
if (asset.refCount > 0) continue;
|
|
try {
|
|
bundle.release(assetPath, conf.type);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] bundle.release after invalidate', conf.bundle, assetPath, String(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
clearNodePool() {
|
|
for (let pool of this._nodePool.values()) {
|
|
for (let node of pool) {
|
|
node.destroy();
|
|
}
|
|
pool = [];
|
|
}
|
|
this._nodePool.clear();
|
|
}
|
|
|
|
/** 内存诊断:已登记资源包与仍持有引用的资源数量 */
|
|
getMemoryDiagStats(): {
|
|
pkgCount: number;
|
|
assetCount: number;
|
|
preloadedCount: number;
|
|
nodePoolKeys: number;
|
|
nodePoolNodes: number;
|
|
} {
|
|
let assetCount = 0;
|
|
for (const pkg of this._resCache.values()) {
|
|
for (const info of pkg.ress) {
|
|
if (info.res) assetCount++;
|
|
}
|
|
}
|
|
let nodePoolNodes = 0;
|
|
for (const pool of this._nodePool.values()) {
|
|
nodePoolNodes += pool.length;
|
|
}
|
|
return {
|
|
pkgCount: this._resCache.size,
|
|
assetCount,
|
|
preloadedCount: this._preloadedKeys.size,
|
|
nodePoolKeys: this._nodePool.size,
|
|
nodePoolNodes,
|
|
};
|
|
}
|
|
|
|
loadBundle(bundleName, onComplete: Function = null, owner = 'res') {
|
|
void BundleLifecycle.getInstance().ensureLoaded(bundleName).then((bundle) => {
|
|
if (bundle) {
|
|
BundleLifecycle.getInstance().acquire(bundleName, owner);
|
|
}
|
|
if (onComplete) onComplete(bundle);
|
|
});
|
|
}
|
|
|
|
/** 加载单个资源并 addRef(供 AssetService 晋升预加载资源) */
|
|
loadAsset(
|
|
bundleName: string,
|
|
path: string,
|
|
name: string,
|
|
type: typeof Asset,
|
|
onComplete: (asset: Asset | null) => void,
|
|
): void {
|
|
const cached = this.getRes(name, path, bundleName, type) as Asset | null;
|
|
if (cached) {
|
|
cached.addRef();
|
|
onComplete(cached);
|
|
return;
|
|
}
|
|
this.loadBundle(bundleName, (b) => {
|
|
if (!b) {
|
|
onComplete(null);
|
|
return;
|
|
}
|
|
const assetPath = path + name;
|
|
this.loadRes(b, assetPath, (res: Asset | null) => {
|
|
if (res) res.addRef();
|
|
onComplete(res);
|
|
}, type);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 加载资源。第二参 fromCache=true 表示命中缓存,调用方须自行 addRef。
|
|
* @param bundle 分包
|
|
* @param assetPath 资源路径
|
|
* @param onComplete 完成回调
|
|
* @param type 资源类型
|
|
*/
|
|
loadResPublic(bundle: AssetManager.Bundle, assetPath: string, onComplete: ((asset: Asset | null, fromCache?: boolean) => void) | null = null, type = Asset) {
|
|
this.loadRes(bundle, assetPath, onComplete, type);
|
|
}
|
|
|
|
/**
|
|
* bundle 缓存中仍有效的资源;失效则剔除,避免后续打开永久失败。
|
|
* @param bundle 分包
|
|
* @param assetPath 资源路径
|
|
* @param type 资源类型
|
|
*/
|
|
private _validBundleAsset(bundle: AssetManager.Bundle, assetPath: string, type: typeof Asset): Asset | null {
|
|
const cached = bundle.get(assetPath, type) as Asset | null;
|
|
if (!cached) return null;
|
|
if (cached.isValid) return cached;
|
|
this._evictInvalidAsset(bundle, assetPath, type, cached);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 剔除 bundle.get 仍返回的已销毁资源。
|
|
* @param bundle 分包
|
|
* @param assetPath 资源路径
|
|
* @param type 资源类型
|
|
* @param asset 失效资源
|
|
*/
|
|
private _evictInvalidAsset(bundle: AssetManager.Bundle, assetPath: string, type: typeof Asset, asset: Asset): void {
|
|
console.error('[ResourcesManager] cached asset invalid, evict', bundle.name, assetPath);
|
|
try {
|
|
assetManager.releaseAsset(asset);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] releaseAsset invalid', bundle.name, assetPath, String(e));
|
|
}
|
|
try {
|
|
bundle.release(assetPath, type);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] bundle.release invalid', bundle.name, assetPath, String(e));
|
|
}
|
|
}
|
|
|
|
private loadRes(bundle: AssetManager.Bundle, assetPath, onComplete: Function = null, type = Asset, forceLoad = false) {
|
|
if (!forceLoad) {
|
|
const cached = this._validBundleAsset(bundle, assetPath, type);
|
|
if (cached) {
|
|
if (onComplete) onComplete(cached, true);
|
|
return;
|
|
}
|
|
}
|
|
const waitKey = `${bundle.name}|${assetPath}|${type.name}`;
|
|
const inflight = this._assetLoadWait[waitKey];
|
|
if (inflight && !forceLoad) {
|
|
void inflight.then((asset) => {
|
|
const valid = asset?.isValid ? asset : this._validBundleAsset(bundle, assetPath, type);
|
|
if (onComplete) onComplete(valid, false);
|
|
});
|
|
return;
|
|
}
|
|
const native = new Promise<Asset | null>((resolve) => {
|
|
bundle.load(assetPath, type, (err, asset) => {
|
|
if (this._assetLoadWait[waitKey] === native) {
|
|
this._assetLoadWait[waitKey] = undefined;
|
|
}
|
|
if (err) {
|
|
console.error('[ResourcesManager] bundle.load fail', bundle.name, assetPath, err.message ?? err);
|
|
resolve(null);
|
|
return;
|
|
}
|
|
resolve(asset);
|
|
});
|
|
});
|
|
this._assetLoadWait[waitKey] = native;
|
|
let done = false;
|
|
const finish = (asset: Asset | null, fromCache: boolean): void => {
|
|
if (done) return;
|
|
done = true;
|
|
if (onComplete) onComplete(asset, fromCache);
|
|
};
|
|
const timer = setTimeout(() => {
|
|
const late = this._validBundleAsset(bundle, assetPath, type);
|
|
if (!late) {
|
|
console.error('[ResourcesManager] bundle.load timeout', bundle.name, assetPath);
|
|
}
|
|
finish(late, !!late);
|
|
}, LoadRecoveryConfig.prefabTimeoutMs);
|
|
void native.then((asset) => {
|
|
clearTimeout(timer);
|
|
if (asset?.isValid) {
|
|
finish(asset, false);
|
|
return;
|
|
}
|
|
const late = this._validBundleAsset(bundle, assetPath, type);
|
|
if (!late && asset && !asset.isValid) {
|
|
console.error('[ResourcesManager] bundle.load 预制无效', bundle.name, assetPath);
|
|
}
|
|
finish(late, !!late);
|
|
});
|
|
}
|
|
|
|
/** bundle.loadDir:目录内直接子资源(非递归) */
|
|
private _loadDirAssets(
|
|
bundle: AssetManager.Bundle,
|
|
dirPath: string,
|
|
type: typeof Asset,
|
|
forceLoad: boolean,
|
|
onComplete: (ok: boolean, assets?: Asset[]) => void,
|
|
): void {
|
|
if (forceLoad) {
|
|
try {
|
|
bundle.release(dirPath, type);
|
|
} catch (e) {
|
|
if (DEV) console.log('[ResourcesManager] bundle.release dir before loadDir', bundle.name, dirPath, String(e));
|
|
}
|
|
}
|
|
const waitKey = `${bundle.name}|${dirPath}|${type.name}`;
|
|
const inflight = this._dirLoadWait[waitKey];
|
|
if (inflight && !forceLoad) {
|
|
void inflight.then((r) => onComplete(r.ok, r.assets));
|
|
return;
|
|
}
|
|
const native = new Promise<{ ok: boolean; assets?: Asset[] }>((resolve) => {
|
|
bundle.loadDir(dirPath, type, (err, assets) => {
|
|
if (this._dirLoadWait[waitKey] === native) {
|
|
this._dirLoadWait[waitKey] = undefined;
|
|
}
|
|
if (err || !assets || assets.length <= 0) {
|
|
console.error('[ResourcesManager] bundle.loadDir fail', bundle.name, dirPath, err?.message ?? err);
|
|
resolve({ ok: false });
|
|
return;
|
|
}
|
|
for (const asset of assets) {
|
|
asset.addRef();
|
|
}
|
|
resolve({ ok: true, assets });
|
|
});
|
|
});
|
|
this._dirLoadWait[waitKey] = native;
|
|
let done = false;
|
|
const finish = (ok: boolean, assets?: Asset[]): void => {
|
|
if (done) return;
|
|
done = true;
|
|
onComplete(ok, assets);
|
|
};
|
|
const timer = setTimeout(() => {
|
|
console.error('[ResourcesManager] bundle.loadDir timeout', bundle.name, dirPath);
|
|
finish(false);
|
|
}, LoadRecoveryConfig.dirTimeoutMs);
|
|
void native.then((r) => {
|
|
clearTimeout(timer);
|
|
finish(r.ok, r.assets);
|
|
});
|
|
}
|
|
|
|
/** 预加载资源(走 loadRes 同路径合并,避免与 openPanel 双飞丢回调) */
|
|
preLoadRes(
|
|
bundle: AssetManager.Bundle,
|
|
assetPath: string,
|
|
type: typeof Asset,
|
|
onComplete: ((asset: Asset | null) => void) | null = null,
|
|
) {
|
|
this.loadRes(bundle, assetPath, (asset: Asset | null) => {
|
|
if (onComplete) onComplete(asset);
|
|
}, type);
|
|
}
|
|
}
|
|
|
|
|
|
|