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.
487 lines
18 KiB
487 lines
18 KiB
|
3 weeks ago
|
/**
|
||
|
|
* 小游戏 zip Bundle 存储补丁。
|
||
|
|
*
|
||
|
|
* 背景:微信等小游戏本地缓存满时,引擎 unzip 失败后会调异步 clearLRU(每 500ms 删一个文件),
|
||
|
|
* 空间未腾出前无法重试解压,导致 loadBundle 永久挂起;坏缓存写入 cacheList.json 后重启仍命中。
|
||
|
|
*
|
||
|
|
* 本文件在 GameLaunch 最早阶段 monkey-patch 引擎 cacheManager:
|
||
|
|
* 1. 替换 fsUtils.makeDirSync — 目录已存在时不打 Make directory failed 警告
|
||
|
|
* 2. 新增 clearLRUImmediate — 同步删 LRU 缓存(无 setTimeout)
|
||
|
|
* 3. 重写 unzipAndCacheBundle — 存储不足时同步清缓存并立即重试解压
|
||
|
|
*
|
||
|
|
* 测试时在控制台过滤关键字:CacheManagerPatch
|
||
|
|
* 线上诊断:关键节点经 PfDiagnostics 上报(CACHE:PATCH_OK / LRU_CLEAR / UNZIP_RETRY / UNZIP_FAIL)
|
||
|
|
*
|
||
|
|
* @see Cocos 3.8.8 platforms/minigame/common/engine/cache-manager.js
|
||
|
|
*/
|
||
|
|
import { assetManager, path } from "cc";
|
||
|
|
import { PfDiagnostics } from "./PfDiagnostics";
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface Window {
|
||
|
|
fsUtils?: MinigameFsUtils;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 小游戏引擎适配层注入的文件系统工具(window.fsUtils) */
|
||
|
|
interface MinigameFsUtils {
|
||
|
|
fs?: {
|
||
|
|
unlinkSync?(filePath: string): void;
|
||
|
|
mkdirSync?(dirPath: string, recursive?: boolean): void;
|
||
|
|
accessSync?(filePath: string): void;
|
||
|
|
/** 内部标记:mkdirSync 已打过补丁 */
|
||
|
|
_mkdirSyncPatched?: boolean;
|
||
|
|
};
|
||
|
|
rmdirSync(dirPath: string, recursive?: boolean): Error | null;
|
||
|
|
makeDirSync(dirPath: string, recursive?: boolean): Error | null;
|
||
|
|
unzip(zipFilePath: string, targetPath: string, onComplete: (err: Error | null) => void): void;
|
||
|
|
isOutOfStorage(errMsg: string): boolean;
|
||
|
|
/** 内部标记:makeDirSync 已打过补丁 */
|
||
|
|
_makeDirSyncPatched?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 引擎 cacheManager 运行时扩展字段(TS 公开类型未暴露,运行时有) */
|
||
|
|
interface CacheManagerLike {
|
||
|
|
cacheDir: string;
|
||
|
|
autoClear: boolean;
|
||
|
|
outOfStorage: boolean;
|
||
|
|
cachedFiles: {
|
||
|
|
forEach(cb: (val: { bundle: string; url: string; lastTime: number }, key: string) => void): void;
|
||
|
|
add(key: string, val: { bundle: string; url: string; lastTime: string | number }): void;
|
||
|
|
remove(key: string): unknown;
|
||
|
|
};
|
||
|
|
_isZipFile(url: string): boolean;
|
||
|
|
_write(): void;
|
||
|
|
clearLRU(): void;
|
||
|
|
clearLRUImmediate?(): void;
|
||
|
|
unzipAndCacheBundle(
|
||
|
|
id: string,
|
||
|
|
zipFilePath: string,
|
||
|
|
cacheBundleRoot: string,
|
||
|
|
onComplete: (err: Error | null, targetPath?: string) => void,
|
||
|
|
retryCount?: number
|
||
|
|
): void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 日志前缀,控制台过滤:CacheManagerPatch */
|
||
|
|
const LOG_TAG = "[CacheManagerPatch]";
|
||
|
|
|
||
|
|
/** 存储不足时 unzip 最大重试次数(每次重试前同步清 1/3 LRU) */
|
||
|
|
const MAX_UNZIP_STORAGE_RETRIES = 5;
|
||
|
|
|
||
|
|
/** 进大厅静默预载前:缓存条目超过此值则主动清 LRU,降低 unzip 撞配额概率 */
|
||
|
|
const PROACTIVE_TRIM_CACHE_THRESHOLD = 320;
|
||
|
|
|
||
|
|
/** 磁盘 LRU 不得驱逐的大厅壳分包(回大厅再开弹窗依赖本机 zip) */
|
||
|
|
const SHELL_BUNDLE_NAMES: readonly string[] = ['first', 'comm', 'home', 'popCommon'];
|
||
|
|
|
||
|
|
/** 是否大厅壳分包名 */
|
||
|
|
function isShellBundleName(name: string): boolean {
|
||
|
|
if (!name) return false;
|
||
|
|
for (let i = 0; i < SHELL_BUNDLE_NAMES.length; i++) {
|
||
|
|
if (SHELL_BUNDLE_NAMES[i] === name) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 解压目标目录后缀,避免同毫秒多次解压路径冲突 */
|
||
|
|
let unzipPathSuffix = 0;
|
||
|
|
|
||
|
|
type CacheDiagTag = 'PATCH_OK' | 'LRU_CLEAR' | 'UNZIP_RETRY' | 'UNZIP_FAIL';
|
||
|
|
|
||
|
|
/** 上报 PfDiagnostics(平台实时日志 + pfLog);detail 建议 ≤120 字符 */
|
||
|
|
function reportCacheDiag(
|
||
|
|
tag: CacheDiagTag,
|
||
|
|
detail: string,
|
||
|
|
level: 'error' | 'warn' | 'info' = 'warn',
|
||
|
|
cooldownMs = 600000,
|
||
|
|
): void {
|
||
|
|
PfDiagnostics.write('CACHE', tag, detail, cooldownMs, level);
|
||
|
|
}
|
||
|
|
|
||
|
|
function log(message: string, detail?: unknown): void {
|
||
|
|
if (detail !== undefined) {
|
||
|
|
// console.log(LOG_TAG, message, detail);
|
||
|
|
} else {
|
||
|
|
// console.log(LOG_TAG, message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function warn(message: string, _detail?: unknown): void {
|
||
|
|
console.warn(LOG_TAG, message);
|
||
|
|
}
|
||
|
|
|
||
|
|
function logError(message: string, _detail?: unknown): void {
|
||
|
|
console.error(LOG_TAG, message);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 统计 cachedFiles 条目数 */
|
||
|
|
function countCachedFiles(cm: CacheManagerLike): number {
|
||
|
|
let count = 0;
|
||
|
|
cm.cachedFiles.forEach(() => {
|
||
|
|
count++;
|
||
|
|
});
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取小游戏平台 fsUtils。
|
||
|
|
* 仅在小游戏环境存在;Web 预览等环境为 null,补丁自动跳过。
|
||
|
|
*/
|
||
|
|
function getFsUtils(): MinigameFsUtils | null {
|
||
|
|
const fsUtils = (globalThis as typeof globalThis & { fsUtils?: MinigameFsUtils }).fsUtils;
|
||
|
|
return fsUtils ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 目录已存在时各小游戏平台 mkdir 报错文案(如抖音 mkdirSync:fail file already exists) */
|
||
|
|
function isDirAlreadyExistsMessage(message: string): boolean {
|
||
|
|
return /already exists|file exists|EEXIST/i.test(message);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 替换引擎 fsUtils.makeDirSync / fs.mkdirSync,避免「目录已存在」时刷 Make directory failed 警告。
|
||
|
|
* 引擎默认实现会对 EEXIST 打 cc.warn;Bundle 二次加载时 gamecaches/xxx 目录通常已存在,属正常现象。
|
||
|
|
* 抖音等平台 engine-adapter 可能直接调 fs.mkdirSync 再 warn,故两层都需补丁。
|
||
|
|
*/
|
||
|
|
function installMakeDirSyncPatch(fsUtils: MinigameFsUtils): void {
|
||
|
|
if (fsUtils._makeDirSyncPatched) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const fs = fsUtils.fs;
|
||
|
|
if (fs?.mkdirSync && !fs._mkdirSyncPatched) {
|
||
|
|
const origMkdir = fs.mkdirSync.bind(fs);
|
||
|
|
fs.mkdirSync = (dirPath: string, recursive?: boolean): void => {
|
||
|
|
try {
|
||
|
|
origMkdir(dirPath, recursive);
|
||
|
|
} catch (e: unknown) {
|
||
|
|
const message = e instanceof Error ? e.message : String(e);
|
||
|
|
if (isDirAlreadyExistsMessage(message)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fs._mkdirSyncPatched = true;
|
||
|
|
}
|
||
|
|
fsUtils.makeDirSync = (dirPath: string, recursive?: boolean): Error | null => {
|
||
|
|
const mkdir = fsUtils.fs?.mkdirSync;
|
||
|
|
if (!mkdir) {
|
||
|
|
return new Error('fs.mkdirSync unavailable');
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
mkdir(dirPath, recursive);
|
||
|
|
return null;
|
||
|
|
} catch (e: unknown) {
|
||
|
|
const message = e instanceof Error ? e.message : String(e);
|
||
|
|
if (isDirAlreadyExistsMessage(message)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
warn('makeDirSync failed', { dirPath, message });
|
||
|
|
return e instanceof Error ? e : new Error(message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fsUtils._makeDirSyncPatched = true;
|
||
|
|
log('makeDirSync patched (ignore dir already exists)');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 同步删除单条缓存对应的磁盘文件/目录。
|
||
|
|
* - zip bundle 解压后是目录 → rmdirSync
|
||
|
|
* - 普通文件 / zip 包本身 → unlinkSync
|
||
|
|
*/
|
||
|
|
function removePathOrFileSync(cm: CacheManagerLike, originUrl: string, localPath: string): boolean {
|
||
|
|
const fsUtils = getFsUtils();
|
||
|
|
if (!fsUtils) {
|
||
|
|
warn("removePathOrFileSync: fsUtils missing", { originUrl, localPath });
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (cm._isZipFile(originUrl)) {
|
||
|
|
if (cm._isZipFile(localPath)) {
|
||
|
|
try {
|
||
|
|
fsUtils.fs?.unlinkSync?.(localPath);
|
||
|
|
log("removed file (zip)", { originUrl: path.basename(originUrl), localPath });
|
||
|
|
return true;
|
||
|
|
} catch (e) {
|
||
|
|
warn("unlinkSync zip failed", { localPath, error: e });
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const rmErr = fsUtils.rmdirSync(localPath, true);
|
||
|
|
if (rmErr) {
|
||
|
|
warn("rmdirSync failed", { localPath, error: rmErr.message });
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
log("removed dir (unzipped bundle)", { originUrl: path.basename(originUrl), localPath });
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
fsUtils.fs?.unlinkSync?.(localPath);
|
||
|
|
log("removed file", { originUrl: path.basename(originUrl), localPath });
|
||
|
|
return true;
|
||
|
|
} catch (e) {
|
||
|
|
warn("unlinkSync failed", { localPath, error: e });
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 按引擎 clearLRU 相同规则筛选待删缓存条目:
|
||
|
|
* 1. 遍历 cachedFiles,跳过正在使用的 zip bundle
|
||
|
|
* 2. 按 lastTime 升序(最久未用在前)
|
||
|
|
* 3. 总数 ≥ 3 取最旧 1/3;< 3 则全取
|
||
|
|
*/
|
||
|
|
function collectLruCandidates(cm: CacheManagerLike): Array<{ originUrl: string; url: string; lastTime: number }> {
|
||
|
|
const allCandidates: Array<{ originUrl: string; url: string; lastTime: number }> = [];
|
||
|
|
let skippedInUse = 0;
|
||
|
|
cm.cachedFiles.forEach((val, key) => {
|
||
|
|
let skip = false;
|
||
|
|
if (cm._isZipFile(key)) {
|
||
|
|
if (isShellBundleName(val.bundle)) {
|
||
|
|
skippedInUse++;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
assetManager.bundles.forEach((bundle) => {
|
||
|
|
if (bundle.base.indexOf(val.url) !== -1) {
|
||
|
|
skip = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (skip) {
|
||
|
|
skippedInUse++;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
allCandidates.push({ originUrl: key, url: val.url, lastTime: val.lastTime });
|
||
|
|
});
|
||
|
|
allCandidates.sort((a, b) => a.lastTime - b.lastTime);
|
||
|
|
const totalEligible = allCandidates.length;
|
||
|
|
if (allCandidates.length >= 3) {
|
||
|
|
allCandidates.length = Math.floor(allCandidates.length / 3);
|
||
|
|
}
|
||
|
|
log("collectLruCandidates", {
|
||
|
|
totalInCache: countCachedFiles(cm),
|
||
|
|
eligible: totalEligible,
|
||
|
|
skippedInUseZipBundle: skippedInUse,
|
||
|
|
toRemove: allCandidates.length,
|
||
|
|
rule: totalEligible >= 3 ? "remove oldest 1/3" : "remove all eligible",
|
||
|
|
});
|
||
|
|
return allCandidates;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 给 cacheManager 挂载 clearLRUImmediate。
|
||
|
|
* 与引擎 clearLRU 筛选逻辑相同,但同步删除磁盘文件并立即写回 cacheList.json,
|
||
|
|
* 不经过 setTimeout(deferredDelete, deleteInterval)。
|
||
|
|
*/
|
||
|
|
function installClearLRUImmediate(cm: CacheManagerLike): void {
|
||
|
|
cm.clearLRUImmediate = function clearLRUImmediate(this: CacheManagerLike): void {
|
||
|
|
const beforeCount = countCachedFiles(this);
|
||
|
|
log("clearLRUImmediate start", {
|
||
|
|
cacheDir: this.cacheDir,
|
||
|
|
cachedCount: beforeCount,
|
||
|
|
outOfStorage: this.outOfStorage,
|
||
|
|
autoClear: this.autoClear,
|
||
|
|
});
|
||
|
|
const caches = collectLruCandidates(this);
|
||
|
|
if (caches.length === 0) {
|
||
|
|
log("clearLRUImmediate: nothing to remove");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let diskOk = 0;
|
||
|
|
let diskFail = 0;
|
||
|
|
for (let i = 0, l = caches.length; i < l; i++) {
|
||
|
|
const item = caches[i];
|
||
|
|
const cacheKey = `${assetManager.utils.getUuidFromURL(item.originUrl)}@native`;
|
||
|
|
(assetManager as unknown as { files: { remove(key: string): void } }).files.remove(cacheKey);
|
||
|
|
this.cachedFiles.remove(item.originUrl);
|
||
|
|
if (removePathOrFileSync(this, item.originUrl, item.url)) {
|
||
|
|
diskOk++;
|
||
|
|
} else {
|
||
|
|
diskFail++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._write();
|
||
|
|
this.outOfStorage = false;
|
||
|
|
const afterCount = countCachedFiles(this);
|
||
|
|
log("clearLRUImmediate done", {
|
||
|
|
removedFromIndex: caches.length,
|
||
|
|
diskOk,
|
||
|
|
diskFail,
|
||
|
|
cachedCountBefore: beforeCount,
|
||
|
|
cachedCountAfter: afterCount,
|
||
|
|
outOfStorage: this.outOfStorage,
|
||
|
|
});
|
||
|
|
reportCacheDiag(
|
||
|
|
'LRU_CLEAR',
|
||
|
|
`rm=${caches.length}|ok=${diskOk}|fail=${diskFail}|before=${beforeCount}|after=${afterCount}`,
|
||
|
|
'warn',
|
||
|
|
);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重写引擎 unzipAndCacheBundle。
|
||
|
|
* 解压失败且报错为「存储空间不足」时:调 clearLRUImmediate 同步腾空间 → 立即递归重试;
|
||
|
|
* 不再调引擎异步 clearLRU + 直接 onComplete(err) 让上层卡死。
|
||
|
|
*/
|
||
|
|
function installUnzipAndCacheBundle(cm: CacheManagerLike, fsUtils: MinigameFsUtils): void {
|
||
|
|
cm.unzipAndCacheBundle = function unzipAndCacheBundle(
|
||
|
|
this: CacheManagerLike,
|
||
|
|
id: string,
|
||
|
|
zipFilePath: string,
|
||
|
|
cacheBundleRoot: string,
|
||
|
|
onComplete: (err: Error | null, targetPath?: string) => void,
|
||
|
|
retryCount = 0
|
||
|
|
): void {
|
||
|
|
const time = Date.now().toString();
|
||
|
|
const targetPath = `${this.cacheDir}/${cacheBundleRoot}/${time}${unzipPathSuffix++}`;
|
||
|
|
log("unzip start", {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
retry: `${retryCount}/${MAX_UNZIP_STORAGE_RETRIES}`,
|
||
|
|
zipFilePath,
|
||
|
|
targetPath,
|
||
|
|
cachedCount: countCachedFiles(this),
|
||
|
|
outOfStorage: this.outOfStorage,
|
||
|
|
});
|
||
|
|
const mkdirErr = fsUtils.makeDirSync(targetPath, true);
|
||
|
|
if (mkdirErr) {
|
||
|
|
logError("makeDirSync failed before unzip", { targetPath, error: mkdirErr.message });
|
||
|
|
}
|
||
|
|
fsUtils.unzip(zipFilePath, targetPath, (err) => {
|
||
|
|
if (err) {
|
||
|
|
fsUtils.rmdirSync(targetPath, true);
|
||
|
|
const outOfStorage = fsUtils.isOutOfStorage(err.message);
|
||
|
|
warn("unzip failed", {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
retry: retryCount,
|
||
|
|
outOfStorage,
|
||
|
|
message: err.message,
|
||
|
|
});
|
||
|
|
if (outOfStorage && retryCount < MAX_UNZIP_STORAGE_RETRIES) {
|
||
|
|
this.outOfStorage = true;
|
||
|
|
if (this.autoClear !== false && this.clearLRUImmediate) {
|
||
|
|
this.clearLRUImmediate();
|
||
|
|
} else {
|
||
|
|
warn("skip clearLRUImmediate", {
|
||
|
|
autoClear: this.autoClear,
|
||
|
|
hasImmediate: !!this.clearLRUImmediate,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
warn(`unzip out-of-storage → retry ${retryCount + 1}/${MAX_UNZIP_STORAGE_RETRIES}`, {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
});
|
||
|
|
reportCacheDiag(
|
||
|
|
'UNZIP_RETRY',
|
||
|
|
`b=${cacheBundleRoot}|z=${path.basename(id)}|r=${retryCount + 1}/${MAX_UNZIP_STORAGE_RETRIES}|cache=${countCachedFiles(this)}`,
|
||
|
|
'warn',
|
||
|
|
60000,
|
||
|
|
);
|
||
|
|
cm.unzipAndCacheBundle(id, zipFilePath, cacheBundleRoot, onComplete, retryCount + 1);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (outOfStorage) {
|
||
|
|
logError("unzip gave up: out-of-storage retries exhausted", {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
maxRetries: MAX_UNZIP_STORAGE_RETRIES,
|
||
|
|
message: err.message,
|
||
|
|
});
|
||
|
|
reportCacheDiag(
|
||
|
|
'UNZIP_FAIL',
|
||
|
|
`b=${cacheBundleRoot}|z=${path.basename(id)}|reason=storage|retries=${MAX_UNZIP_STORAGE_RETRIES}|msg=${err.message.slice(0, 48)}`,
|
||
|
|
'error',
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
logError("unzip gave up: non-storage error (no retry)", {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
message: err.message,
|
||
|
|
});
|
||
|
|
reportCacheDiag(
|
||
|
|
'UNZIP_FAIL',
|
||
|
|
`b=${cacheBundleRoot}|z=${path.basename(id)}|reason=other|msg=${err.message.slice(0, 64)}`,
|
||
|
|
'error',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
onComplete?.(err);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.cachedFiles.add(id, { bundle: cacheBundleRoot, url: targetPath, lastTime: time });
|
||
|
|
this._write();
|
||
|
|
this.outOfStorage = false;
|
||
|
|
log("unzip ok", {
|
||
|
|
bundle: cacheBundleRoot,
|
||
|
|
zipName: path.basename(id),
|
||
|
|
retry: retryCount,
|
||
|
|
targetPath,
|
||
|
|
cachedCount: countCachedFiles(this),
|
||
|
|
});
|
||
|
|
onComplete?.(null, targetPath);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 进大厅等大包静默预载前调用:缓存条目过多时同步清 LRU,避免首次 unzip 就撞 user 目录上限。
|
||
|
|
* 幂等;无 cacheManager 或非小游戏环境则 no-op。
|
||
|
|
*/
|
||
|
|
export function tryProactiveCacheTrimBeforePreload(): void {
|
||
|
|
const cm = assetManager.cacheManager as unknown as CacheManagerLike | null;
|
||
|
|
if (!cm?.clearLRUImmediate) return;
|
||
|
|
const count = countCachedFiles(cm);
|
||
|
|
if (count < PROACTIVE_TRIM_CACHE_THRESHOLD) return;
|
||
|
|
log('proactive trim before preload', { cachedCount: count, threshold: PROACTIVE_TRIM_CACHE_THRESHOLD });
|
||
|
|
cm.clearLRUImmediate();
|
||
|
|
reportCacheDiag(
|
||
|
|
'LRU_CLEAR',
|
||
|
|
`proactive|before=${count}|after=${countCachedFiles(cm)}`,
|
||
|
|
'warn',
|
||
|
|
120000,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 入口:在 GameLaunch.start 最早阶段调用。
|
||
|
|
* 仅在小游戏环境(cacheManager + fsUtils 均存在)生效;已打过补丁则跳过(幂等)。
|
||
|
|
*/
|
||
|
|
export function applyCacheManagerPatch(): void {
|
||
|
|
const cm = assetManager.cacheManager as unknown as CacheManagerLike | null;
|
||
|
|
const fsUtils = getFsUtils();
|
||
|
|
if (!cm && !fsUtils) {
|
||
|
|
log("skip patch: not minigame env (cacheManager & fsUtils both missing)");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!fsUtils) {
|
||
|
|
warn("skip patch: window.fsUtils missing");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
installMakeDirSyncPatch(fsUtils);
|
||
|
|
if (!cm) {
|
||
|
|
warn("skip patch: assetManager.cacheManager missing");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (typeof cm.clearLRUImmediate === "function") {
|
||
|
|
log("skip patch: cacheManager already applied");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
installClearLRUImmediate(cm);
|
||
|
|
installUnzipAndCacheBundle(cm, fsUtils);
|
||
|
|
log("patch applied", {
|
||
|
|
cacheDir: cm.cacheDir,
|
||
|
|
autoClear: cm.autoClear,
|
||
|
|
cachedCount: countCachedFiles(cm),
|
||
|
|
maxUnzipRetries: MAX_UNZIP_STORAGE_RETRIES,
|
||
|
|
replaced: ["clearLRUImmediate (new)", "unzipAndCacheBundle (override)"],
|
||
|
|
});
|
||
|
|
reportCacheDiag(
|
||
|
|
'PATCH_OK',
|
||
|
|
`dir=${cm.cacheDir}|autoClear=${cm.autoClear}|cached=${countCachedFiles(cm)}|maxRetry=${MAX_UNZIP_STORAGE_RETRIES}`,
|
||
|
|
'info',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 模块加载时尽早尝试打补丁(GameLaunch.start 可能晚于首包缓存目录创建) */
|
||
|
|
applyCacheManagerPatch();
|