feat : add editor bundle cache markers

This commit is contained in:
何冠峰
2026-05-18 18:58:56 +08:00
parent 45ecd8baff
commit f8f2dd8faf
18 changed files with 466 additions and 41 deletions

View File

@@ -80,7 +80,7 @@ namespace YooAsset
PackageName = packageName;
RootPath = rootPath;
Config = config;
IsReadOnly = true;
IsReadOnly = config.VirtualDownloadMode == false;
}
/// <inheritdoc />
@@ -208,8 +208,43 @@ namespace YooAsset
if (_cacheEntries.TryGetValue(bundleGuid, out EditorBundleCacheEntry entry))
{
_cacheEntries.Remove(bundleGuid);
entry.Delete();
}
}
/// <summary>
/// 获取 marker 文件路径
/// </summary>
/// <param name="bundle">资源包描述</param>
/// <returns>marker 文件的完整路径</returns>
internal string GetMarkerFilePath(PackageBundle bundle)
{
string bundleGuid = bundle.BundleGuid;
string hashFolder = GetHashFolderName(bundleGuid);
return PathUtility.Combine(RootPath, hashFolder, bundleGuid, EditorBundleCacheConsts.MarkerFileName);
}
/// <summary>
/// 获取 marker 临时文件路径
/// </summary>
/// <param name="bundle">资源包描述</param>
/// <returns>marker 临时文件的完整路径</returns>
internal string GetMarkerTempFilePath(PackageBundle bundle)
{
string bundleGuid = bundle.BundleGuid;
string hashFolder = GetHashFolderName(bundleGuid);
return PathUtility.Combine(RootPath, hashFolder, bundleGuid, EditorBundleCacheConsts.MarkerTempFileName);
}
private string GetHashFolderName(string bundleGuid)
{
if (string.IsNullOrEmpty(bundleGuid))
throw new YooInternalException("Bundle GUID is null or empty.");
if (bundleGuid.Length <= EditorBundleCacheConsts.HashFolderNameLength)
return bundleGuid;
return bundleGuid.Substring(0, EditorBundleCacheConsts.HashFolderNameLength);
}
#endregion
}
}

View File

@@ -0,0 +1,24 @@
namespace YooAsset
{
/// <summary>
/// 编辑器文件缓存常量定义
/// </summary>
internal static class EditorBundleCacheConsts
{
/// <summary>
/// 标记文件名称
/// </summary>
public const string MarkerFileName = "__marker";
/// <summary>
/// 标记临时文件名称
/// </summary>
public const string MarkerTempFileName = "__marker.tmp";
/// <summary>
/// 哈希分片目录前缀长度
/// </summary>
public const int HashFolderNameLength = 2;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc5ebd7b9a6b77145b004dadfe7ed587
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,3 +1,5 @@
using System;
using System.IO;
namespace YooAsset
{
@@ -11,13 +13,47 @@ namespace YooAsset
/// </summary>
public string BundleGuid { get; private set; }
/// <summary>
/// 标记文件路径
/// </summary>
public string MarkerFilePath { get; private set; }
/// <summary>
/// 创建编辑器文件缓存条目实例
/// </summary>
/// <param name="bundleGuid">资源包唯一标识</param>
public EditorBundleCacheEntry(string bundleGuid)
/// <param name="markerFilePath">标记文件路径</param>
public EditorBundleCacheEntry(string bundleGuid, string markerFilePath)
{
BundleGuid = bundleGuid;
MarkerFilePath = markerFilePath;
}
/// <summary>
/// 删除缓存文件夹及其所有内容
/// </summary>
/// <returns>删除是否成功</returns>
public bool Delete()
{
try
{
string directory = Path.GetDirectoryName(MarkerFilePath);
var directoryInfo = new DirectoryInfo(directory);
if (directoryInfo.Exists)
{
directoryInfo.Delete(true);
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
YooLogger.LogError($"Failed to delete editor cache file: {ex.Message}.");
return false;
}
}
}
}
}

View File

@@ -46,7 +46,6 @@ namespace YooAsset
{
var cacheEntries = _fileCache.GetAllEntries();
EvictionResult clearResult = _policy.SelectEvictionTargets(cacheEntries, _options);
if (clearResult.Succeeded == false)
{
_steps = ESteps.Done;

View File

@@ -1,4 +1,3 @@
namespace YooAsset
{
/// <summary>
@@ -6,7 +5,17 @@ namespace YooAsset
/// </summary>
internal sealed class EBCInitializeOperation : BCInitializeOperation
{
private enum ESteps
{
None,
ScanMarkerFiles,
AddCacheEntries,
Done,
}
private readonly EditorBundleCache _fileCache;
private ScanMarkerFilesOperation _scanMarkerFilesOp;
private ESteps _steps = ESteps.None;
public EBCInitializeOperation(EditorBundleCache cache)
{
@@ -14,10 +23,58 @@ namespace YooAsset
}
protected override void InternalStart()
{
SetResult();
if (_fileCache.Config.VirtualDownloadMode == false)
{
SetResult();
return;
}
_steps = ESteps.ScanMarkerFiles;
}
protected override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.ScanMarkerFiles)
{
if (_scanMarkerFilesOp == null)
{
_scanMarkerFilesOp = new ScanMarkerFilesOperation(_fileCache);
_scanMarkerFilesOp.StartOperation();
AddChildOperation(_scanMarkerFilesOp);
}
_scanMarkerFilesOp.UpdateOperation();
Progress = _scanMarkerFilesOp.Progress;
if (_scanMarkerFilesOp.IsDone == false)
return;
if (_scanMarkerFilesOp.Status == EOperationStatus.Succeeded)
{
_steps = ESteps.AddCacheEntries;
}
else
{
_steps = ESteps.Done;
SetError(_scanMarkerFilesOp.Error);
}
}
if (_steps == ESteps.AddCacheEntries)
{
foreach (var fileInfo in _scanMarkerFilesOp.Result)
{
if (_fileCache.IsCached(fileInfo.BundleGuid) == false)
{
var cacheEntry = new EditorBundleCacheEntry(fileInfo.BundleGuid, fileInfo.MarkerFilePath);
_fileCache.AddEntry(fileInfo.BundleGuid, cacheEntry);
}
}
_steps = ESteps.Done;
SetResult();
}
}
}
}

View File

@@ -1,3 +1,5 @@
using System;
using System.IO;
namespace YooAsset
{
@@ -52,7 +54,38 @@ namespace YooAsset
if (_steps == ESteps.CacheFile)
{
var cacheEntry = new EditorBundleCacheEntry(_options.Bundle.BundleGuid);
string markerFilePath = _fileCache.GetMarkerFilePath(_options.Bundle);
string markerTempPath = _fileCache.GetMarkerTempFilePath(_options.Bundle);
try
{
// 阶段A准备目标目录清理可能存在的残留临时文件
FileUtility.EnsureParentDirectoryExists(markerFilePath);
DeleteFileSafely(markerTempPath);
// 阶段B写入临时文件内容仅用于人工调试
string debugContent = $"BundleName={_options.Bundle.BundleName}\n"
+ $"BundleGuid={_options.Bundle.BundleGuid}\n";
File.WriteAllText(markerTempPath, debugContent);
// 阶段C原子提交
if (File.Exists(markerFilePath))
File.Delete(markerFilePath);
File.Move(markerTempPath, markerFilePath);
}
catch (Exception ex)
{
_steps = ESteps.Done;
SetError($"Failed to write marker file: {ex.Message}.");
YooLogger.LogError(Error);
// 回滚:清理临时文件,正式文件不受影响
DeleteFileSafely(markerTempPath);
return;
}
// 阶段D注册内存缓存条目
var cacheEntry = new EditorBundleCacheEntry(_options.Bundle.BundleGuid, markerFilePath);
_fileCache.AddEntry(_options.Bundle.BundleGuid, cacheEntry);
_steps = ESteps.Done;
SetResult();
@@ -62,5 +95,18 @@ namespace YooAsset
{
ExecuteBatch();
}
private static void DeleteFileSafely(string filePath)
{
try
{
if (File.Exists(filePath))
File.Delete(filePath);
}
catch (Exception ex)
{
YooLogger.LogWarning($"Failed to delete file '{filePath}': {ex.Message}.");
}
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2e9a70495d8ca514caa7647851844101
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using System.IO;
using System.Collections.Generic;
namespace YooAsset
{
/// <summary>
/// 扫描标记文件操作
/// </summary>
internal sealed class ScanMarkerFilesOperation : AsyncOperationBase
{
private enum ESteps
{
None,
Prepare,
ScanFiles,
Done,
}
private readonly EditorBundleCache _fileCache;
private IEnumerator<string> _shardEnumerator = null;
private double _scanStartTime;
private ESteps _steps = ESteps.None;
/// <summary>
/// 扫描到的标记件信息
/// </summary>
public readonly List<ScanFileInfo> Result = new List<ScanFileInfo>(5000);
/// <summary>
/// 创建操作实例
/// </summary>
/// <param name="fileCache">编辑器文件缓存系统</param>
internal ScanMarkerFilesOperation(EditorBundleCache fileCache)
{
_fileCache = fileCache;
}
protected override void InternalStart()
{
_steps = ESteps.Prepare;
}
protected override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.Prepare)
{
if (Directory.Exists(_fileCache.RootPath))
{
var directories = Directory.EnumerateDirectories(_fileCache.RootPath);
_shardEnumerator = directories.GetEnumerator();
_scanStartTime = TimeUtility.RealtimeSinceStartup;
_steps = ESteps.ScanFiles;
}
else
{
_steps = ESteps.Done;
SetResult();
}
}
if (_steps == ESteps.ScanFiles)
{
if (ScanFiles())
return;
_shardEnumerator.Dispose();
_shardEnumerator = null;
_steps = ESteps.Done;
SetResult();
double costTime = TimeUtility.RealtimeSinceStartup - _scanStartTime;
YooLogger.Log($"Marker file scan completed in {costTime:f1} seconds. Found {Result.Count} marker files.");
}
}
protected override void InternalDispose()
{
if (_shardEnumerator != null)
{
_shardEnumerator.Dispose();
_shardEnumerator = null;
}
}
private bool ScanFiles()
{
bool hasMore;
while (true)
{
hasMore = _shardEnumerator.MoveNext();
if (hasMore == false)
break;
string shardFolder = _shardEnumerator.Current;
var childDirectories = Directory.EnumerateDirectories(shardFolder);
foreach (string childDirectory in childDirectories)
{
string bundleGuid = Path.GetFileName(childDirectory);
string markerFilePath = PathUtility.Combine(childDirectory, EditorBundleCacheConsts.MarkerFileName);
if (File.Exists(markerFilePath))
{
var fileInfo = new ScanFileInfo(bundleGuid, markerFilePath);
Result.Add(fileInfo);
}
}
if (IsBusy)
break;
}
return hasMore;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc7b3752dfb973447bdae44ea8722995
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
namespace YooAsset
{
/// <summary>
/// 扫描到的标记文件信息
/// </summary>
internal class ScanFileInfo
{
/// <summary>
/// 资源包唯一标识
/// </summary>
public string BundleGuid { get; private set; }
/// <summary>
/// 标记文件路径
/// </summary>
public string MarkerFilePath { get; private set; }
/// <summary>
/// 创建扫描文件信息实例
/// </summary>
/// <param name="bundleGuid">资源包唯一标识</param>
/// <param name="markerFilePath">标记文件路径</param>
public ScanFileInfo(string bundleGuid, string markerFilePath)
{
BundleGuid = bundleGuid;
MarkerFilePath = markerFilePath;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4a3717e25e79a14485f85e83794dd6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -19,7 +19,7 @@ namespace YooAsset
}
private readonly SandboxBundleCache _fileCache;
private IEnumerator<string> _filesEnumerator = null;
private IEnumerator<string> _shardEnumerator = null;
private double _verifyStartTime;
private ESteps _steps = ESteps.None;
@@ -50,7 +50,7 @@ namespace YooAsset
if (Directory.Exists(_fileCache.RootPath))
{
var directories = Directory.EnumerateDirectories(_fileCache.RootPath);
_filesEnumerator = directories.GetEnumerator();
_shardEnumerator = directories.GetEnumerator();
_verifyStartTime = TimeUtility.RealtimeSinceStartup;
_steps = ESteps.SearchFiles;
}
@@ -66,35 +66,35 @@ namespace YooAsset
if (SearchFiles())
return;
_filesEnumerator.Dispose();
_filesEnumerator = null;
_shardEnumerator.Dispose();
_shardEnumerator = null;
_steps = ESteps.Done;
SetResult();
double costTime = TimeUtility.RealtimeSinceStartup - _verifyStartTime;
YooLogger.Log($"Cache file search completed in {costTime:f1} seconds.");
YooLogger.Log($"Cache file search completed in {costTime:f1} seconds. Found {Result.Count} cache files.");
}
}
protected override void InternalDispose()
{
if (_filesEnumerator != null)
if (_shardEnumerator != null)
{
_filesEnumerator.Dispose();
_filesEnumerator = null;
_shardEnumerator.Dispose();
_shardEnumerator = null;
}
}
private bool SearchFiles()
{
bool isFindItem;
bool hasMore;
while (true)
{
isFindItem = _filesEnumerator.MoveNext();
if (isFindItem == false)
hasMore = _shardEnumerator.MoveNext();
if (hasMore == false)
break;
var rootFolder = _filesEnumerator.Current;
var childDirectories = Directory.EnumerateDirectories(rootFolder);
var shardFolder = _shardEnumerator.Current;
var childDirectories = Directory.EnumerateDirectories(shardFolder);
foreach (var childDirectory in childDirectories)
{
string bundleGuid = Path.GetFileName(childDirectory);
@@ -113,7 +113,7 @@ namespace YooAsset
break;
}
return isFindItem;
return hasMore;
}
}
}

View File

@@ -46,7 +46,6 @@ namespace YooAsset
}
}
private const int HashFolderNameLength = 2;
private readonly Dictionary<string, SandboxBundleCacheEntry> _cacheEntries = new Dictionary<string, SandboxBundleCacheEntry>(10000);
private readonly Dictionary<string, string> _dataFilePathMapping = new Dictionary<string, string>(10000);
private readonly Dictionary<string, string> _infoFilePathMapping = new Dictionary<string, string>(10000);
@@ -196,11 +195,12 @@ namespace YooAsset
/// <returns>数据文件的完整路径</returns>
internal string GetDataFilePath(PackageBundle bundle)
{
if (_dataFilePathMapping.TryGetValue(bundle.BundleGuid, out string filePath) == false)
string bundleGuid = bundle.BundleGuid;
if (_dataFilePathMapping.TryGetValue(bundleGuid, out string filePath) == false)
{
string folderName = GetHashFolderName(bundle.FileHash);
filePath = PathUtility.Combine(RootPath, folderName, bundle.BundleGuid, SandboxBundleCacheConsts.BundleDataFileName);
_dataFilePathMapping.Add(bundle.BundleGuid, filePath);
string folderName = GetHashFolderName(bundleGuid);
filePath = PathUtility.Combine(RootPath, folderName, bundleGuid, SandboxBundleCacheConsts.BundleDataFileName);
_dataFilePathMapping.Add(bundleGuid, filePath);
}
return filePath;
}
@@ -212,11 +212,12 @@ namespace YooAsset
/// <returns>信息文件的完整路径</returns>
internal string GetInfoFilePath(PackageBundle bundle)
{
if (_infoFilePathMapping.TryGetValue(bundle.BundleGuid, out string filePath) == false)
string bundleGuid = bundle.BundleGuid;
if (_infoFilePathMapping.TryGetValue(bundleGuid, out string filePath) == false)
{
string folderName = GetHashFolderName(bundle.FileHash);
filePath = PathUtility.Combine(RootPath, folderName, bundle.BundleGuid, SandboxBundleCacheConsts.BundleInfoFileName);
_infoFilePathMapping.Add(bundle.BundleGuid, filePath);
string folderName = GetHashFolderName(bundleGuid);
filePath = PathUtility.Combine(RootPath, folderName, bundleGuid, SandboxBundleCacheConsts.BundleInfoFileName);
_infoFilePathMapping.Add(bundleGuid, filePath);
}
return filePath;
}
@@ -228,8 +229,9 @@ namespace YooAsset
/// <returns>数据临时文件的完整路径</returns>
internal string GetDataTempFilePath(PackageBundle bundle)
{
string folderName = GetHashFolderName(bundle.FileHash);
return PathUtility.Combine(RootPath, folderName, bundle.BundleGuid, SandboxBundleCacheConsts.BundleDataTempFileName);
string bundleGuid = bundle.BundleGuid;
string folderName = GetHashFolderName(bundleGuid);
return PathUtility.Combine(RootPath, folderName, bundleGuid, SandboxBundleCacheConsts.BundleDataTempFileName);
}
/// <summary>
@@ -239,8 +241,9 @@ namespace YooAsset
/// <returns>信息临时文件的完整路径</returns>
internal string GetInfoTempFilePath(PackageBundle bundle)
{
string folderName = GetHashFolderName(bundle.FileHash);
return PathUtility.Combine(RootPath, folderName, bundle.BundleGuid, SandboxBundleCacheConsts.BundleInfoTempFileName);
string bundleGuid = bundle.BundleGuid;
string folderName = GetHashFolderName(bundleGuid);
return PathUtility.Combine(RootPath, folderName, bundleGuid, SandboxBundleCacheConsts.BundleInfoTempFileName);
}
/// <summary>
@@ -297,14 +300,14 @@ namespace YooAsset
}
}
private string GetHashFolderName(string fileHash)
private string GetHashFolderName(string bundleGuid)
{
if (string.IsNullOrEmpty(fileHash))
throw new YooInternalException("File hash is null or empty.");
if (string.IsNullOrEmpty(bundleGuid))
throw new YooInternalException("Bundle GUID is null or empty.");
if (fileHash.Length <= HashFolderNameLength)
return fileHash;
return fileHash.Substring(0, HashFolderNameLength);
if (bundleGuid.Length <= SandboxBundleCacheConsts.HashFolderNameLength)
return bundleGuid;
return bundleGuid.Substring(0, SandboxBundleCacheConsts.HashFolderNameLength);
}
#endregion
}

View File

@@ -40,5 +40,10 @@ namespace YooAsset
/// 信息文件预期大小(字节)
/// </summary>
public const int InfoFileExpectedSize = 36;
/// <summary>
/// 哈希分片目录前缀长度
/// </summary>
public const int HashFolderNameLength = 2;
}
}

View File

@@ -233,7 +233,8 @@ namespace YooAsset
virtualWebGLMode: VirtualWebGLMode,
asyncSimulateMinFrame: AsyncSimulateMinFrame,
asyncSimulateMaxFrame: AsyncSimulateMaxFrame);
BundleCache = new EditorBundleCache(packageName, _packageRoot, cacheConfig);
string cacheRoot = GetEditorBundleCacheRoot();
BundleCache = new EditorBundleCache(packageName, cacheRoot, cacheConfig);
}
/// <inheritdoc />
public void OnDestroy()
@@ -279,6 +280,15 @@ namespace YooAsset
}
#region
/// <summary>
/// 获取编辑器缓存根目录路径
/// </summary>
private string GetEditorBundleCacheRoot()
{
string root = YooAssetConfiguration.GetEditorCacheRoot();
return PathUtility.Combine(root, PackageName, EditorFileSystemConsts.CacheFolderName);
}
/// <summary>
/// 获取编辑器包裹版本文件路径
/// </summary>

View File

@@ -0,0 +1,14 @@
namespace YooAsset
{
/// <summary>
/// 编辑器文件系统常量定义
/// </summary>
internal static class EditorFileSystemConsts
{
/// <summary>
/// 编辑器缓存文件的文件夹名称
/// </summary>
public const string CacheFolderName = "SimulateCache";
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 983d63c1d4cc2a24bb2d048e6c2feb2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: