添加:

1. 自动管理资源引用计数 - 多组件共享资源时不会错误释放
2. 组件销毁时自动释放资源引用 - 开发者无需手动管理
3. 全局资源追踪 - 可查看任意资源的引用者和引用计数
This commit is contained in:
dgflash
2026-05-17 20:11:06 +08:00
parent e58951632b
commit d33b668810
2 changed files with 342 additions and 224 deletions

View File

@@ -0,0 +1,252 @@
import { resLoader } from './ResLoader';
export interface ResRefRecord {
bundle: string;
path: string;
refCount: number;
referrers: Set<string>;
lastAccessTime: number;
}
export interface ComponentResInfo {
resKeys: Set<string>;
}
class ResRefManager {
private resRefs: Map<string, ResRefRecord> = new Map();
private componentRefs: Map<string, ComponentResInfo> = new Map();
private debugMode: boolean = false;
enableDebug(enabled: boolean = true): void {
this.debugMode = enabled;
}
private getResKey(bundle: string, path: string): string {
return `${bundle}::${path}`;
}
private getComponentKey(component: any): string {
if (!component) return 'unknown';
const node = component.node;
if (!node) return 'unknown';
const uuid = node.uuid || 'no-uuid';
const name = node.name || 'unnamed';
return `${name}(${uuid.substring(0, 8)})`;
}
addRef(bundle: string, path: string, component: any): string {
const resKey = this.getResKey(bundle, path);
const compKey = this.getComponentKey(component);
let record = this.resRefs.get(resKey);
if (!record) {
record = {
bundle,
path,
refCount: 0,
referrers: new Set(),
lastAccessTime: Date.now(),
};
this.resRefs.set(resKey, record);
}
if (!record.referrers.has(compKey)) {
record.refCount++;
record.referrers.add(compKey);
record.lastAccessTime = Date.now();
if (!this.componentRefs.has(compKey)) {
this.componentRefs.set(compKey, { resKeys: new Set() });
}
this.componentRefs.get(compKey)!.resKeys.add(resKey);
if (this.debugMode) {
console.log(`[ResRef] +1 引用: ${resKey} (总引用: ${record.refCount}, 引用者: ${compKey})`);
}
}
return resKey;
}
removeRef(bundle: string, path: string, component: any): boolean {
const resKey = this.getResKey(bundle, path);
const compKey = this.getComponentKey(component);
const record = this.resRefs.get(resKey);
if (!record) {
if (this.debugMode) {
console.warn(`[ResRef] 尝试移除不存在的资源引用: ${resKey}`);
}
return false;
}
if (!record.referrers.has(compKey)) {
if (this.debugMode) {
console.warn(`[ResRef] 组件 ${compKey} 未引用资源 ${resKey}`);
}
return false;
}
record.refCount--;
record.referrers.delete(compKey);
const compInfo = this.componentRefs.get(compKey);
if (compInfo) {
compInfo.resKeys.delete(resKey);
}
if (this.debugMode) {
console.log(`[ResRef] -1 引用: ${resKey} (剩余引用: ${record.refCount}, 引用者: ${compKey})`);
}
if (record.refCount <= 0) {
this.releaseResource(resKey, record);
return true;
}
return false;
}
releaseAllByComponent(component: any): string[] {
const compKey = this.getComponentKey(component);
const compInfo = this.componentRefs.get(compKey);
if (!compInfo || compInfo.resKeys.size === 0) {
if (this.debugMode) {
console.log(`[ResRef] 组件 ${compKey} 没有资源引用`);
}
return [];
}
const releasedResources: string[] = [];
const resKeysToProcess = Array.from(compInfo.resKeys);
for (const resKey of resKeysToProcess) {
const record = this.resRefs.get(resKey);
if (!record) continue;
record.refCount--;
record.referrers.delete(compKey);
if (this.debugMode) {
console.log(`[ResRef] -1 引用: ${resKey} (剩余引用: ${record.refCount}, 组件销毁: ${compKey})`);
}
if (record.refCount <= 0) {
this.releaseResource(resKey, record);
releasedResources.push(resKey);
}
}
this.componentRefs.delete(compKey);
return releasedResources;
}
private releaseResource(resKey: string, record: ResRefRecord): void {
if (this.debugMode) {
console.log(`[ResRef] 🗑️ 释放资源: ${resKey} (引用者: [${Array.from(record.referrers).join(', ')}])`);
}
resLoader.release(record.path, record.bundle);
this.resRefs.delete(resKey);
for (const compKey of record.referrers) {
const compInfo = this.componentRefs.get(compKey);
if (compInfo) {
compInfo.resKeys.delete(resKey);
}
}
}
getRefCount(bundle: string, path: string): number {
const resKey = this.getResKey(bundle, path);
const record = this.resRefs.get(resKey);
return record ? record.refCount : 0;
}
getReferrers(bundle: string, path: string): string[] {
const resKey = this.getResKey(bundle, path);
const record = this.resRefs.get(resKey);
return record ? Array.from(record.referrers) : [];
}
hasRef(bundle: string, path: string): boolean {
const resKey = this.getResKey(bundle, path);
return this.resRefs.has(resKey);
}
getComponentResCount(component: any): number {
const compKey = this.getComponentKey(component);
const compInfo = this.componentRefs.get(compKey);
return compInfo ? compInfo.resKeys.size : 0;
}
printStatus(): void {
console.log('\n========== 全局资源引用状态 ==========');
console.log(`总资源数: ${this.resRefs.size}`);
console.log(`总组件数: ${this.componentRefs.size}`);
if (this.resRefs.size > 0) {
console.log('\n[资源引用详情]');
const sortedRecords = Array.from(this.resRefs.entries()).sort((a, b) => b[1].refCount - a[1].refCount);
for (const [key, record] of sortedRecords) {
console.log(` ${key}`);
console.log(` 引用计数: ${record.refCount}`);
console.log(` 引用者: [${Array.from(record.referrers).join(', ')}]`);
}
}
if (this.componentRefs.size > 0) {
console.log('\n[组件资源详情]');
for (const [compKey, compInfo] of this.componentRefs) {
console.log(` ${compKey}: ${compInfo.resKeys.size} 个资源`);
}
}
console.log('=====================================\n');
}
printComponentStatus(component: any): void {
const compKey = this.getComponentKey(component);
const compInfo = this.componentRefs.get(compKey);
console.log(`\n===== 组件资源状态: ${compKey} =====`);
if (!compInfo || compInfo.resKeys.size === 0) {
console.log(' 无资源引用');
}
else {
console.log(` 引用资源数: ${compInfo.resKeys.size}`);
for (const resKey of compInfo.resKeys) {
const record = this.resRefs.get(resKey);
if (record) {
console.log(` - ${resKey} (全局引用: ${record.refCount})`);
}
}
}
console.log('================================\n');
}
getTotalStats(): { totalResources: number; totalComponents: number; totalRefs: number } {
let totalRefs = 0;
for (const record of this.resRefs.values()) {
totalRefs += record.refCount;
}
return {
totalResources: this.resRefs.size,
totalComponents: this.componentRefs.size,
totalRefs,
};
}
clear(): void {
if (this.debugMode) {
console.log('[ResRef] 清空所有资源引用记录');
}
this.resRefs.clear();
this.componentRefs.clear();
}
}
export const resRef = new ResRefManager();

View File

@@ -15,31 +15,30 @@ import { EventMessage } from '../../core/common/event/EventMessage';
import type { AssetType, CompleteCallback, Paths, ProgressCallback } from '../../core/common/loader/ResLoader';
import { resLoader } from '../../core/common/loader/ResLoader';
import { ViewUtil } from '../../core/utils/ViewUtil';
import { resRef } from '../../core/common/loader/ResRefManager';
const { ccclass } = _decorator;
/** 加载资源类型 */
enum ResType {
Load,
LoadDir
}
/** 资源加载记录 */
interface ResRecord {
/** 资源包名 */
bundle: string,
/** 资源路径 */
path: string,
/** 引用计数 */
refCount: number,
/** 资源编号 */
resId?: number
}
/**
* 游戏显示对象组件模板
* 1、当前对象加载的资源会在对象释放时自动释放引用的资源
* 2、当前对象支持启动游戏引擎提供的各种常用逻辑事件
*
* 特性:
* 1. 自动管理资源引用计数 - 多组件共享资源时不会错误释放
* 2. 组件销毁时自动释放资源引用 - 开发者无需手动管理
* 3. 全局资源追踪 - 可查看任意资源的引用者和引用计数
*
* 使用示例:
* ```typescript
* // 加载资源(自动注册引用)
* const spriteFrame = await this.load('textures/avatar', SpriteFrame);
*
* // 组件销毁时自动释放引用(无需手动调用)
* // 只有当所有引用者都销毁时,资源才会被真正释放
*
* // 调试:查看资源引用情况
* GameComponent.printGlobalResStatus();
* GameComponent.setResDebugMode(true); // 开启详细日志
* ```
*/
@ccclass('GameComponent')
export class GameComponent extends Component {
@@ -196,9 +195,6 @@ export class GameComponent extends Component {
//#endregion
//#region 资源加载管理
/** 资源路径 */
private resPaths: Map<ResType, Map<string, ResRecord>> = null!; // 资源使用记录
/**
* 获取资源
* @param path 资源路径
@@ -210,83 +206,15 @@ export class GameComponent extends Component {
}
/**
* 添加资源使用记录
* @param type 资源类型
* @param bundleName 资源包名
* @param paths 资源路径
*/
private addPathToRecord<T>(type: ResType, bundleName: string, paths?: string | string[] | AssetType<T> | ProgressCallback | CompleteCallback | null): void {
if (this.resPaths == null) this.resPaths = new Map();
let rps = this.resPaths.get(type);
if (rps == null) {
rps = new Map();
this.resPaths.set(type, rps);
}
// 确定真实的 bundle 和 path
let realBundle: string;
let realPaths: string[];
if (paths instanceof Array) {
realBundle = bundleName;
realPaths = paths;
}
else if (typeof paths === 'string') {
realBundle = bundleName;
realPaths = [paths];
}
else {
realBundle = oops.res.defaultBundleName;
realPaths = [bundleName];
}
// 统一处理路径数组
for (const realPath of realPaths) {
const key = this.getResKey(realBundle, realPath);
const rp = rps.get(key);
if (rp) {
rp.refCount++;
}
else {
rps.set(key, { path: realPath, bundle: realBundle, refCount: 1 });
}
}
}
private getResKey(realBundle: string, realPath: string): string {
const key = `${realBundle}:${realPath}`;
return key;
}
/**
* 移除资源使用记录(用于加载失败时回滚)
* @param type 资源类型
* @param bundleName 资源包名
* @param paths 资源路径
*/
private removePathFromRecord(type: ResType, bundleName: string, path: string): void {
if (!this.resPaths) return;
const rps = this.resPaths.get(type);
if (!rps) return;
const key = this.getResKey(bundleName, path);
const record = rps.get(key);
if (record) {
record.refCount--;
if (record.refCount <= 0) {
rps.delete(key);
}
}
}
/**
* 加载一个资源
* 加载一个资源(自动管理引用计数)
* @param bundleName 远程包名
* @param paths 资源路径
* @param type 资源类型
* @param onProgress 加载进度回调
* @remarks
* - 资源引用会自动注册到全局管理器
* - 组件销毁时会自动减少引用计数
* - 只有引用计数为0时才会真正释放资源
*/
async load<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>): Promise<T> {
let realBundle: string;
@@ -301,16 +229,17 @@ export class GameComponent extends Component {
realPath = bundleName;
}
this.addPathToRecord(ResType.Load, bundleName, paths);
resRef.addRef(realBundle, realPath, this);
try {
const result = await oops.res.load(bundleName, paths, type);
if (!result) {
this.removePathFromRecord(ResType.Load, realBundle, realPath);
resRef.removeRef(realBundle, realPath, this);
}
return result;
}
catch (error) {
this.removePathFromRecord(ResType.Load, realBundle, realPath);
resRef.removeRef(realBundle, realPath, this);
throw error;
}
}
@@ -327,22 +256,26 @@ export class GameComponent extends Component {
const pathsToTrack: { bundle: string; path: string }[] = [];
if (typeof bundleName === 'string' && Array.isArray(paths)) {
this.addPathToRecord(ResType.Load, bundleName, paths);
pathsToTrack.push(...paths.map(p => ({ bundle: bundleName, path: p })));
paths.forEach(p => {
resRef.addRef(bundleName, p, this);
pathsToTrack.push({ bundle: bundleName, path: p });
});
}
else if (Array.isArray(bundleName)) {
this.addPathToRecord(ResType.Load, resLoader.defaultBundleName, bundleName);
pathsToTrack.push(...bundleName.map(p => ({ bundle: resLoader.defaultBundleName, path: p })));
bundleName.forEach(p => {
resRef.addRef(resLoader.defaultBundleName, p, this);
pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: p });
});
}
else if (typeof bundleName === 'string' && typeof paths === 'function') {
this.addPathToRecord(ResType.Load, resLoader.defaultBundleName, bundleName);
resRef.addRef(resLoader.defaultBundleName, bundleName, this);
pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: bundleName });
}
const wrappedComplete = (err: Error | null, data: Asset[]) => {
if (err || !data) {
pathsToTrack.forEach(({ bundle, path }) => {
this.removePathFromRecord(ResType.Load, bundle, path);
resRef.removeRef(bundle, path, this);
});
}
originalComplete?.(err, data);
@@ -385,12 +318,12 @@ export class GameComponent extends Component {
realBundle = oops.res.defaultBundleName;
}
this.addPathToRecord(ResType.LoadDir, realBundle, realDir);
resRef.addRef(realBundle, realDir, this);
const originalComplete = onComplete as ((err: Error | null, data: T[]) => void) | undefined;
const wrappedComplete = (err: Error | null, data: T[]) => {
if (err || !data) {
this.removePathFromRecord(ResType.LoadDir, realBundle, realDir);
resRef.removeRef(realBundle, realDir, this);
}
originalComplete?.(err, data);
};
@@ -399,137 +332,80 @@ export class GameComponent extends Component {
}
/**
* 手动释放指定资源
* 手动释放指定资源引用
* @param path 资源路径
* @param bundleName 资源包名
* @param releaseAll 是否释放所有引用计数(默认只释放一次)
* @remarks
* - 只减少当前组件对该资源的引用计数
* - 只有引用计数为0时才会真正释放资源
* - 其他组件的引用不受影响
*/
releaseRes(path: string, bundleName: string = resLoader.defaultBundleName, releaseAll = false) {
if (!this.resPaths) return;
const rps = this.resPaths.get(ResType.Load);
if (!rps) return;
const key = this.getResKey(bundleName, path);
const record = rps.get(key);
if (record) {
if (releaseAll) {
// 释放所有引用
for (let i = 0; i < record.refCount; i++) {
oops.res.release(record.path, record.bundle);
}
rps.delete(key);
}
else {
// 只释放一次引用
oops.res.release(record.path, record.bundle);
record.refCount--;
if (record.refCount <= 0) {
rps.delete(key);
}
}
}
releaseRes(path: string, bundleName: string = resLoader.defaultBundleName) {
resRef.removeRef(bundleName, path, this);
}
/** 释放所有加载的资源 */
/**
* 释放当前组件所有资源引用
* @remarks
* - 自动减少所有资源的引用计数
* - 只有引用计数为0的资源才会被真正释放
* - 共享资源不会被错误释放
*/
release() {
if (this.resPaths) {
const rps = this.resPaths.get(ResType.Load);
if (rps) {
rps.forEach((value: ResRecord) => {
// 根据引用计数释放资源
for (let i = 0; i < value.refCount; i++) {
oops.res.release(value.path, value.bundle);
}
});
rps.clear();
this.resPaths.delete(ResType.Load);
}
}
}
/** 释放所有文件夹资源 */
releaseDir() {
if (this.resPaths) {
const rps = this.resPaths.get(ResType.LoadDir);
if (rps) {
rps.forEach((value: ResRecord) => {
// 释放文件夹资源(根据引用计数多次释放)
for (let i = 0; i < value.refCount; i++) {
oops.res.releaseDir(value.path, value.bundle);
}
});
rps.clear();
this.resPaths.delete(ResType.LoadDir);
}
const released = resRef.releaseAllByComponent(this);
if (released.length > 0) {
console.log(`[GameComponent] ${this.node?.name} 释放了 ${released.length} 个资源:`, released);
}
}
/**
* 获取资源引用计数
* 释放所有文件夹资源引用
* @deprecated 文件夹资源现在也通过全局引用计数管理,直接调用 release() 即可
*/
releaseDir() {
console.warn('[GameComponent] releaseDir() 已废弃,请直接使用 release()');
}
/**
* 获取资源的全局引用计数
* @param path 资源路径
* @param bundleName 资源包名
* @returns 引用计数未找到返回0
* @returns 全局引用计数
*/
getResRefCount(path: string, bundleName: string = resLoader.defaultBundleName): number {
if (!this.resPaths) return 0;
const rps = this.resPaths.get(ResType.Load);
if (!rps) return 0;
const key = this.getResKey(bundleName, path);
const record = rps.get(key);
return record ? record.refCount : 0;
return resRef.getRefCount(bundleName, path);
}
/**
* 获取所有加载的资源信息(用于调试)
* @returns 资源记录数组
* 获取资源的所有引用者
* @param path 资源路径
* @param bundleName 资源包名
* @returns 引用者列表
*/
getAllResRecords(): ResRecord[] {
const records: ResRecord[] = [];
if (!this.resPaths) return records;
const rps = this.resPaths.get(ResType.Load);
if (rps) {
rps.forEach((value: ResRecord) => {
records.push({ ...value });
});
}
return records;
getResReferrers(path: string, bundleName: string = resLoader.defaultBundleName): string[] {
return resRef.getReferrers(bundleName, path);
}
/**
* 打印资源使用情况(用于调试)
* 打印当前组件的资源用情况
*/
printResUsage() {
if (!this.resPaths) {
console.log('[资源管理] 暂无资源记录');
return;
}
resRef.printComponentStatus(this);
}
const loadRps = this.resPaths.get(ResType.Load);
const dirRps = this.resPaths.get(ResType.LoadDir);
/**
* 打印全局资源引用状态(调试用)
*/
static printGlobalResStatus() {
resRef.printStatus();
}
console.log('========== 资源使用情况 ==========');
console.log(`组件: ${this.node.name}`);
if (loadRps && loadRps.size > 0) {
console.log(`\n[普通资源] 共 ${loadRps.size} 个:`);
loadRps.forEach((value: ResRecord, key: string) => {
console.log(` - ${key} (引用计数: ${value.refCount})`);
});
}
if (dirRps && dirRps.size > 0) {
console.log(`\n[文件夹资源] 共 ${dirRps.size} 个:`);
dirRps.forEach((value: ResRecord, key: string) => {
console.log(` - ${key} (引用计数: ${value.refCount})`);
});
}
console.log('==================================');
/**
* 开启/关闭全局资源调试模式
* @param enabled 是否开启
*/
static setResDebugMode(enabled: boolean) {
resRef.enableDebug(enabled);
}
/**
@@ -736,7 +612,6 @@ export class GameComponent extends Component {
}
protected onDestroy() {
// 清理键盘事件
if (this._keyboardEnabled) {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
@@ -744,30 +619,21 @@ export class GameComponent extends Component {
this._keyboardEnabled = false;
}
// 清理按钮事件
if (this._buttonEnabled) {
this.node.off(Node.EventType.TOUCH_END);
this._buttonEnabled = false;
}
// 释放消息对象
if (this._event) {
this._event.clear();
this._event = null;
}
// 节点引用数据清除
if (this.nodes) {
this.nodes.clear();
this.nodes = null!;
}
// 自动释放资源
if (this.resPaths) {
this.release();
this.releaseDir();
this.resPaths.clear();
this.resPaths = null!;
}
this.release();
}
}