Compare commits

...

6 Commits

6 changed files with 1111 additions and 739 deletions

View File

@@ -0,0 +1,5 @@
declare module "cc" {
interface SpriteFrame {
remoteUrl?: string;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "22f3e111-a4f8-4e05-aa61-cbea5c1b9cc1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,193 @@
import { nsStringUtils } from "@taqu/Util/StringUtils";
import { isValid } from "cc";
import { assetManager, ImageAsset, Texture2D } from "cc";
import { SpriteFrame } from "cc";
import { UITransform } from "cc";
import { Sprite } from "cc";
import { _decorator } from "cc";
import { EDITOR } from "cc/env";
const { ccclass, property } = _decorator;
@ccclass("CCRemoteSprite")
export class CCRemoteSprite extends Sprite {
private static _remoteSpriteCache: Map<string, SpriteFrame> = new Map();
private static _loadingSpriteCache: Map<string, Promise<SpriteFrame>> = new Map();
private static _checkTimer: NodeJS.Timeout;
/**
* 远程图片URL
*/
@property({
displayName: "远程图片URL",
tooltip: "远程图片的URL地址",
visible: true
})
private _remoteUrl: string = "";
public get remoteUrl(): string {
return this._remoteUrl;
}
public set remoteUrl(value: string) {
const newValue = value || "";
if (this._remoteUrl !== newValue) {
this._remoteUrl = newValue;
if (nsStringUtils.isEmpty(this._remoteUrl)) {
this.release();
return;
}
if (this.spriteFrame == null || this._remoteUrl !== this.spriteFrame.remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch(err => {
app.log.error("RemoteSprite", `加载远程图片失败: ${this._remoteUrl}`, err);
// 可以添加默认图片或错误状态处理
if (this.isValid) {
this.spriteFrame = null;
}
});
}
}
}
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
if (!CCRemoteSprite._checkTimer) {
CCRemoteSprite._checkTimer = setInterval(() => {
CCRemoteSprite.checkCache();
}, 5000);
}
if (!nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.addRef();
}
if (!nsStringUtils.isEmpty(this._remoteUrl) && this.spriteFrame?.remoteUrl !== this._remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch(err => {
app.log.error("RemoteSprite", `onLoad加载远程图片失败: ${this._remoteUrl}`, err);
});
} else if (nsStringUtils.isEmpty(this._remoteUrl) && !nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.release();
}
}
private async loadRemoteSprite(url: string): Promise<void> {
if (nsStringUtils.isEmpty(url)) {
return;
}
// URL 相同,且已经加载完成,直接返回
if (this.spriteFrame && this.spriteFrame.remoteUrl == url) {
return;
}
this.release();
if (CCRemoteSprite._remoteSpriteCache.has(url)) {
const sp = CCRemoteSprite._remoteSpriteCache.get(url);
sp.addRef();
this.spriteFrame = sp;
return;
}
let loadingPromise: Promise<SpriteFrame> = null;
if (CCRemoteSprite._loadingSpriteCache.has(url)) {
loadingPromise = CCRemoteSprite._loadingSpriteCache.get(url);
} else {
loadingPromise = new Promise<SpriteFrame>((resolve, reject) => {
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
if (err) {
app.log.error(`loadRemote error: ${url}`, err);
CCRemoteSprite._loadingSpriteCache.delete(url);
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.remoteUrl = url;
CCRemoteSprite._remoteSpriteCache.set(url, spriteFrame);
CCRemoteSprite._loadingSpriteCache.delete(url);
resolve(spriteFrame);
} catch (createErr) {
CCRemoteSprite._loadingSpriteCache.delete(url);
reject(createErr);
}
}
});
});
CCRemoteSprite._loadingSpriteCache.set(url, loadingPromise);
}
const sp = await loadingPromise;
sp.addRef();
// // 检查是否在加载过程中URL发生了变化
if (sp && (!isValid(this.node, true) || this._remoteUrl !== url || this.spriteFrame?.remoteUrl === url)) {
sp.decRef();
return;
}
console.log("loadSpriteFrame:", url);
this.spriteFrame = sp;
}
public setSize(widthOrHeight: number, height?: number): void {
height ??= widthOrHeight;
this.sizeMode = Sprite.SizeMode.CUSTOM;
this.getComponent(UITransform).setContentSize(widthOrHeight, height);
}
public release(): void {
if (!nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.decRef();
}
this.spriteFrame = null;
}
public onDestroy(): void {
this.release();
super.onDestroy();
}
private static checkCache() {
// 检查缓存中是否有已加载的SpriteFrame
let clearUrls = [];
for (const [url, spriteFrame] of CCRemoteSprite._remoteSpriteCache) {
if (spriteFrame.refCount <= 0) {
let texture = spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (spriteFrame.packable && spriteFrame.original) {
texture = spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (spriteFrame.isValid) {
spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
clearUrls.push(url);
}
}
app.log.log("清理缓存:", clearUrls.length);
clearUrls.forEach(url => {
CCRemoteSprite._remoteSpriteCache.delete(url);
});
}
}

View File

@@ -1,13 +1,22 @@
import { _decorator, Sprite } from "cc"; import { isValid } from "cc";
import { EDITOR } from "cc/env"; import { assetManager, ImageAsset, Texture2D } from "cc";
import { SpriteFrame } from "cc";
import { UITransform } from "cc"; import { UITransform } from "cc";
import { Sprite } from "cc";
import { _decorator } from "cc";
import { EDITOR } from "cc/env";
import { StringUtils } from "../utils/StringUtils"; import { StringUtils } from "../utils/StringUtils";
import { RemoteSpriteCache } from "./RemoteSpriteCache";
import LogUtils from "../utils/LogUtils"; import LogUtils from "../utils/LogUtils";
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
const CACHE_EXPIRE_TIME = 30000; // 30s 缓存30s
const CACHE_CHECK_INTERVAL = 5000; // 5秒检查一次
@ccclass("RemoteSprite") @ccclass("RemoteSprite")
export default class RemoteSprite extends Sprite { export class RemoteSprite extends Sprite {
private static _remoteSpriteCache: Map<string, SpriteFrame> = new Map();
private static _loadingSpriteCache: Map<string, Promise<SpriteFrame>> = new Map();
private static _checkTimer: NodeJS.Timeout;
/** /**
* 远程图片URL * 远程图片URL
*/ */
@@ -26,13 +35,13 @@ export default class RemoteSprite extends Sprite {
const newValue = value || ""; const newValue = value || "";
if (this._remoteUrl !== newValue) { if (this._remoteUrl !== newValue) {
this._remoteUrl = newValue; this._remoteUrl = newValue;
if (this._remoteUrl !== this._currentUrl) { if (StringUtils.isEmpty(this._remoteUrl)) {
this.release();
return;
}
if (this.spriteFrame == null || this._remoteUrl !== this.spriteFrame.remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch((err) => { this.loadRemoteSprite(this._remoteUrl).catch((err) => {
LogUtils.error( LogUtils.error("RemoteSprite", `加载远程图片失败: ${this._remoteUrl}`, err);
"RemoteSprite",
`加载远程图片失败: ${this._remoteUrl}`,
err,
);
// 可以添加默认图片或错误状态处理 // 可以添加默认图片或错误状态处理
if (this.isValid) { if (this.isValid) {
this.spriteFrame = null; this.spriteFrame = null;
@@ -42,29 +51,31 @@ export default class RemoteSprite extends Sprite {
} }
} }
// 私有属性 // override set spriteFrame(value: SpriteFrame) {
private _currentUrl: string = ""; // super.spriteFrame = value;
// }
public onLoad(): void { public onLoad(): void {
super.onLoad(); super.onLoad();
if (EDITOR) { if (EDITOR) {
return; return;
} }
RemoteSpriteCache.getInstance().checkAndRegisterSprite( if (!RemoteSprite._checkTimer) {
this._currentUrl, RemoteSprite._checkTimer = setInterval(() => {
this.spriteFrame, RemoteSprite.checkCache();
); }, CACHE_CHECK_INTERVAL);
if ( }
!StringUtils.isEmpty(this._remoteUrl) &&
StringUtils.isEmpty(this._currentUrl) if (!StringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
) { this.spriteFrame.addRef();
this.spriteFrame.lastAccessTime = Date.now();
}
if (!StringUtils.isEmpty(this._remoteUrl) && this.spriteFrame?.remoteUrl !== this._remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch((err) => { this.loadRemoteSprite(this._remoteUrl).catch((err) => {
LogUtils.error( LogUtils.error("RemoteSprite", `onLoad加载远程图片失败: ${this._remoteUrl}`, err);
"RemoteSprite",
`onLoad加载远程图片失败: ${this._remoteUrl}`,
err,
);
}); });
} else if (StringUtils.isEmpty(this._remoteUrl) && !StringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.release();
} }
} }
@@ -74,21 +85,61 @@ export default class RemoteSprite extends Sprite {
} }
// URL 相同,且已经加载完成,直接返回 // URL 相同,且已经加载完成,直接返回
if (this._currentUrl === url && this.spriteFrame) { if (this.spriteFrame && this.spriteFrame.remoteUrl == url) {
return; return;
} }
if (RemoteSprite._remoteSpriteCache.has(url)) {
const sp = RemoteSprite._remoteSpriteCache.get(url);
sp.lastAccessTime = Date.now();
sp.addRef();
this.release();
this.spriteFrame = sp;
return;
}
let loadingPromise: Promise<SpriteFrame> = null;
if (RemoteSprite._loadingSpriteCache.has(url)) {
loadingPromise = RemoteSprite._loadingSpriteCache.get(url);
} else {
loadingPromise = new Promise<SpriteFrame>((resolve, reject) => {
LogUtils.log("RemoteSprite", `loadRemoteSprite: ${url}`);
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
if (err) {
LogUtils.error(`loadRemote error: ${url}`, err);
RemoteSprite._loadingSpriteCache.delete(url);
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.remoteUrl = url;
RemoteSprite._remoteSpriteCache.set(url, spriteFrame);
RemoteSprite._loadingSpriteCache.delete(url);
resolve(spriteFrame);
} catch (createErr) {
RemoteSprite._loadingSpriteCache.delete(url);
reject(createErr);
}
}
});
});
RemoteSprite._loadingSpriteCache.set(url, loadingPromise);
}
const sp = await loadingPromise;
sp.addRef();
sp.lastAccessTime = Date.now();
// // 检查是否在加载过程中URL发生了变化
if (sp && (!isValid(this.node, true) || this._remoteUrl !== url || this.spriteFrame?.remoteUrl === url)) {
sp.decRef(false);
return;
}
console.log("loadSpriteFrame:", url);
this.release(); this.release();
const sp = await RemoteSpriteCache.getInstance().loadSpriteFrame(url);
// 检查是否在加载过程中URL发生了变化
if (this._remoteUrl !== url || this._currentUrl === url) {
RemoteSpriteCache.getInstance().releaseResource(url);
return;
}
this.spriteFrame = sp; this.spriteFrame = sp;
this._currentUrl = url;
} }
public setSize(widthOrHeight: number, height?: number): void { public setSize(widthOrHeight: number, height?: number): void {
@@ -98,10 +149,9 @@ export default class RemoteSprite extends Sprite {
} }
public release(): void { public release(): void {
if (!StringUtils.isEmpty(this._currentUrl)) { if (!StringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
RemoteSpriteCache.getInstance()?.releaseResource(this._currentUrl); this.spriteFrame.decRef(false);
} }
this._currentUrl = "";
this.spriteFrame = null; this.spriteFrame = null;
} }
@@ -109,4 +159,48 @@ export default class RemoteSprite extends Sprite {
this.release(); this.release();
super.onDestroy(); super.onDestroy();
} }
private static checkCache() {
// 检查缓存中是否有已加载的SpriteFrame
let clearUrls = [];
let now = Date.now();
for (const [url, spriteFrame] of RemoteSprite._remoteSpriteCache) {
if (spriteFrame.refCount <= 0 && spriteFrame.lastAccessTime < now - CACHE_EXPIRE_TIME) {
let texture = spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (spriteFrame.packable && spriteFrame.original) {
texture = spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (spriteFrame.isValid) {
spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
clearUrls.push(url);
}
}
clearUrls.forEach((url) => {
RemoteSprite._remoteSpriteCache.delete(url);
});
LogUtils.log("清理缓存:", clearUrls.length, RemoteSprite._remoteSpriteCache.size);
}
} }

View File

@@ -1,10 +1,4 @@
import { import { _decorator, SpriteFrame, ImageAsset, Texture2D, assetManager } from "cc";
_decorator,
SpriteFrame,
ImageAsset,
Texture2D,
assetManager,
} from "cc";
import NodeSingleton from "../NodeSingleton"; import NodeSingleton from "../NodeSingleton";
import LogUtils from "../utils/LogUtils"; import LogUtils from "../utils/LogUtils";
import { StringUtils } from "../utils/StringUtils"; import { StringUtils } from "../utils/StringUtils";
@@ -71,7 +65,7 @@ class CacheItem {
const TAG = "RemoteSpriteCache"; const TAG = "RemoteSpriteCache";
const CLEANUP_CHECK_INTERVAL = 10; // 10秒检查一次 const CLEANUP_CHECK_INTERVAL = 10; // 10秒检查一次
const CACHE_EXPIRE_TIME = 1 * 60 * 1000; // 1分钟 const CACHE_EXPIRE_TIME = 1 * 30 * 1000; // 1分钟
// 添加最大缓存数量限制 // 添加最大缓存数量限制
const MAX_CACHE_SIZE = 100; // 最大缓存数量 const MAX_CACHE_SIZE = 100; // 最大缓存数量
@@ -98,13 +92,10 @@ export class RemoteSpriteCache extends NodeSingleton {
/** /**
* 检查并注册精灵 * 检查并注册精灵
*/ */
public checkAndRegisterSprite( public checkAndRegisterSprite(spriteFrame: SpriteFrame): boolean {
url: string, if (spriteFrame && !StringUtils.isEmpty(spriteFrame.remoteUrl)) {
spriteFrame: SpriteFrame, const cacheItem = this._cache.get(spriteFrame.remoteUrl);
): boolean { if (cacheItem) {
if (spriteFrame && !StringUtils.isEmpty(url)) {
const cacheItem = this._cache.get(url);
if (cacheItem && cacheItem.spriteFrame === spriteFrame) {
cacheItem.refCount++; cacheItem.refCount++;
return true; return true;
} }
@@ -115,10 +106,7 @@ export class RemoteSpriteCache extends NodeSingleton {
protected update(dt: number): void { protected update(dt: number): void {
const currentTime = Date.now(); const currentTime = Date.now();
// 每隔指定时间检查一次过期资源 // 每隔指定时间检查一次过期资源
if ( if (currentTime - this._lastCleanupTime >= CLEANUP_CHECK_INTERVAL * 1000) {
currentTime - this._lastCleanupTime >=
CLEANUP_CHECK_INTERVAL * 1000
) {
this.cleanupUnusedResources(); this.cleanupUnusedResources();
this._lastCleanupTime = currentTime; this._lastCleanupTime = currentTime;
} }
@@ -130,16 +118,10 @@ export class RemoteSpriteCache extends NodeSingleton {
public async loadSpriteFrame(url: string): Promise<SpriteFrame> { public async loadSpriteFrame(url: string): Promise<SpriteFrame> {
let cacheItem = this._cache.get(url); let cacheItem = this._cache.get(url);
if (cacheItem) { if (cacheItem) {
if ( if (cacheItem.loadState === LoadState.LOADED && cacheItem.spriteFrame) {
cacheItem.loadState === LoadState.LOADED &&
cacheItem.spriteFrame
) {
cacheItem.refCount++; cacheItem.refCount++;
return cacheItem.spriteFrame; return cacheItem.spriteFrame;
} else if ( } else if (cacheItem.loadState === LoadState.LOADING && cacheItem.loadPromise) {
cacheItem.loadState === LoadState.LOADING &&
cacheItem.loadPromise
) {
try { try {
const spriteFrame = await cacheItem.loadPromise; const spriteFrame = await cacheItem.loadPromise;
if (spriteFrame) { if (spriteFrame) {
@@ -155,14 +137,8 @@ export class RemoteSpriteCache extends NodeSingleton {
} }
// 如果加载失败且重试次数未达到上限,重新尝试 // 如果加载失败且重试次数未达到上限,重新尝试
if ( if (cacheItem.loadState === LoadState.FAILED && cacheItem.retryCount < MAX_RETRY_COUNT) {
cacheItem.loadState === LoadState.FAILED && LogUtils.warn(TAG, `重试加载: ${url}, 第${cacheItem.retryCount + 1}`);
cacheItem.retryCount < MAX_RETRY_COUNT
) {
LogUtils.warn(
TAG,
`重试加载: ${url}, 第${cacheItem.retryCount + 1}`,
);
cacheItem.incrementRetry(); cacheItem.incrementRetry();
return this.doLoadSpriteFrame(url, cacheItem); return this.doLoadSpriteFrame(url, cacheItem);
} }
@@ -178,10 +154,7 @@ export class RemoteSpriteCache extends NodeSingleton {
return this.doLoadSpriteFrame(url, cacheItem); return this.doLoadSpriteFrame(url, cacheItem);
} }
private async doLoadSpriteFrame( private async doLoadSpriteFrame(url: string, cacheItem: CacheItem): Promise<SpriteFrame> {
url: string,
cacheItem: CacheItem,
): Promise<SpriteFrame> {
cacheItem.loadState = LoadState.LOADING; cacheItem.loadState = LoadState.LOADING;
cacheItem.setLoadStartTime(); cacheItem.setLoadStartTime();
@@ -193,6 +166,7 @@ export class RemoteSpriteCache extends NodeSingleton {
reject(new Error(`加载超时: ${url}`)); reject(new Error(`加载超时: ${url}`));
}, LOAD_TIMEOUT); }, LOAD_TIMEOUT);
console.log(`开始加载资源: ${url}`);
assetManager.loadRemote(url, (err, asset: ImageAsset) => { assetManager.loadRemote(url, (err, asset: ImageAsset) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -209,6 +183,7 @@ export class RemoteSpriteCache extends NodeSingleton {
const spriteFrame = new SpriteFrame(); const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture; spriteFrame.texture = texture;
spriteFrame.addRef(); spriteFrame.addRef();
spriteFrame.remoteUrl = url;
cacheItem.refCount++; cacheItem.refCount++;
cacheItem.spriteFrame = spriteFrame; cacheItem.spriteFrame = spriteFrame;
@@ -218,11 +193,7 @@ export class RemoteSpriteCache extends NodeSingleton {
resolve(spriteFrame); resolve(spriteFrame);
} catch (createErr) { } catch (createErr) {
LogUtils.error( LogUtils.error(TAG, `创建SpriteFrame失败: ${url}`, createErr);
TAG,
`创建SpriteFrame失败: ${url}`,
createErr,
);
cacheItem.loadState = LoadState.FAILED; cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null; cacheItem.loadPromise = null;
reject(createErr); reject(createErr);
@@ -242,6 +213,7 @@ export class RemoteSpriteCache extends NodeSingleton {
const cacheItem = this._cache.get(url); const cacheItem = this._cache.get(url);
if (cacheItem) { if (cacheItem) {
cacheItem.refCount--; cacheItem.refCount--;
console.log(`释放资源引用: ${url}, 引用计数: ${cacheItem.refCount}`);
} }
} }
} }
@@ -300,11 +272,7 @@ export class RemoteSpriteCache extends NodeSingleton {
private cleanupByUsageTime(targetSize: number): void { private cleanupByUsageTime(targetSize: number): void {
// 直接筛选并排序需要清理的资源 // 直接筛选并排序需要清理的资源
const entriesToClean = Array.from(this._cache.entries()) const entriesToClean = Array.from(this._cache.entries())
.filter( .filter(([_, cacheItem]) => cacheItem.refCount <= 0 && cacheItem.loadState === LoadState.LOADED)
([_, cacheItem]) =>
cacheItem.refCount <= 0 &&
cacheItem.loadState === LoadState.LOADED,
)
.sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime); .sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime);
let cleanedCount = 0; let cleanedCount = 0;
@@ -343,10 +311,7 @@ export class RemoteSpriteCache extends NodeSingleton {
let imageAsset: ImageAsset = null; let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D // 如果已加入动态合图必须取原始的Texture2D
if ( if (cacheItem.spriteFrame.packable && cacheItem.spriteFrame.original) {
cacheItem.spriteFrame.packable &&
cacheItem.spriteFrame.original
) {
texture = cacheItem.spriteFrame.original._texture as Texture2D; texture = cacheItem.spriteFrame.original._texture as Texture2D;
} }

File diff suppressed because it is too large Load Diff