Compare commits

...

32 Commits

Author SHA1 Message Date
何冠峰
3e3661ad95 refactor : 重构代码 2026-01-16 14:37:04 +08:00
何冠峰
611ac886b3 refactor : 重构代码 2026-01-16 10:30:02 +08:00
何冠峰
b0e85017d2 refactor : 重构代码 2026-01-16 10:28:01 +08:00
何冠峰
2b6e7856e7 update mini game 2026-01-14 19:28:30 +08:00
何冠峰
8d461056e4 update extension sample 2026-01-14 19:28:07 +08:00
何冠峰
f188cc715a update test sample 2026-01-14 19:27:39 +08:00
何冠峰
329cae1441 refactor : 重构资源包裹模块 2026-01-14 19:26:40 +08:00
何冠峰
c8c74b8c20 refactor : 重构资源包裹模块 2026-01-13 19:11:49 +08:00
何冠峰
b3d024743c update space shooter 2026-01-13 17:09:18 +08:00
何冠峰
246a62a675 refactor : 重构异步操作模块 2026-01-13 17:01:15 +08:00
何冠峰
354ca5197f refactor : 重构异步操作模块 2026-01-13 14:55:31 +08:00
何冠峰
ce4d6911db refactor : 重构异步操作模块 2026-01-13 12:08:02 +08:00
何冠峰
b796b1a44e refactor : 重构异步操作模块 2026-01-13 11:23:50 +08:00
何冠峰
7198e639d9 refactor : 重构异步操作模块 2026-01-12 16:10:24 +08:00
何冠峰
294fa18fec refactor : 重构异步操作模块 2026-01-12 15:35:00 +08:00
何冠峰
a3f689d815 refactor : 重构异步操作模块 2026-01-12 11:09:27 +08:00
何冠峰
d228e41df7 Merge branch 'dev' of https://github.com/tuyoogame/YooAsset into dev 2026-01-12 10:42:57 +08:00
何冠峰
23032cc269 Merge pull request #718 from absences/desc
add FileSystemParameters description
2026-01-12 10:40:30 +08:00
xiewen
72f02bd73f add FileSystemParameters description 2026-01-09 10:03:57 +08:00
何冠峰
a37663a8c2 refactor : 重构异步操作模块 2026-01-08 19:19:05 +08:00
何冠峰
f375d45bd6 refactor : 重构异步操作模块 2026-01-08 17:22:19 +08:00
何冠峰
3dd3d4ef76 refactor : 重构异步操作模块 2026-01-08 11:21:26 +08:00
何冠峰
f0563cce0b refactor : 重构网络下载模块 2026-01-07 15:52:32 +08:00
何冠峰
9b83dcf723 refactor : 重构网络下载模块 2026-01-07 15:08:05 +08:00
何冠峰
ee67a55c0f refactor : 重构网络下载模块 2026-01-07 10:23:11 +08:00
何冠峰
454afc9ba6 docs : 增加模块的文档说明 2026-01-06 17:12:07 +08:00
何冠峰
539ca3523e style : 规范代码注释 2026-01-06 14:57:15 +08:00
何冠峰
c87efdb509 refactor : 重构网络下载模块
新增通用下载接口,扩展了默认的Unity引擎下载器
2026-01-05 19:44:10 +08:00
何冠峰
1884fab0c2 refactor : remove weak reference handle 2025-12-23 15:07:52 +08:00
何冠峰
e5d0a856a5 perf : 异常处理替换为YOO的异常类 2025-12-05 15:45:04 +08:00
何冠峰
5da8c6baf8 perf : 文件验证和文件下载并发设置为合理的默认值,并限制参数为合理范围。 2025-12-04 21:12:49 +08:00
何冠峰
33356cb270 完善一些高危风险的代码容错机制。 2025-12-04 20:34:29 +08:00
292 changed files with 14703 additions and 4724 deletions

View File

@@ -19,7 +19,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = EBuildPipeline.EditorSimulateBuildPipeline.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.VirtualBundle;
buildParameters.BuildBundleType = (int)EBundleType.VirtualBundle;
buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
buildParameters.PackageName = packageName;
buildParameters.PackageVersion = "Simulate";

View File

@@ -96,7 +96,7 @@ namespace YooAsset.Editor
/// <summary>
/// 资源包加密服务类
/// </summary>
public IEncryptionServices EncryptionServices;
public IBundleEncryptionServices EncryptionServices;
/// <summary>
/// 资源清单加密服务类
@@ -146,7 +146,7 @@ namespace YooAsset.Editor
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildPipelineIsNullOrEmpty, "Build pipeline is null or empty !");
throw new Exception(message);
}
if (BuildBundleType == (int)EBuildBundleType.Unknown)
if (BuildBundleType == (int)EBundleType.Unknown)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildBundleTypeIsUnknown, $"Build bundle type is unknown {BuildBundleType} !");
throw new Exception(message);

View File

@@ -15,7 +15,7 @@ namespace YooAsset.Editor
string buildinRootDirectory = buildParametersContext.GetBuildinRootDirectory();
string buildPackageName = buildParametersContext.Parameters.PackageName;
var manifestServices = buildParametersContext.Parameters.ManifestRestoreServices;
CatalogTools.CreateCatalogFile(manifestServices, buildPackageName, buildinRootDirectory);
CatalogFileHelper.CreateFile(manifestServices, buildPackageName, buildinRootDirectory);
// 刷新目录
AssetDatabase.Refresh();

View File

@@ -31,7 +31,7 @@ namespace YooAsset.Editor
// 创建新补丁清单
PackageManifest manifest = new PackageManifest();
manifest.FileVersion = ManifestDefine.FileVersion;
manifest.FileVersion = PackageManifestDefine.FileVersion;
manifest.EnableAddressable = buildMapContext.Command.EnableAddressable;
manifest.SupportExtensionless = buildMapContext.Command.SupportExtensionless;
manifest.LocationToLower = buildMapContext.Command.LocationToLower;
@@ -71,7 +71,7 @@ namespace YooAsset.Editor
{
string fileName = YooAssetSettingsData.GetManifestJsonFileName(buildParameters.PackageName, buildParameters.PackageVersion);
string filePath = $"{packageOutputDirectory}/{fileName}";
ManifestTools.SerializeToJson(filePath, manifest);
PackageManifestTools.SerializeToJson(filePath, manifest);
BuildLogger.Log($"Create package manifest file: {filePath}");
}
@@ -81,7 +81,7 @@ namespace YooAsset.Editor
{
string fileName = YooAssetSettingsData.GetManifestBinaryFileName(buildParameters.PackageName, buildParameters.PackageVersion);
packagePath = $"{packageOutputDirectory}/{fileName}";
ManifestTools.SerializeToBinary(packagePath, manifest, buildParameters.ManifestProcessServices);
PackageManifestTools.SerializeToBinary(packagePath, manifest, buildParameters.ManifestProcessServices);
packageHash = HashUtility.FileCRC32(packagePath);
BuildLogger.Log($"Create package manifest file: {packagePath}");
}
@@ -106,7 +106,7 @@ namespace YooAsset.Editor
{
ManifestContext manifestContext = new ManifestContext();
byte[] bytesData = FileUtility.ReadAllBytes(packagePath);
manifestContext.Manifest = ManifestTools.DeserializeFromBinary(bytesData, buildParameters.ManifestRestoreServices);
manifestContext.Manifest = PackageManifestTools.DeserializeFromBinary(bytesData, buildParameters.ManifestRestoreServices);
context.SetContextObject(manifestContext);
}
}

View File

@@ -24,7 +24,7 @@ namespace YooAsset.Editor
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
foreach (var bundleInfo in buildMapContext.Collection)
{
EncryptFileInfo fileInfo = new EncryptFileInfo();
EncryptBundleInfo fileInfo = new EncryptBundleInfo();
fileInfo.BundleName = bundleInfo.BundleName;
fileInfo.FileLoadPath = $"{pipelineOutputDirectory}/{bundleInfo.BundleName}";
var encryptResult = encryptionServices.Encrypt(fileInfo);

View File

@@ -54,8 +54,8 @@ namespace YooAsset.Editor
{
string bundleName = bundleInfo.BundleName;
string fileHash = bundleInfo.PackageFileHash;
string fileExtension = ManifestTools.GetRemoteBundleFileExtension(bundleName);
string fileName = ManifestTools.GetRemoteBundleFileName(outputNameStyle, bundleName, fileExtension, fileHash);
string fileExtension = PackageManifestTools.GetRemoteBundleFileExtension(bundleName);
string fileName = PackageManifestTools.GetRemoteBundleFileName(outputNameStyle, bundleName, fileExtension, fileHash);
bundleInfo.PackageDestFilePath = $"{packageOutputDirectory}/{fileName}";
}
}

View File

@@ -1,9 +1,9 @@

namespace YooAsset.Editor
{
public class EncryptionNone : IEncryptionServices
public class EncryptionNone : IBundleEncryptionServices
{
public EncryptResult Encrypt(EncryptFileInfo fileInfo)
public EncryptResult Encrypt(EncryptBundleInfo fileInfo)
{
throw new System.NotImplementedException();
}

View File

@@ -46,13 +46,13 @@ namespace YooAsset.Editor
/// <summary>
/// 创建资源包加密服务类实例
/// </summary>
protected IEncryptionServices CreateEncryptionServicesInstance()
protected IBundleEncryptionServices CreateEncryptionServicesInstance()
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
var classTypes = EditorTools.GetAssignableTypes(typeof(IBundleEncryptionServices));
var classType = classTypes.Find(x => x.FullName.Equals(className));
if (classType != null)
return (IEncryptionServices)Activator.CreateInstance(classType);
return (IBundleEncryptionServices)Activator.CreateInstance(classType);
else
return null;
}
@@ -184,7 +184,7 @@ namespace YooAsset.Editor
protected PopupField<Type> CreateEncryptionServicesField(VisualElement container)
{
// 资源包加密服务类
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
var classTypes = EditorTools.GetAssignableTypes(typeof(IBundleEncryptionServices));
if (classTypes.Count > 0)
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);

View File

@@ -109,7 +109,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildBundleType = (int)EBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -66,7 +66,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.VirtualBundle;
buildParameters.BuildBundleType = (int)EBundleType.VirtualBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -103,7 +103,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.RawBundle;
buildParameters.BuildBundleType = (int)EBundleType.RawBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -109,7 +109,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildBundleType = (int)EBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -277,7 +277,7 @@ namespace YooAsset.Editor
if (dependTableData.BundleInfo.Encrypted)
return;
if (_buildReport.Summary.BuildBundleType == (int)EBuildBundleType.AssetBundle)
if (_buildReport.Summary.BuildBundleType == (int)EBundleType.AssetBundle)
{
string rootDirectory = Path.GetDirectoryName(_reportFilePath);
string filePath = $"{rootDirectory}/{dependTableData.BundleInfo.FileName}";

View File

@@ -348,7 +348,7 @@ namespace YooAsset.Editor
if (bundleTableData.BundleInfo.Encrypted)
return;
if (_buildReport.Summary.BuildBundleType == (int)EBuildBundleType.AssetBundle)
if (_buildReport.Summary.BuildBundleType == (int)EBundleType.AssetBundle)
{
string rootDirectory = Path.GetDirectoryName(_reportFilePath);
string filePath = $"{rootDirectory}/{bundleTableData.BundleInfo.FileName}";

View File

@@ -0,0 +1,945 @@
# DiagnosticSystem 诊断系统
## 模块概述
DiagnosticSystem 是 YooAsset 的**远程调试诊断系统**提供运行时资源管理状态的实时可视化和性能分析能力。该系统通过编辑器窗口与运行时游戏进行双向通信实时采集和展示资源加载、Bundle 管理、异步操作等调试信息。
### 核心特性
- **实时远程调试**:在 Unity 编辑器中查看游戏运行时的资源管理状态
- **完整状态快照**采集所有资源、Bundle、异步操作的实时信息
- **历史数据回溯**:缓存最近 500 帧数据,支持时间回溯分析
- **双模式采样**:支持单次采样和自动连续采样
- **低性能开销**:按需采样,无需连续监控
### 模块统计
| 组件 | 职责 |
|------|------|
| 核心通信 | RemoteDebuggerInRuntime + 双连接层 |
| 数据结构 | 5 个调试信息结构体 |
| 命令系统 | RemoteCommand 命令定义 |
| **总计** | 10 个核心文件,完整的远程诊断框架 |
---
## 设计目标
| 目标 | 说明 |
|------|------|
| **实时可视化** | 在编辑器中实时查看运行时资源状态 |
| **性能监控** | 收集加载耗时、引用计数、下载进度等性能指标 |
| **内存诊断** | 追踪资源出生场景、引用计数,识别潜在内存泄漏 |
| **操作追踪** | 显示异步操作树,包括嵌套和依赖关系 |
| **低开销设计** | 按需采样而非连续监控DEBUG 模式自动启用 |
---
## 文件结构
```
DiagnosticSystem/
├── RemoteDebuggerDefine.cs # 全局定义和常量
├── RemoteCommand.cs # 命令定义和序列化
├── DebugReport.cs # 调试报告(顶层容器)
├── DebugPackageData.cs # 包级调试数据
├── DebugProviderInfo.cs # 资源加载器调试信息
├── DebugBundleInfo.cs # 资源包调试信息
├── DebugOperationInfo.cs # 异步操作调试信息
├── RemoteDebuggerInRuntime.cs # 运行时调试器主类
├── RemotePlayerConnection.cs # 编辑器模拟连接层
└── RemoteEditorConnection.cs # 运行时模拟连接层
```
---
## 核心类说明
### RemoteDebuggerDefine
全局定义类,包含调试器版本和通信协议的 GUID 标识符。
```csharp
internal class RemoteDebuggerDefine
{
// 调试器版本(用于版本校验)
public const string DebuggerVersion = "2.3.3";
// 消息标识符GUID
public static readonly Guid kMsgPlayerSendToEditor =
new Guid("e34a5702dd353724aa315fb8011f08c3"); // 运行时→编辑器
public static readonly Guid kMsgEditorSendToPlayer =
new Guid("4d1926c9df5b052469a1c63448b7609a"); // 编辑器→运行时
}
```
### RemoteCommand
命令定义类,用于编辑器向运行时发送采样指令。
```csharp
internal enum ERemoteCommand
{
SampleOnce = 0, // 单次采样
SampleAuto = 1, // 自动采样(连续)
}
[Serializable]
internal class RemoteCommand
{
public int CommandType; // ERemoteCommand 枚举值
public string CommandParam; // 命令参数
// 序列化/反序列化JSON 格式)
public static byte[] Serialize(RemoteCommand command)
{
return Encoding.UTF8.GetBytes(JsonUtility.ToJson(command));
}
public static RemoteCommand Deserialize(byte[] data)
{
return JsonUtility.FromJson<RemoteCommand>(Encoding.UTF8.GetString(data));
}
}
```
**命令示例:**
```json
// 单次采样
{
"CommandType": 0,
"CommandParam": ""
}
// 开启自动采样
{
"CommandType": 1,
"CommandParam": "open"
}
// 关闭自动采样
{
"CommandType": 1,
"CommandParam": "close"
}
```
### DebugReport
调试报告容器,包含完整的系统状态快照。
```csharp
[Serializable]
internal class DebugReport
{
// 调试器版本(用于版本校验)
public string DebuggerVersion = RemoteDebuggerDefine.DebuggerVersion;
// 游戏帧数
public int FrameCount;
// 包级调试数据列表(一个游戏可能有多个资源包)
public List<DebugPackageData> PackageDatas = new List<DebugPackageData>(10);
// 序列化/反序列化
public static byte[] Serialize(DebugReport debugReport);
public static DebugReport Deserialize(byte[] data);
}
```
### DebugPackageData
包级调试数据,包含单个资源包的所有诊断信息。
```csharp
[Serializable]
internal class DebugPackageData
{
// 资源包名称
public string PackageName;
// 资源加载器列表
public List<DebugProviderInfo> ProviderInfos = new List<DebugProviderInfo>(1000);
// 资源包列表
public List<DebugBundleInfo> BundleInfos = new List<DebugBundleInfo>(1000);
// 异步操作列表
public List<DebugOperationInfo> OperationInfos = new List<DebugOperationInfo>(1000);
// 运行时查询字典(非序列化,按需构建)
[NonSerialized]
public Dictionary<string, DebugBundleInfo> BundleInfoDic;
// 延迟解析字典
public DebugBundleInfo GetBundleInfo(string bundleName);
}
```
### DebugProviderInfo
资源加载器Provider的调试信息。
```csharp
[Serializable]
internal struct DebugProviderInfo : IComparer<DebugProviderInfo>, IComparable<DebugProviderInfo>
{
public string PackageName; // 所属包名
public string AssetPath; // 资源路径(如 "Assets/Prefabs/Player.prefab"
public string SpawnScene; // 资源加载时的活跃场景名
public string BeginTime; // 加载开始时间格式HH:mm:ss.fff
public long LoadingTime; // 加载耗时(单位:毫秒)
public int RefCount; // 引用计数
public string Status; // 加载状态None/Processing/Succeed/Failed
public List<string> DependBundles; // 依赖的资源包名列表
// 按 AssetPath 字母排序
public int CompareTo(DebugProviderInfo other);
}
```
**关键诊断价值:**
- `SpawnScene`:追踪资源在哪个场景被加载,帮助识别资源泄漏
- `LoadingTime`:性能分析,识别加载慢的资源
- `RefCount`引用计数监控RefCount > 0 表示资源仍在使用
- `DependBundles`:依赖分析,理解资源加载的完整依赖链
### DebugBundleInfo
资源包Bundle的调试信息。
```csharp
[Serializable]
internal struct DebugBundleInfo : IComparer<DebugBundleInfo>, IComparable<DebugBundleInfo>
{
public string BundleName; // 资源包名称
public int RefCount; // 引用计数(当前有多少个 Provider 在使用)
public string Status; // 加载状态
public List<string> ReferenceBundles; // 反向依赖(谁引用了我)
// 按 BundleName 字母排序
public int CompareTo(DebugBundleInfo other);
}
```
**关键诊断价值:**
- `RefCount`Bundle 引用计数,为 0 时可以被卸载
- `ReferenceBundles`:反向依赖分析,了解 Bundle 被哪些其他 Bundle 依赖
### DebugOperationInfo
异步操作的调试信息,支持递归树结构。
```csharp
[Serializable]
internal struct DebugOperationInfo : IComparer<DebugOperationInfo>, IComparable<DebugOperationInfo>
{
public string OperationName; // 操作类名(如 "LoadAssetOperation"
public string OperationDesc; // 操作说明(自定义描述)
public uint Priority; // 优先级(用于操作排序)
public float Progress; // 进度0.0 - 1.0
public string BeginTime; // 操作开始时间
public long ProcessTime; // 处理耗时(单位:毫秒)
public string Status; // 操作状态None/Processing/Succeed/Failed
public List<DebugOperationInfo> Childs; // 子操作列表(支持嵌套树结构)
public int CompareTo(DebugOperationInfo other);
}
```
**递归树结构示例:**
```
InitializationOperation
├─ LoadManifestOperation
│ ├─ LoadBundleFileOperation (manifest.bundle)
│ └─ DeserializeManifestOperation
└─ InitFileSystemOperation
```
**关键诊断价值:**
- `OperationName`:操作类型识别
- `ProcessTime`:性能瓶颈分析
- `Childs`:操作依赖关系可视化
---
## 通信系统
### RemoteDebuggerInRuntime
运行时调试器主类,负责接收命令、采样数据、发送报告。
```csharp
internal class RemoteDebuggerInRuntime : MonoBehaviour
{
// 采样控制标志
private static bool _sampleOnce = false; // 单次采样
private static bool _autoSample = false; // 连续采样
// 运行时初始化
[RuntimeInitializeOnLoadMethod]
private static void RuntimeInitializeOnLoad()
{
_sampleOnce = false;
_autoSample = false;
}
private void Awake()
{
RemotePlayerConnection.Instance.Initialize();
}
private void OnEnable()
{
// 注册命令接收回调
RemotePlayerConnection.Instance.Register(
RemoteDebuggerDefine.kMsgEditorSendToPlayer,
OnHandleEditorMessage);
}
private void LateUpdate()
{
// 采样逻辑(在一帧的最后执行)
if (_autoSample || _sampleOnce)
{
_sampleOnce = false;
var debugReport = YooAssets.GetDebugReport();
var data = DebugReport.Serialize(debugReport);
RemotePlayerConnection.Instance.Send(
RemoteDebuggerDefine.kMsgPlayerSendToEditor,
data);
}
}
private static void OnHandleEditorMessage(MessageEventArgs args)
{
var command = RemoteCommand.Deserialize(args.data);
if (command.CommandType == (int)ERemoteCommand.SampleOnce)
{
_sampleOnce = true;
}
else if (command.CommandType == (int)ERemoteCommand.SampleAuto)
{
_autoSample = (command.CommandParam == "open");
}
}
}
```
**关键设计点:**
1. **LateUpdate 时机**:确保该帧所有资源加载完成后再采样
2. **状态重置**`[RuntimeInitializeOnLoadMethod]` 确保编辑器重新编译时重置状态
3. **DEBUG 模式自动启用**:通过 `#if DEBUG` 条件编译自动添加组件
### 双连接层架构
YooAsset 支持两种通信模式:
| 模式 | 使用场景 | 实现方式 |
|------|----------|----------|
| **编辑器模拟模式** | 开发调试 | `RemotePlayerConnection` + `RemoteEditorConnection`(虚拟连接) |
| **发布版本** | 运营期监控 | Unity 的 `PlayerConnection` API真实网络 |
#### RemotePlayerConnection编辑器模拟模式
```csharp
internal class RemotePlayerConnection
{
private static RemotePlayerConnection _instance;
private readonly Dictionary<Guid, UnityAction<MessageEventArgs>> _messageCallbacks;
public static RemotePlayerConnection Instance
{
get
{
if (_instance == null)
_instance = new RemotePlayerConnection();
return _instance;
}
}
public void Register(Guid messageID, UnityAction<MessageEventArgs> callback)
{
_messageCallbacks.Add(messageID, callback);
}
public void Send(Guid messageID, byte[] data)
{
// 在编辑器模拟模式下,发送给虚拟编辑器连接
RemoteEditorConnection.Instance.HandlePlayerMessage(messageID, data);
}
internal void HandleEditorMessage(Guid messageID, byte[] data)
{
if (_messageCallbacks.TryGetValue(messageID, out var callback))
{
callback.Invoke(new MessageEventArgs { playerId = 0, data = data });
}
}
}
```
#### RemoteEditorConnection运行时模拟模式
```csharp
internal class RemoteEditorConnection
{
private static RemoteEditorConnection _instance;
private readonly Dictionary<Guid, UnityAction<MessageEventArgs>> _messageCallbacks;
public static RemoteEditorConnection Instance
{
get
{
if (_instance == null)
_instance = new RemoteEditorConnection();
return _instance;
}
}
public void Register(Guid messageID, UnityAction<MessageEventArgs> callback)
{
_messageCallbacks.Add(messageID, callback);
}
public void Send(Guid messageID, byte[] data)
{
// 发送给虚拟运行时连接
RemotePlayerConnection.Instance.HandleEditorMessage(messageID, data);
}
internal void HandlePlayerMessage(Guid messageID, byte[] data)
{
if (_messageCallbacks.TryGetValue(messageID, out var callback))
{
callback.Invoke(new MessageEventArgs { playerId = 0, data = data });
}
}
}
```
---
## 通信协议
### 协议规范
**协议版本:** 2.3.3
**编码格式:**
```
C# 对象 → JsonUtility.ToJson() → JSON 字符串 → Encoding.UTF8.GetBytes() → byte[]
```
**消息类型:**
| 方向 | GUID | 数据类型 | 说明 |
|------|------|----------|------|
| 编辑器→运行时 | `4d1926c9df5b052469a1c63448b7609a` | `RemoteCommand` | 采样命令 |
| 运行时→编辑器 | `e34a5702dd353724aa315fb8011f08c3` | `DebugReport` | 调试报告 |
### 双向通信流程
```
[编辑器 UI]
├─ 用户点击 "Sample" 按钮
│ └─ 发送 RemoteCommand (SampleOnce)
└─ 用户开启 "Record" 开关
└─ 发送 RemoteCommand (SampleAuto, "open")
↓ RemoteEditorConnection.Send()
RemotePlayerConnection.HandleEditorMessage()
↓ 触发回调
[运行时]
RemoteDebuggerInRuntime.OnHandleEditorMessage()
设置采样标志 (_sampleOnce 或 _autoSample)
LateUpdate 中采样
YooAssets.GetDebugReport()
├─ 收集所有 ResourcePackage 数据
├─ DebugProviderInfo[] (从 ProviderDic)
├─ DebugBundleInfo[] (从 LoaderDic)
└─ DebugOperationInfo[] (从 _operations)
DebugReport.Serialize()
RemotePlayerConnection.Send()
RemoteEditorConnection.HandlePlayerMessage()
[编辑器]
AssetBundleDebuggerWindow.OnHandlePlayerMessage()
版本校验 (DebuggerVersion)
RemotePlayerSession.AddDebugReport()
缓存到历史记录 (最多 500 帧)
UI 刷新显示
```
### 版本校验机制
```csharp
// 编辑器端版本校验
private void OnHandlePlayerMessage(MessageEventArgs args)
{
var debugReport = DebugReport.Deserialize(args.data);
// 版本校验
if (debugReport.DebuggerVersion != RemoteDebuggerDefine.DebuggerVersion)
{
Debug.LogWarning(
$"Debugger versions are inconsistent : " +
$"{debugReport.DebuggerVersion} != {RemoteDebuggerDefine.DebuggerVersion}");
return; // 丢弃不兼容的数据
}
// 处理数据...
}
```
**设计意图:** 防止编辑器和运行时的调试器版本不一致导致的数据格式错误。
---
## 数据收集流程
### 完整采样流程
```
RemoteDebuggerInRuntime.LateUpdate()
检查 _sampleOnce 或 _autoSample 标志
↓ YES
调用 YooAssets.GetDebugReport()
├─ 初始化 DebugReport
├─ 设置 FrameCount = Time.frameCount
├─ 遍历每个 ResourcePackage
│ │
│ └─ package.GetDebugPackageData()
│ │
│ ├─ 创建 DebugPackageData
│ ├─ 设置 PackageName
│ │
│ ├─ 收集 ProviderInfos
│ │ ResourceManager.GetDebugProviderInfos()
│ │ 遍历 ProviderDic
│ │ └─ 每个 ProviderOperation 提供:
│ │ - MainAssetInfo.AssetPath
│ │ - SpawnScene场景名
│ │ - BeginTime开始时间
│ │ - ProcessTime耗时
│ │ - RefCount引用计数
│ │ - Status加载状态
│ │ - GetDebugDependBundles()(依赖列表)
│ │
│ ├─ 收集 BundleInfos
│ │ ResourceManager.GetDebugBundleInfos()
│ │ 遍历 LoaderDic
│ │ └─ 每个 LoadBundleFileOperation 提供:
│ │ - BundleName
│ │ - RefCount
│ │ - Status
│ │ - FilterReferenceBundles()(反向依赖)
│ │
│ └─ 收集 OperationInfos
│ OperationSystem.GetDebugOperationInfos(PackageName)
│ 遍历 _operations按 PackageName 过滤):
│ └─ 递归构建操作树:
│ - GetType().Name操作类名
│ - GetOperationDesc()(自定义描述)
│ - Priority优先级
│ - Progress进度
│ - BeginTime开始时间
│ - ProcessTime耗时
│ - Status状态
│ - Childs子操作列表
└─ 返回 DebugReport
```
### 性能指标收集
#### 资源加载耗时
```csharp
// AsyncOperationBase 中的自动计时
private Stopwatch _watch = null;
internal void InternalStart()
{
if (_watch == null)
{
BeginTime = SpawnTimeToString(UnityEngine.Time.realtimeSinceStartup);
_watch = Stopwatch.StartNew();
}
}
internal void InternalUpdate()
{
ProcessTime = _watch.ElapsedMilliseconds;
// ... 持续计时
}
```
#### 场景信息追踪
```csharp
// ProviderOperation.cs
[Conditional("DEBUG")] // 仅在 DEBUG 模式下启用
public void InitProviderDebugInfo()
{
SpawnScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
}
```
#### 引用计数监控
```csharp
// ProviderOperation 和 LoadBundleFileOperation 都维护 RefCount
public int RefCount { get; } // 当前被引用的次数
```
---
## 与其他模块的交互
```
YooAssets (全局入口)
├─ 初始化阶段:
│ #if DEBUG
│ _driver.AddComponent<RemoteDebuggerInRuntime>();
│ #endif
└─ 数据收集入口:
GetDebugReport()
├─ 遍历所有 ResourcePackage
└─ 构建 DebugReport
ResourcePackage (资源包)
└─ GetDebugPackageData()
├─ 调用 ResourceManager.GetDebugProviderInfos()
├─ 调用 ResourceManager.GetDebugBundleInfos()
└─ 调用 OperationSystem.GetDebugOperationInfos()
ResourceManager (资源管理器)
├─ GetDebugProviderInfos()
│ └─ 遍历 ProviderDic (Dictionary<string, ProviderOperation>)
└─ GetDebugBundleInfos()
└─ 遍历 LoaderDic (Dictionary<string, LoadBundleFileOperation>)
OperationSystem (操作系统)
└─ GetDebugOperationInfos(packageName)
└─ 遍历 _operations (List<AsyncOperationBase>)
└─ 递归收集子操作 (Childs)
AsyncOperationBase (异步操作基类)
├─ BeginTime操作开始时间
├─ ProcessTime累计处理耗时
├─ Status操作状态
├─ Progress进度
└─ GetOperationDesc():自定义描述
ProviderOperation (资源提供者)
├─ SpawnScene加载时的活跃场景
└─ GetDebugDependBundles():依赖包列表
LoadBundleFileOperation (Bundle 加载器)
├─ RefCount引用计数
└─ LoadBundleInfoBundle 信息
```
---
## 使用场景
### 场景 1运行时资源泄漏诊断
**问题:** 游戏切换场景后内存持续增长,怀疑有资源未释放。
**诊断步骤:**
1. 打开 AssetBundle Debugger 窗口
2. 开启 Record 模式(自动采样)
3. 切换场景前后观察 ProviderInfos 列表
4. 检查 `RefCount > 0``SpawnScene` 为旧场景的资源
5. 定位未释放的资源和对应的代码位置
**关键字段:**
- `SpawnScene`:资源在哪个场景被加载
- `RefCount`:引用计数,应该为 0
- `AssetPath`:资源路径,定位具体资源
### 场景 2资源加载性能分析
**问题:** 首次加载场景卡顿严重。
**诊断步骤:**
1. 单次采样Sample Once
2. 切换到 Asset View
3.`LoadingTime` 降序排序
4. 识别加载耗时最长的资源
5. 分析 `DependBundles` 了解依赖链
**关键字段:**
- `LoadingTime`:加载耗时(毫秒)
- `DependBundles`:依赖的 Bundle 列表
- `Status`:加载状态
### 场景 3Bundle 引用分析
**问题:** 某个 Bundle 无法被卸载。
**诊断步骤:**
1. 切换到 Bundle View
2. 搜索目标 Bundle
3. 检查 `RefCount``ReferenceBundles`
4. 追踪哪些资源正在使用该 Bundle
5. 定位未释放的资源引用
**关键字段:**
- `RefCount`Bundle 引用计数
- `ReferenceBundles`:反向依赖列表
- `Status`Bundle 加载状态
### 场景 4异步操作监控
**问题:** 复杂的初始化流程卡住,不知道在哪个步骤。
**诊断步骤:**
1. 切换到 Operation View
2. 查看操作树结构
3. 检查 `Status``Processing` 的操作
4. 分析 `Progress` 了解进度
5. 通过 `Childs` 了解操作依赖关系
**关键字段:**
- `OperationName`:操作类型
- `OperationDesc`:操作描述
- `Progress`进度0.0 - 1.0
- `Childs`:子操作列表
---
## 数据导出
### JSON 导出功能
编辑器窗口支持导出当前帧的完整调试数据为 JSON 文件。
**导出示例:**
```json
{
"DebuggerVersion": "2.3.3",
"FrameCount": 1234,
"PackageDatas": [
{
"PackageName": "DefaultPackage",
"ProviderInfos": [
{
"PackageName": "DefaultPackage",
"AssetPath": "Assets/Prefabs/Player.prefab",
"SpawnScene": "GameScene",
"BeginTime": "12:34:56.789",
"LoadingTime": 45,
"RefCount": 1,
"Status": "Succeed",
"DependBundles": [
"assets_prefabs.bundle"
]
}
],
"BundleInfos": [
{
"BundleName": "assets_prefabs.bundle",
"RefCount": 1,
"Status": "Succeed",
"ReferenceBundles": []
}
],
"OperationInfos": [
{
"OperationName": "LoadAssetOperation",
"OperationDesc": "Load assets_prefabs.bundle",
"Priority": 0,
"Progress": 1.0,
"BeginTime": "12:34:56.745",
"ProcessTime": 44,
"Status": "Succeed",
"Childs": []
}
]
}
]
}
```
**用途:**
- 离线分析和归档
- 性能数据对比
- 问题复现和追踪
---
## 系统架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ Unity Editor Window │
│ AssetBundleDebuggerWindow │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ UI Controls: │ │
│ │ - Sample Button (SampleOnce) │ │
│ │ - Record Toggle (SampleAuto) │ │
│ │ - View Mode Menu (Asset/Bundle/Operation View) │ │
│ │ - Frame Slider (历史帧导航) │ │
│ │ - Search Field (关键词搜索) │ │
│ │ - Export Button (JSON 导出) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────┬──────────────────────────────────────────────────────────┘
┌─────▼───────────────────────────────────┐
│ RemoteEditorConnection (虚拟连接) │
│ - Register callbacks │
│ - Send/Receive commands & reports │
└─────┬──────────────────────────────┬────┘
│ │
┌────────▼─────────────┐ ┌──────────▼───────────────┐
│ RemoteCommand │ │ DebugReport │
│ (Serialize) │ │ (Deserialize) │
│ ↓ JSON │ │ ← JSON │
│ ↓ UTF-8 bytes │ │ ← UTF-8 bytes │
└────────┬─────────────┘ └──────────┬───────────────┘
│ │
│ ═══════════════════════ │
│ Internet / Emulation │
│ ═══════════════════════ │
│ │
┌────────▼─────────────┐ ┌──────────▼───────────────┐
│ PlayerConnection │ │ RemotePlayerConnection │
│ (真实连接) │ │ (虚拟连接) │
│ 或模拟连接 │ │ │
└────────┬─────────────┘ └──────────┬───────────────┘
│ │
└──────────────┬───────────────┘
┌──────▼──────────────────────────────┐
│ RemoteDebuggerInRuntime │
│ (MonoBehaviour) │
│ ┌──────────────────────────────┐ │
│ │ _sampleOnce (bool) │ │
│ │ _autoSample (bool) │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ Awake() - 初始化连接 │ │
│ │ OnEnable() - 注册回调 │ │
│ │ LateUpdate() - 采样触发 │ │
│ │ OnHandleEditorMessage() - 收命令│ │
│ └──────────────────────────────┘ │
└──────────┬──────────────────────────┘
┌──────▼────────────────────┐
│ YooAssets.GetDebugReport() │
│ (全系统数据收集入口) │
└──────┬────────────────────┘
┌───────────┼───────────┐
│ │ │
┌──────────▼──┐ ┌─────▼────┐ ┌──▼─────────────┐
│ResourcePkg 1│ │ResourcePkg2 │ResourcePackageN│
└──────┬──────┘ └────┬─────┘ └──┬──────────┘
│ │ │
└───────────────┼──────────┘
┌───────────▼──────────┐
│ DebugPackageData │
│ ┌─────────────────┐ │
│ │ PackageName │ │
│ │ ProviderInfos[] │ │
│ │ BundleInfos[] │ │
│ │ OperationInfos[]│ │
│ └─────────────────┘ │
└─────────────────────┘
```
---
## 注意事项
1. **DEBUG 模式自动启用**
- 诊断系统仅在 `DEBUG` 模式下启用(通过 `#if DEBUG` 条件编译)
- Release 构建中不会包含诊断代码,无性能开销
2. **版本兼容性**
- 编辑器和运行时的调试器版本必须一致
- 版本不一致的数据会被自动丢弃
3. **历史数据限制**
- 最多缓存 500 帧历史数据(可配置)
- 超过限制后,最早的数据会被移除
4. **JSON 序列化深度限制**
- Unity JsonUtility 序列化深度限制为 10 层
- 操作树Childs嵌套过深可能导致序列化失败
5. **性能开销**
- 单次采样:低开销,仅在需要时采集
- 自动采样:每帧采集,有一定性能开销,建议仅在需要时开启
6. **LateUpdate 时机**
- 采样在 LateUpdate 中执行,确保该帧所有操作已更新
- 避免在采样过程中资源状态发生变化
7. **非序列化字典**
- `DebugPackageData.BundleInfoDic` 使用 `[NonSerialized]` 标记
- 字典在首次查询时才构建,减少序列化开销
8. **场景追踪条件编译**
- `SpawnScene` 字段仅在 DEBUG 模式下赋值(`[Conditional("DEBUG")]`
- Release 构建中该字段为空字符串
---
## 性能优化建议
1. **按需采样**
- 优先使用单次采样Sample Once
- 仅在需要连续监控时开启自动采样Record
2. **及时关闭 Record**
- 分析完成后及时关闭自动采样
- 避免不必要的性能开销
3. **合理设置历史缓存**
- 根据内存情况调整 `MaxReportCount`
- 默认 500 帧已足够大多数分析场景
4. **导出数据离线分析**
- 对于复杂的性能问题,导出 JSON 数据
- 在编辑器外使用专业工具进行分析
5. **Release 构建移除诊断代码**
- 确保 Release 构建使用 Release 配置
- 诊断代码通过 `#if DEBUG` 自动移除

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5fa2b66c20800124c8dd5cb77e854ce3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e9d6cb1ce5d510645866ad7c122abfab
guid: bb70e2274ce5a5d419cfbd7212efdf4a
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,110 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 下载后台
/// </summary>
/// <remarks>
/// 基于 Unity 内置 UnityWebRequest 实现的下载后台。
/// 支持自定义 UnityWebRequest 创建方式,例如添加证书验证、代理设置等。
/// </remarks>
internal sealed class UnityWebRequestBackend : IDownloadBackend
{
private readonly UnityWebRequestCreator _webRequestCreator;
/// <summary>
/// 后端名称
/// </summary>
public string Name
{
get { return nameof(UnityWebRequestBackend); }
}
/// <summary>
/// 创建 UnityWebRequest 下载后端(使用默认创建方式)
/// </summary>
public UnityWebRequestBackend() : this(null)
{
}
/// <summary>
/// 创建 UnityWebRequest 下载后端
/// </summary>
/// <param name="webRequestCreator">
/// 自定义 UnityWebRequest 创建委托(可选)。
/// 如果为 null则使用默认的 UnityWebRequest 构造方式。
/// </param>
public UnityWebRequestBackend(UnityWebRequestCreator webRequestCreator)
{
_webRequestCreator = webRequestCreator;
}
/// <summary>
/// 驱动更新
/// </summary>
/// <remarks>
/// UnityWebRequest 由 Unity 引擎自动驱动,无需额外更新。
/// </remarks>
public void Update()
{
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
// 无需释放资源
}
/// <summary>
/// 创建 HEAD 请求
/// </summary>
public IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestHeadDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建文件下载请求
/// </summary>
public IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args)
{
return new UnityWebRequestFileDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建字节下载请求
/// </summary>
public IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestBytesDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建文本下载请求
/// </summary>
public IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestTextDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建 AssetBundle 下载请求
/// </summary>
public IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args)
{
return new UnityWebRequestAssetBundleDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建模拟下载请求
/// </summary>
public IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args)
{
return new VirtualFileDownloader(args);
}
}
}

View File

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

View File

@@ -0,0 +1,10 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
/// <summary>
/// 自定义下载器的请求委托
/// </summary>
public delegate UnityWebRequest UnityWebRequestCreator(string url, string method);
}

View File

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

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 4630dac2050606043bb146325fdce6ad
fileFormatVersion: 2
guid: 1d30af9086de4fb282c2cdd3d26244c0
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,90 @@
using System;
using UnityEngine;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest AssetBundle 下载器
/// </summary>
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包。
/// 支持 Unity 内置缓存机制和 CRC 校验。
/// </remarks>
internal sealed class UnityWebRequestAssetBundleDownloader : UnityWebRequestDownloaderBase, IDownloadAssetBundleRequest
{
private readonly DownloadAssetBundleRequestArgs _args;
private DownloadHandlerAssetBundle _downloadHandler;
/// <summary>
/// 下载结果AssetBundle 对象)
/// </summary>
public AssetBundle Result { get; private set; }
/// <summary>
/// 构造 AssetBundle 下载器
/// </summary>
/// <param name="args">AssetBundle 下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestAssetBundleDownloader(DownloadAssetBundleRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
_downloadHandler = CreateAssetBundleDownloadHandler();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = _downloadHandler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
AssetBundle assetBundle = _downloadHandler.assetBundle;
if (assetBundle == null)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] URL: {URL} - AssetBundle object is null";
}
else
{
Result = assetBundle;
}
}
/// <summary>
/// 创建 AssetBundle 下载处理器
/// </summary>
private DownloadHandlerAssetBundle CreateAssetBundleDownloadHandler()
{
DownloadHandlerAssetBundle handler;
if (_args.DisableUnityWebCache)
{
// 禁用 Unity 缓存
handler = new DownloadHandlerAssetBundle(URL, _args.UnityCRC);
}
else
{
if (string.IsNullOrEmpty(_args.FileHash))
throw new YooInternalException("File hash is null or empty !");
// 使用 Unity 缓存
// 说明The file hash defining the version of the asset bundle.
Hash128 fileHash = Hash128.Parse(_args.FileHash);
handler = new DownloadHandlerAssetBundle(URL, fileHash, _args.UnityCRC);
}
return handler;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e01cc308d7179a34281087fafe455b42
guid: 6f635344c08e2b04295a108c2bcc6a40
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,52 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 字节下载器
/// </summary>
/// <remarks>
/// 将下载内容保存到内存中的字节数组。
/// </remarks>
internal sealed class UnityWebRequestBytesDownloader : UnityWebRequestDownloaderBase, IDownloadBytesRequest
{
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 下载结果(字节数组)
/// </summary>
public byte[] Result { get; private set; }
/// <summary>
/// 构造字节数组下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestBytesDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerBuffer();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
Result = _webRequest.downloadHandler.data;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 6602c4be2ef295546b7bbb328de8fb0c
guid: 38884a96e8c2df74082d4e059e2e73ed
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 下载器基类
/// </summary>
/// <remarks>
/// 封装 UnityWebRequest 的通用下载逻辑,包括状态管理、进度追踪等。
/// 子类只需实现 CreateWebRequest 方法来创建特定类型的下载请求。
/// </remarks>
internal abstract class UnityWebRequestDownloaderBase : IDownloadRequest
{
private readonly UnityWebRequestCreator _webRequestCreator;
protected UnityWebRequest _webRequest;
// 看门狗相关
private int _watchdogTime = 0;
private bool _watchdogAborted = false;
private long _lastDownloadBytes = -1;
private double _lastGetDataTime;
#region
/// <summary>
/// 请求地址
/// </summary>
public string URL { get; }
/// <summary>
/// 是否完成
/// </summary>
/// <remarks>
/// 每次调用都会主动轮询请求 PollingRequest
/// </remarks>
public bool IsDone
{
get
{
PollingRequest();
return Status == EDownloadRequestStatus.Succeed
|| Status == EDownloadRequestStatus.Failed
|| Status == EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 请求状态
/// </summary>
public EDownloadRequestStatus Status { get; protected set; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { get; private set; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
public long DownloadedBytes { get; private set; }
/// <summary>
/// HTTP 返回码
/// </summary>
public long HttpCode { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; protected set; }
#endregion
/// <summary>
/// 构造下载器基类
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
protected UnityWebRequestDownloaderBase(string url, UnityWebRequestCreator webRequestCreator)
{
URL = url;
_webRequestCreator = webRequestCreator;
Status = EDownloadRequestStatus.None;
}
/// <summary>
/// 发起请求
/// </summary>
public void SendRequest()
{
if (Status == EDownloadRequestStatus.None)
{
Status = EDownloadRequestStatus.Running;
try
{
CreateWebRequest();
if (_webRequest == null)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] Created web request is null.";
}
else
{
_webRequest.SendWebRequest();
}
}
catch (Exception ex)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] Failed to create web request : {ex.Message}";
}
}
}
/// <summary>
/// 轮询请求
/// </summary>
public void PollingRequest()
{
if (Status != EDownloadRequestStatus.Running)
return;
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
CheckWatchdog();
if (_webRequest.isDone == false)
return;
HttpCode = _webRequest.responseCode;
#if UNITY_2020_3_OR_NEWER
bool isSuccess = _webRequest.result == UnityWebRequest.Result.Success;
#else
bool isSuccess = !_webRequest.isNetworkError && !_webRequest.isHttpError;
#endif
if (isSuccess)
{
Status = EDownloadRequestStatus.Succeed;
OnRequestSucceed();
}
else
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] URL: {URL} - 错误: {_webRequest.error}";
OnRequestFailed();
}
// 完成后释放
DisposeWebRequest();
}
/// <summary>
/// 中止请求
/// </summary>
public void AbortRequest()
{
if (Status == EDownloadRequestStatus.None || Status == EDownloadRequestStatus.Running)
{
Status = EDownloadRequestStatus.Aborted;
if (_webRequest != null)
_webRequest.Abort();
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
DisposeWebRequest();
}
/// <summary>
/// 创建 UnityWebRequest子类实现
/// </summary>
protected abstract void CreateWebRequest();
/// <summary>
/// 请求成功时的回调(子类可重写)
/// </summary>
protected virtual void OnRequestSucceed()
{
}
/// <summary>
/// 请求失败时的回调(子类可重写)
/// </summary>
protected virtual void OnRequestFailed()
{
}
/// <summary>
/// 创建 UnityWebRequest GET 请求
/// </summary>
/// <param name="requestUrl">请求地址</param>
/// <returns>UnityWebRequest 实例</returns>
protected UnityWebRequest CreateUnityWebRequestGet(string requestUrl)
{
if (_webRequestCreator != null)
return _webRequestCreator.Invoke(requestUrl, UnityWebRequest.kHttpVerbGET);
return new UnityWebRequest(requestUrl, UnityWebRequest.kHttpVerbGET);
}
/// <summary>
/// 创建 UnityWebRequest HEAD 请求
/// </summary>
/// <param name="requestUrl">请求地址</param>
/// <returns>UnityWebRequest 实例</returns>
protected UnityWebRequest CreateUnityWebRequestHead(string requestUrl)
{
if (_webRequestCreator != null)
return _webRequestCreator.Invoke(requestUrl, UnityWebRequest.kHttpVerbHEAD);
return new UnityWebRequest(requestUrl, UnityWebRequest.kHttpVerbHEAD);
}
/// <summary>
/// 应用通用请求参数
/// </summary>
protected void ApplyRequestOptions(int timeout, int watchdogTime, Dictionary<string, string> headers)
{
if (_webRequest == null)
throw new YooInternalException("Web request is null !");
// 设置看门狗超时时间
_watchdogTime = watchdogTime;
// 设置响应的超时时间
if (timeout > 0)
_webRequest.timeout = timeout;
// 设置响应头
if (headers != null)
{
foreach (var header in headers)
{
_webRequest.SetRequestHeader(header.Key, header.Value);
}
}
}
/// <summary>
/// 检测看门狗
/// </summary>
private void CheckWatchdog()
{
if (_watchdogTime == 0)
return;
if (_watchdogAborted)
return;
double realtimeSinceStartup = TimeUtility.RealtimeSinceStartup;
if (DownloadedBytes != _lastDownloadBytes)
{
_lastDownloadBytes = DownloadedBytes;
_lastGetDataTime = realtimeSinceStartup;
}
else
{
double deltaTime = realtimeSinceStartup - _lastGetDataTime;
if (deltaTime > _watchdogTime)
{
_watchdogAborted = true;
AbortRequest(); //看门狗终止网络请求
}
}
}
/// <summary>
/// 释放资源
/// </summary>
private void DisposeWebRequest()
{
if (_webRequest != null)
{
//注意引擎底层会自动调用Abort方法
_webRequest.Dispose();
_webRequest = null;
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9e71e850eded0da43906cb4f7cb75629
guid: 76b0712524ed69542914db8e44fa64fd
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,56 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 文件下载器
/// </summary>
/// <remarks>
/// 将下载内容保存到本地文件,支持断点续传和追加写入。
/// </remarks>
internal sealed class UnityWebRequestFileDownloader : UnityWebRequestDownloaderBase, IDownloadFileRequest
{
private readonly DownloadFileRequestArgs _args;
/// <summary>
/// 文件保存路径
/// </summary>
public string SavePath
{
get { return _args.SavePath; }
}
/// <summary>
/// 构造文件下载器
/// </summary>
/// <param name="args">文件下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestFileDownloader(DownloadFileRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerFile(_args.SavePath, _args.AppendToFile);
handler.removeFileOnAbort = _args.RemoveFileOnAbort;
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
// 断点续传:设置 Range 请求头
if (_args.ResumeFromBytes > 0)
{
_webRequest.SetRequestHeader("Range", $"bytes={_args.ResumeFromBytes}-");
}
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 375d88bcf5b9a6146adaf98ceb5369f8
guid: 101a4be1bb0a85c4d84ecbf3e74f89e4
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest HEAD 请求下载器
/// </summary>
/// <remarks>
/// 仅获取响应头信息,不下载实际内容。
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// </remarks>
internal sealed class UnityWebRequestHeadDownloader : UnityWebRequestDownloaderBase, IDownloadHeadRequest
{
// 注意:缓存响应头(因为 WebRequest 释放后无法获取)
private Dictionary<string, string> _cachedResponseHeaders;
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 获取 ETag 响应头
/// </summary>
public string ETag
{
get { return GetResponseHeader("ETag"); }
}
/// <summary>
/// 获取 Last-Modified 响应头
/// </summary>
public string LastModified
{
get { return GetResponseHeader("Last-Modified"); }
}
/// <summary>
/// 获取 Content-Type 响应头
/// </summary>
public string ContentType
{
get { return GetResponseHeader("Content-Type"); }
}
/// <summary>
/// 获取 Content-Length 响应头
/// 预期下载的总字节数
/// </summary>
public long ContentLength
{
get
{
string contentLengthStr = GetResponseHeader("Content-Length");
if (string.IsNullOrEmpty(contentLengthStr))
return -1;
if (long.TryParse(contentLengthStr, out long contentLength))
{
return contentLength;
}
else
{
return -1;
}
}
}
/// <summary>
/// 构造 HEAD 请求下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestHeadDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="name">响应头名称(不区分大小写)</param>
/// <returns>响应头的值,如果不存在或请求未完成则返回 null</returns>
public string GetResponseHeader(string name)
{
if (_cachedResponseHeaders == null)
return null;
// 注意UnityWebRequest 的响应头 key 是小写的
string lowerName = name.ToLowerInvariant();
if (_cachedResponseHeaders.TryGetValue(lowerName, out string value))
return value;
return null;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
_webRequest = CreateUnityWebRequestHead(URL);
_webRequest.downloadHandler = null; // HEAD 请求不需要 DownloadHandler
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
var headers = _webRequest.GetResponseHeaders();
if (headers != null)
{
_cachedResponseHeaders = new Dictionary<string, string>(headers.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in headers)
{
string name = kvp.Key.ToLowerInvariant();
string value = kvp.Value;
_cachedResponseHeaders[name] = value;
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,52 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 文本下载器
/// </summary>
/// <remarks>
/// 将下载内容解析为 UTF-8 文本字符串。
/// </remarks>
internal sealed class UnityWebRequestTextDownloader : UnityWebRequestDownloaderBase, IDownloadTextRequest
{
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 下载结果(文本字符串)
/// </summary>
public string Result { get; private set; }
/// <summary>
/// 构造文本下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestTextDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerBuffer();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
Result = _webRequest.downloadHandler.text;
}
}
}

View File

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

View File

@@ -0,0 +1,141 @@
using System;
namespace YooAsset
{
/// <summary>
/// 模拟下载器
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// 根据配置的下载速度模拟进度变化。
/// </remarks>
internal sealed class VirtualFileDownloader : IDownloadFileRequest
{
private readonly DownloadSimulateRequestArgs _args;
private double _lastUpdateTime;
/// <summary>
/// 文件保存路径(模拟下载不需要)
/// </summary>
public string SavePath
{
get { return null; }
}
#region
/// <summary>
/// 请求地址
/// </summary>
public string URL { get; }
/// <summary>
/// 是否完成
/// </summary>
public bool IsDone
{
get
{
PollingRequest();
return Status == EDownloadRequestStatus.Succeed
|| Status == EDownloadRequestStatus.Failed
|| Status == EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 请求状态
/// </summary>
public EDownloadRequestStatus Status { get; private set; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { get; private set; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
public long DownloadedBytes { get; private set; }
/// <summary>
/// HTTP 返回码(模拟固定返回 200
/// </summary>
public long HttpCode { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; private set; }
#endregion
/// <summary>
/// 构造模拟下载器
/// </summary>
/// <param name="args">模拟下载参数</param>
public VirtualFileDownloader(DownloadSimulateRequestArgs args)
{
_args = args;
URL = args.URL;
Status = EDownloadRequestStatus.None;
}
/// <summary>
/// 发起请求
/// </summary>
public void SendRequest()
{
if (Status == EDownloadRequestStatus.None)
{
Status = EDownloadRequestStatus.Running;
_lastUpdateTime = TimeUtility.RealtimeSinceStartup;
}
}
/// <summary>
/// 轮询请求
/// </summary>
public void PollingRequest()
{
if (Status != EDownloadRequestStatus.Running)
return;
double currentTime = TimeUtility.RealtimeSinceStartup;
double deltaTime = currentTime - _lastUpdateTime;
_lastUpdateTime = currentTime;
// 计算本帧下载的字节数
long downloadBytes = (long)(_args.DownloadSpeed * deltaTime);
DownloadedBytes += downloadBytes;
if (_args.FileSize > 0)
DownloadProgress = (float)DownloadedBytes / _args.FileSize;
// 检查是否完成
if (DownloadedBytes >= _args.FileSize)
{
HttpCode = 200;
DownloadProgress = 1f;
DownloadedBytes = _args.FileSize;
Status = EDownloadRequestStatus.Succeed;
}
}
/// <summary>
/// 中止请求
/// </summary>
public void AbortRequest()
{
if (Status == EDownloadRequestStatus.None || Status == EDownloadRequestStatus.Running)
{
Status = EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
}
}
}

View File

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

View File

@@ -1,118 +0,0 @@

namespace YooAsset
{
/// <summary>
/// 下载器结束
/// </summary>
public struct DownloaderFinishData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 是否成功
/// </summary>
public bool Succeed;
}
/// <summary>
/// 下载器相关的更新数据
/// </summary>
public struct DownloadUpdateData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载进度 (0-1f)
/// </summary>
public float Progress;
/// <summary>
/// 下载文件总数
/// </summary>
public int TotalDownloadCount;
/// <summary>
/// 当前完成的下载文件数量
/// </summary>
public int CurrentDownloadCount;
/// <summary>
/// 下载数据总大小(单位:字节)
/// </summary>
public long TotalDownloadBytes;
/// <summary>
/// 当前完成的下载数据大小(单位:字节)
/// </summary>
public long CurrentDownloadBytes;
}
/// <summary>
/// 下载器相关的错误数据
/// </summary>
public struct DownloadErrorData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载失败的文件名称
/// </summary>
public string FileName;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorInfo;
}
/// <summary>
/// 下载器相关的文件数据
/// </summary>
public struct DownloadFileData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载的文件名称
/// </summary>
public string FileName;
/// <summary>
/// 下载的文件大小
/// </summary>
public long FileSize;
}
/// <summary>
/// 导入文件的信息
/// </summary>
public struct ImportFileInfo
{
/// <summary>
/// 本地文件路径
/// </summary>
public string FilePath;
/// <summary>
/// 资源包名称
/// </summary>
public string BundleName;
/// <summary>
/// 资源包GUID
/// </summary>
public string BundleGUID;
}
}

View File

@@ -0,0 +1,349 @@
using System.Collections.Generic;
namespace YooAsset
{
/// <summary>
/// 导入的资源包信息
/// </summary>
public struct ImportBundleInfo
{
/// <summary>
/// 本地文件路径
/// </summary>
public string FilePath;
/// <summary>
/// 资源包名称
/// </summary>
public string BundleName;
/// <summary>
/// 资源包GUID
/// </summary>
public string BundleGUID;
}
/// <summary>
/// 下载请求状态
/// </summary>
internal enum EDownloadRequestStatus
{
/// <summary>
/// 未开始
/// </summary>
None,
/// <summary>
/// 进行中
/// </summary>
Running,
/// <summary>
/// 已成功
/// </summary>
Succeed,
/// <summary>
/// 已失败
/// </summary>
Failed,
/// <summary>
/// 已中止
/// </summary>
Aborted,
}
/// <summary>
/// 文件下载请求参数
/// </summary>
/// <remarks>
/// 用于将下载内容保存到本地文件的请求配置。
/// 支持断点续传和追加写入模式。
/// </remarks>
internal struct DownloadFileRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 文件保存路径
/// </summary>
public readonly string SavePath;
/// <summary>
/// 是否追加写入文件
/// </summary>
/// <remarks>
/// 配合 ResumeFromBytes 使用,用于断点续传场景。
/// </remarks>
public readonly bool AppendToFile;
/// <summary>
/// 中止请求时是否删除目标文件
/// </summary>
public readonly bool RemoveFileOnAbort;
/// <summary>
/// 断点续传的起始字节(小于等于 0 表示不启用)
/// </summary>
/// <remarks>
/// 推荐由后端自动设置 Range 请求头:"bytes={ResumeFromBytes}-"。
/// </remarks>
public readonly long ResumeFromBytes;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造文件下载请求参数
/// </summary>
public DownloadFileRequestArgs(
string url,
string savePath,
int timeout,
int watchdogTime,
bool appendToFile = false,
bool removeFileOnAbort = true,
long resumeFromBytes = 0)
{
URL = url;
SavePath = savePath;
Timeout = timeout;
WatchdogTime = watchdogTime;
AppendToFile = appendToFile;
RemoveFileOnAbort = removeFileOnAbort;
ResumeFromBytes = resumeFromBytes;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// 数据下载请求参数(通用)
/// </summary>
/// <remarks>
/// 用于下载到内存的请求配置。
/// 可用于字节数组bytes或文本text下载。
/// </remarks>
internal struct DownloadDataRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造数据下载请求参数
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="options">通用请求参数</param>
public DownloadDataRequestArgs(string url, int timeout, int watchdogTime)
{
URL = url;
Timeout = timeout;
WatchdogTime = watchdogTime;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// AssetBundle 下载请求参数
/// </summary>
/// <remarks>
/// 用于下载并加载 Unity AssetBundle 的请求配置。
/// 支持 Unity 内置缓存机制和 CRC 校验。
/// </remarks>
internal struct DownloadAssetBundleRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 禁用 Unity 的网络缓存
/// </summary>
public readonly bool DisableUnityWebCache;
/// <summary>
/// AssetBundle 文件哈希(用于 UnityWebRequest 的缓存)
/// </summary>
/// <remarks>
/// 仅当 DisableUnityWebCache 为 false 时需要。
/// </remarks>
public readonly string FileHash;
/// <summary>
/// Unity CRC 校验值
/// </summary>
public readonly uint UnityCRC;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造 AssetBundle 下载请求参数
/// </summary>
public DownloadAssetBundleRequestArgs(
string url,
int timeout,
int watchdogTime,
bool disableUnityWebCache = true,
string fileHash = null,
uint unityCrc = 0)
{
URL = url;
Timeout = timeout;
WatchdogTime = watchdogTime;
DisableUnityWebCache = disableUnityWebCache;
FileHash = fileHash;
UnityCRC = unityCrc;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// 模拟下载请求参数
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// </remarks>
internal struct DownloadSimulateRequestArgs
{
/// <summary>
/// 请求地址(仅用于标识)
/// </summary>
public readonly string URL;
/// <summary>
/// 模拟的文件大小(字节)
/// </summary>
public readonly long FileSize;
/// <summary>
/// 模拟的下载速度(字节/秒)
/// </summary>
/// <remarks>
/// 用于计算模拟的下载进度。
/// </remarks>
public readonly long DownloadSpeed;
/// <summary>
/// 构造模拟下载请求参数
/// </summary>
/// <param name="url">请求地址(仅用于标识)</param>
/// <param name="fileSize">模拟的文件大小(字节)</param>
/// <param name="downloadSpeed">模拟的下载速度(字节/秒),默认 1MB/s</param>
public DownloadSimulateRequestArgs(string url, long fileSize, long downloadSpeed = 1024 * 1024)
{
URL = url;
FileSize = fileSize;
DownloadSpeed = downloadSpeed > 0 ? downloadSpeed : 1024 * 1024;
}
}
}

View File

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

View File

@@ -3,32 +3,8 @@ using UnityEngine;
namespace YooAsset
{
/// <summary>
/// 自定义下载器的请求委托
/// </summary>
public delegate UnityWebRequest UnityWebRequestDelegate(string url);
internal class DownloadSystemHelper
{
#if UNITY_EDITOR
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void OnRuntimeInitialize()
{
UnityWebRequestCreater = null;
}
#endif
public static UnityWebRequestDelegate UnityWebRequestCreater = null;
public static UnityWebRequest NewUnityWebRequestGet(string requestURL)
{
UnityWebRequest webRequest;
if (UnityWebRequestCreater != null)
webRequest = UnityWebRequestCreater.Invoke(requestURL);
else
webRequest = new UnityWebRequest(requestURL, UnityWebRequest.kHttpVerbGET);
return webRequest;
}
/// <summary>
/// 获取WWW加载本地资源的路径
/// </summary>
@@ -81,7 +57,7 @@ namespace YooAsset
#elif UNITY_STANDALONE_LINUX
url = StringUtility.Format("file:///root/{0}", path);
#else
throw new System.NotImplementedException();
throw new System.NotSupportedException($"[{nameof(DownloadSystemHelper.ConvertToWWWPath)}] not implemented platform: {UnityEngine.Application.platform}");
#endif
// For some special cases when users have special characters in their devices, url paths can not be identified correctly.
@@ -94,12 +70,16 @@ namespace YooAsset
public static bool IsRequestLocalFile(string url)
{
//TODO UNITY_STANDALONE_OSX平台目前无法确定
// 本地文件传输协议
if (url.StartsWith("file:"))
return true;
// JAR文件协议
if (url.StartsWith("jar:file:"))
return true;
return false;
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e7f5776546411834d9ed949d54a6f241
guid: d724672f4d6f24b4db435d10eef6c40d
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,78 @@
using System;
namespace YooAsset
{
/// <summary>
/// 下载后台接口
/// </summary>
/// <remarks>
/// 不同网络库UnityWebRequest / BestHTTP / 自研)实现该接口,用于创建具体下载请求。
/// 每个后台实例是独立的,不共享全局状态。
/// </remarks>
internal interface IDownloadBackend : IDisposable
{
/// <summary>
/// 后台名称(用于日志与调试)
/// </summary>
string Name { get; }
/// <summary>
/// 驱动更新
/// </summary>
/// <remarks>
/// 部分第三方网络库需要在 Unity 主线程中周期性调用 Update 进行驱动。
/// 不需要驱动的后台可实现为空方法。
/// </remarks>
void Update();
/// <summary>
/// 创建 HEAD 请求
/// </summary>
/// <remarks>
/// 仅获取响应头信息,不下载实际内容。
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// </remarks>
/// <param name="args">数据请求参数</param>
/// <returns>HEAD 请求实例</returns>
IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建文件下载请求
/// </summary>
/// <param name="args">文件下载参数</param>
/// <returns>文件下载请求实例</returns>
IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args);
/// <summary>
/// 创建内存下载请求(字节数组)
/// </summary>
/// <param name="args">数据下载参数</param>
/// <returns>字节下载请求实例</returns>
IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建文本下载请求
/// </summary>
/// <param name="args">数据下载参数</param>
/// <returns>文本下载请求实例</returns>
IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建 AssetBundle 下载请求
/// </summary>
/// <param name="args">AssetBundle 下载参数</param>
/// <returns>AssetBundle 下载请求实例</returns>
IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args);
/// <summary>
/// 创建模拟下载请求
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// 可用于测试下载流程和 UI 展示。
/// </remarks>
/// <param name="args">模拟下载参数</param>
/// <returns>模拟下载请求实例</returns>
IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args);
}
}

View File

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

View File

@@ -0,0 +1,194 @@
using System;
namespace YooAsset
{
/// <summary>
/// 可轮询的下载请求接口
/// </summary>
/// <remarks>
/// 上层通常会在每帧轮询 Status IsDone并在完成后读取结果。
/// </remarks>
internal interface IDownloadRequest : IDisposable
{
/// <summary>
/// 请求地址
/// </summary>
string URL { get; }
/// <summary>
/// 是否完成(成功/失败/中止)
/// </summary>
bool IsDone { get; }
/// <summary>
/// 请求状态
/// </summary>
EDownloadRequestStatus Status { get; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
/// <remarks>
/// 部分情况下无法准确获取总长度,可返回 0。
/// </remarks>
float DownloadProgress { get; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
/// <remarks>
/// 断点续传场景下,该值仅表示"本次请求新增下载的字节数",不包含已存在的本地文件长度。
/// </remarks>
long DownloadedBytes { get; }
/// <summary>
/// HTTP 返回码
/// </summary>
/// <remarks>
/// 非 HTTP 协议可返回 0。使用 long 类型以兼容各种协议的返回码。
/// </remarks>
long HttpCode { get; }
/// <summary>
/// 错误信息
/// </summary>
/// <remarks>
/// 失败时不为空。
/// </remarks>
string Error { get; }
/// <summary>
/// 发起请求
/// </summary>
void SendRequest();
/// <summary>
/// 轮询请求
/// </summary>
void PollingRequest();
/// <summary>
/// 中止请求
/// </summary>
void AbortRequest();
}
/// <summary>
/// HEAD 请求接口(仅获取响应头)
/// </summary>
/// <remarks>
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// 不下载实际内容,仅获取响应头信息。
/// </remarks>
internal interface IDownloadHeadRequest : IDownloadRequest
{
/// <summary>
/// 获取 ETag 响应头
/// </summary>
/// <remarks>
/// 用于缓存验证,如果服务器未返回则为 null。
/// </remarks>
string ETag { get; }
/// <summary>
/// 获取 Last-Modified 响应头
/// </summary>
/// <remarks>
/// 资源最后修改时间,如果服务器未返回则为 null。
/// </remarks>
string LastModified { get; }
/// <summary>
/// 获取 Content-Type 响应头
/// </summary>
/// <remarks>
/// 资源的 MIME 类型,如果服务器未返回则为 null。
/// </remarks>
string ContentType { get; }
/// <summary>
/// 预期下载的总字节数Content-Length
/// </summary>
/// <remarks>
/// 从响应头 Content-Length 获取。
/// 如果服务器未返回或请求未完成,返回 -1。
/// 用于更准确的进度计算。
/// </remarks>
long ContentLength { get; }
/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="name">响应头名称(不区分大小写)</param>
/// <returns>响应头的值,如果不存在或请求未完成则返回 null</returns>
/// <remarks>
/// 常用响应头Content-Length、Content-Type、ETag、Last-Modified 等。
/// </remarks>
string GetResponseHeader(string name);
}
/// <summary>
/// 文件下载请求接口
/// </summary>
/// <remarks>
/// 将下载内容保存到指定的本地文件路径。
/// </remarks>
internal interface IDownloadFileRequest : IDownloadRequest
{
/// <summary>
/// 文件保存路径
/// </summary>
string SavePath { get; }
}
/// <summary>
/// 内存下载请求接口(字节数组)
/// </summary>
/// <remarks>
/// 将下载内容保存到内存中的字节数组。
/// </remarks>
internal interface IDownloadBytesRequest : IDownloadRequest
{
/// <summary>
/// 下载结果(字节数组)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
byte[] Result { get; }
}
/// <summary>
/// 文本下载请求接口
/// </summary>
/// <remarks>
/// 将下载内容解析为 UTF-8 文本字符串。
/// </remarks>
internal interface IDownloadTextRequest : IDownloadRequest
{
/// <summary>
/// 下载结果(文本字符串)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
string Result { get; }
}
/// <summary>
/// AssetBundle 下载请求接口
/// </summary>
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包。
/// </remarks>
internal interface IDownloadAssetBundleRequest : IDownloadRequest
{
/// <summary>
/// 下载结果AssetBundle 对象)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
UnityEngine.AssetBundle Result { get; }
}
}

View File

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

View File

@@ -1,113 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityAssetBundleRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private DownloadHandlerAssetBundle _downloadhandler;
private readonly PackageBundle _packageBundle;
private readonly bool _disableUnityWebCache;
private ESteps _steps = ESteps.None;
/// <summary>
/// 请求结果
/// </summary>
public AssetBundle Result { private set; get; }
internal UnityAssetBundleRequestOperation(PackageBundle packageBundle, bool disableUnityWebCache, string url) : base(url)
{
_packageBundle = packageBundle;
_disableUnityWebCache = disableUnityWebCache;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
AssetBundle assetBundle = _downloadhandler.assetBundle;
if (assetBundle == null)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler asset bundle object is null !";
}
else
{
_steps = ESteps.Done;
Result = assetBundle;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
_downloadhandler = CreateWebDownloadHandler();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.downloadHandler = _downloadhandler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
private DownloadHandlerAssetBundle CreateWebDownloadHandler()
{
if (_disableUnityWebCache)
{
var downloadhandler = new DownloadHandlerAssetBundle(_requestURL, _packageBundle.UnityCRC);
#if UNITY_2020_3_OR_NEWER
downloadhandler.autoLoadAssetBundle = false;
#endif
return downloadhandler;
}
else
{
// 注意:优先从浏览器缓存里获取文件
// The file hash defining the version of the asset bundle.
Hash128 fileHash = Hash128.Parse(_packageBundle.FileHash);
var downloadhandler = new DownloadHandlerAssetBundle(_requestURL, fileHash, _packageBundle.UnityCRC);
#if UNITY_2020_3_OR_NEWER
downloadhandler.autoLoadAssetBundle = false;
#endif
return downloadhandler;
}
}
}
}

View File

@@ -1,65 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityVirtualBundleRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
Download,
Done,
}
private readonly PackageBundle _bundle;
private readonly int _downloadSpeed;
private ESteps _steps = ESteps.None;
internal UnityVirtualBundleRequestOperation(PackageBundle packageBundle, int downloadSpeed, string url) : base(url)
{
_bundle = packageBundle;
_downloadSpeed = downloadSpeed;
}
internal override void InternalStart()
{
_steps = ESteps.Download;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.Download)
{
// 模拟下载进度
float progress = 0;
if (DownloadedBytes > 0)
progress = DownloadedBytes / _bundle.FileSize;
long downloadBytes = (long)((double)_downloadSpeed * Time.deltaTime);
Progress = progress;
DownloadProgress = progress;
DownloadedBytes += downloadBytes;
if (DownloadedBytes < _bundle.FileSize)
return;
Progress = 1f;
DownloadProgress = 1f;
DownloadedBytes = _bundle.FileSize;
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
}
internal override void InternalWaitForAsyncComplete()
{
if (_steps != ESteps.Done)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Try load bundle {_bundle.BundleName} from remote !";
}
}
}
}

View File

@@ -1,89 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebCacheRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
private ESteps _steps = ESteps.None;
internal UnityWebCacheRequestOperation(string url) : base(url)
{
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
/// <summary>
/// 设置请求头信息
/// </summary>
public void SetRequestHeader(string name, string value)
{
_headers.Add(name, value);
}
private void CreateWebRequest()
{
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.disposeDownloadHandlerOnDispose = true;
// 设置消息头
foreach (var keyValuePair in _headers)
{
string name = keyValuePair.Key;
string value = keyValuePair.Value;
_webRequest.SetRequestHeader(name, value);
}
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

@@ -1,96 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebDataRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
/// <summary>
/// 请求结果
/// </summary>
public byte[] Result { private set; get; }
internal UnityWebDataRequestOperation(string url, int timeout) : base(url)
{
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
var fileData = _webRequest.downloadHandler.data;
if (fileData == null || fileData.Length == 0)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler data is null or empty !";
}
else
{
_steps = ESteps.Done;
Result = fileData;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerBuffer handler = new DownloadHandlerBuffer();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

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

View File

@@ -1,83 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebFileRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private readonly string _fileSavePath;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
internal UnityWebFileRequestOperation(string url, string fileSavePath, int timeout) : base(url)
{
_fileSavePath = fileSavePath;
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerFile handler = new DownloadHandlerFile(_fileSavePath);
handler.removeFileOnAbort = true;
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

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

View File

@@ -1,98 +0,0 @@
using System;
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal abstract class UnityWebRequestOperation : AsyncOperationBase
{
protected UnityWebRequest _webRequest;
protected readonly string _requestURL;
private bool _isAbort = false;
/// <summary>
/// HTTP返回码
/// </summary>
public long HttpCode { private set; get; }
/// <summary>
/// 当前下载的字节数
/// </summary>
public long DownloadedBytes { protected set; get; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { protected set; get; }
/// <summary>
/// 请求的URL地址
/// </summary>
public string URL
{
get { return _requestURL; }
}
internal UnityWebRequestOperation(string url)
{
_requestURL = url;
}
internal override void InternalAbort()
{
//TODO
// 1. 编辑器下停止运行游戏的时候主动终止下载任务
// 2. 真机上销毁包裹的时候主动终止下载任务
if (_isAbort == false)
{
if (_webRequest != null)
{
_webRequest.Abort();
_isAbort = true;
}
}
}
/// <summary>
/// 释放下载器
/// </summary>
protected void DisposeRequest()
{
if (_webRequest != null)
{
//注意引擎底层会自动调用Abort方法
_webRequest.Dispose();
_webRequest = null;
}
}
/// <summary>
/// 检测请求结果
/// </summary>
protected bool CheckRequestResult()
{
HttpCode = _webRequest.responseCode;
#if UNITY_2020_3_OR_NEWER
if (_webRequest.result != UnityWebRequest.Result.Success)
{
Error = $"URL : {_requestURL} Error : {_webRequest.error}";
return false;
}
else
{
return true;
}
#else
if (_webRequest.isNetworkError || _webRequest.isHttpError)
{
Error = $"URL : {_requestURL} Error : {_webRequest.error}";
return false;
}
else
{
return true;
}
#endif
}
}
}

View File

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

View File

@@ -1,96 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebTextRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
/// <summary>
/// 请求结果
/// </summary>
public string Result { private set; get; }
internal UnityWebTextRequestOperation(string url, int timeout) : base(url)
{
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
var fileText = _webRequest.downloadHandler.text;
if (string.IsNullOrEmpty(fileText))
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler text is null or empty !";
}
else
{
_steps = ESteps.Done;
Result = fileText;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerBuffer handler = new DownloadHandlerBuffer();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

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

View File

@@ -0,0 +1,557 @@
# DownloadSystem 下载模块
## 模块概述
DownloadSystem 是 YooAsset 资源管理系统的**底层网络下载层**,负责处理所有 HTTP 网络请求。该模块提供了统一的下载接口抽象,支持文件下载、断点续传、并发请求(由上层调度)、看门狗监控等功能。
### 核心职责
- HTTP/HTTPS 文件下载
- 断点续传支持
- 看门狗超时保护
- 多种下载类型(文件/字节/文本/AssetBundle
- 可插拔的网络库后端
---
## 边界与上层协作
DownloadSystem 的职责是提供“可替换后端 + 统一请求接口 + 轮询式生命周期”的基础能力:
- **本模块不负责并发队列/限流调度**:并发通常由上层同时创建多个 request 并自行控制并发数。
- **本模块不负责重试/回退策略**:失败后的重试、切换 CDN、降级等策略通常由上层系统实现。
- **本模块不负责持久化下载任务**:断点续传依赖本地已有文件与 `Range` 请求头,并由上层管理断点信息。
---
## 设计目标
| 目标 | 说明 |
|------|------|
| **可扩展性** | 支持可插拔的网络库后端UnityWebRequest/BestHTTP/自研) |
| **鲁棒性** | 看门狗超时保护、自动清理失败文件、完整错误信息 |
| **高性能** | 轮询模式无阻塞、支持并发请求(并发数由上层调度) |
| **易用性** | 流畅的参数构建 API、清晰的状态转换 |
---
## 架构概念
### 分层架构
```
┌─────────────────────────────────────────────────────────┐
│ 上层调用者 │
│ (FileSystem / ResourceManager) │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ IDownloadBackend │
│ (后端接口) │
│ 定义网络库合约,工厂模式创建请求 │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ IDownloadRequest │
│ (请求接口) │
│ 轮询式生命周期管理,状态机驱动 │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ UnityWebRequest / 其他网络库 │
│ (底层实现) │
└─────────────────────────────────────────────────────────┘
```
### 核心组件
- **后端层 (IDownloadBackend)**: 定义网络库实现合约,通过工厂方法创建各类请求
- **请求层 (IDownloadRequest)**: 统一的请求生命周期管理,支持轮询驱动
- **参数层 (Args 结构体)**: 配置下载行为(超时、断点续传、看门狗等)
---
## 文件结构
```
DownloadSystem/
├── Interface/ # 接口定义
│ ├── IDownloadBackend.cs # 后端接口(工厂模式)
│ └── IDownloadRequest.cs # 请求接口层次结构
├── DefaultDownloadBackend/ # 默认后端实现
│ ├── UnityWebRequestBackend.cs # UnityWebRequest 后端
│ └── UnityWebRequestCreator.cs # UnityWebRequest 创建委托
├── DefaultDownloadRequest/ # 默认请求实现
│ ├── UnityWebRequestDownloaderBase.cs # 基础下载器(抽象类)
│ ├── UnityWebRequestFileDownloader.cs # 文件下载器
│ ├── UnityWebRequestHeadDownloader.cs # HEAD 请求器
│ ├── UnityWebRequestBytesDownloader.cs # 字节下载器
│ ├── UnityWebRequestTextDownloader.cs # 文本下载器
│ ├── UnityWebRequestAssetBundleDownloader.cs # AssetBundle 下载器
│ └── VirtualFileDownloader.cs # 模拟下载器(编辑器用)
├── DownloadSystemDefine.cs # 枚举、结构体定义
├── DownloadSystemHelper.cs # 工具函数
└── WebRequestCounter.cs # 请求失败计数器
```
---
## 接口说明
### IDownloadBackend后端接口
定义网络库实现的合约,通过工厂方法创建各类下载请求。
```csharp
public interface IDownloadBackend
{
/// <summary>
/// 后端标识名称(用于日志/调试)
/// </summary>
string Name { get; }
/// <summary>
/// 定期驱动更新(部分第三方库需要)
/// </summary>
void Update();
// 工厂方法 - 创建各类请求
IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args);
IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args);
IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args);
IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args);
IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args);
IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args);
}
```
### IDownloadRequest基础请求接口
所有下载请求的通用接口,定义生命周期和状态管理。
```csharp
public interface IDownloadRequest : IDisposable
{
// 元信息
string URL { get; }
// 生命周期
bool IsDone { get; } // 每次访问自动轮询
EDownloadRequestStatus Status { get; }
// 进度跟踪
float DownloadProgress { get; } // 0f - 1f
long DownloadedBytes { get; } // 本次请求新增字节数
// 诊断信息
long HttpCode { get; }
string Error { get; }
// 生命周期方法
void SendRequest(); // 发起请求
void PollingRequest(); // 轮询状态
void AbortRequest(); // 中止请求
}
```
### 专化请求接口
| 接口 | 用途 | 特有属性 |
|------|------|----------|
| `IDownloadHeadRequest` | HEAD 请求,获取响应头 | `ETag`, `LastModified`, `ContentLength`, `ContentType` |
| `IDownloadFileRequest` | 文件下载到本地 | `SavePath` |
| `IDownloadBytesRequest` | 下载到内存(字节数组) | `byte[] Result` |
| `IDownloadTextRequest` | 下载文本内容 | `string Result` |
| `IDownloadAssetBundleRequest` | 下载并加载 AssetBundle | `AssetBundle Result` |
---
## 结构体定义
### 请求状态枚举
```csharp
public enum EDownloadRequestStatus
{
None, // 未开始
Running, // 进行中
Succeed, // 已成功
Failed, // 已失败
Aborted // 已中止(用户中止或看门狗超时)
}
```
### 请求参数结构体
#### DownloadFileRequestArgs文件下载参数
```csharp
public struct DownloadFileRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时0=无限制
public int WatchdogTime; // 看门狗超时(秒)
public string SavePath; // 文件保存路径
public bool AppendToFile; // 追加写入(断点续传)
public bool RemoveFileOnAbort; // 中止时删除文件
public long ResumeFromBytes; // 断点续传起始位置
public Dictionary<string, string> Headers; // 自定义请求头
}
```
#### DownloadDataRequestArgs数据下载参数
```csharp
public struct DownloadDataRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时(秒)
public int WatchdogTime; // 看门狗超时(秒)
public Dictionary<string, string> Headers; // 自定义请求头
}
```
#### DownloadAssetBundleRequestArgsAssetBundle 下载参数)
```csharp
public struct DownloadAssetBundleRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时
public int WatchdogTime; // 看门狗超时
public bool DisableUnityWebCache; // 禁用 Unity 缓存(推荐 true
public string FileHash; // 文件哈希(缓存启用时需要)
public uint UnityCRC; // Unity CRC 校验值
public Dictionary<string, string> Headers;
}
```
#### DownloadSimulateRequestArgs模拟下载参数
```csharp
public struct DownloadSimulateRequestArgs
{
public string URL; // 标识符
public long FileSize; // 模拟文件大小
public long DownloadSpeed; // 模拟速度(字节/秒),默认 1MB/s
}
```
---
## 核心类说明
### UnityWebRequestBackend
默认的后端实现,基于 Unity 的 UnityWebRequest API。
**特性:**
- 支持自定义 UnityWebRequest 创建方式(证书验证、代理等)
- 无需手动调用 Update()UnityWebRequest 自动驱动
```csharp
// 自定义 UnityWebRequest 创建(建议通过 backend 构造函数传入)
UnityWebRequestCreator creator = (url, method) =>
{
var request = new UnityWebRequest(url, method);
// 自定义配置...
return request;
};
IDownloadBackend backend = new UnityWebRequestBackend(creator);
```
### UnityWebRequestDownloaderBase
抽象基类,封装所有下载器的通用逻辑。
**职责:**
- 管理请求生命周期和状态转换
- 实现看门狗监控机制
- 追踪下载进度和字节数
- 处理超时和错误
**生命周期:**
```
None ──► SendRequest() ──► Running ──► PollingRequest() ──┬──► Succeed
├──► Failed
└──► Aborted
```
### 具体下载器
| 下载器 | 实现接口 | 使用场景 |
|--------|----------|----------|
| `UnityWebRequestFileDownloader` | `IDownloadFileRequest` | 大文件下载到本地 |
| `UnityWebRequestHeadDownloader` | `IDownloadHeadRequest` | 检查资源信息 |
| `UnityWebRequestBytesDownloader` | `IDownloadBytesRequest` | 小文件内存加载 |
| `UnityWebRequestTextDownloader` | `IDownloadTextRequest` | 文本文件下载 |
| `UnityWebRequestAssetBundleDownloader` | `IDownloadAssetBundleRequest` | AB 包下载加载 |
| `VirtualFileDownloader` | `IDownloadFileRequest` | 编辑器模拟下载 |
---
## 使用示例
### 基础文件下载
```csharp
// 1. 创建后端和请求
IDownloadBackend backend = new UnityWebRequestBackend();
var args = new DownloadFileRequestArgs(
url: "https://example.com/file.zip",
savePath: "/path/to/save/file.zip",
timeout: 30,
watchdogTime: 0);
IDownloadFileRequest request = backend.CreateFileRequest(args);
// 2. 发起并轮询
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
// 可选:显示进度
float progress = request.DownloadProgress;
}
// 3. 检查结果
if (request.Status == EDownloadRequestStatus.Succeed)
{
Debug.Log("下载成功");
}
else
{
Debug.LogError($"下载失败: {request.Error}");
}
// 4. 清理资源
request.Dispose();
```
### 断点续传
```csharp
// 获取已下载的文件大小
long existingFileSize = new FileInfo(savePath).Length;
var args = new DownloadFileRequestArgs(
url: url,
savePath: savePath,
timeout: 30,
watchdogTime: 0,
appendToFile: true, // 追加写入
removeFileOnAbort: false, // 中止时保留文件
resumeFromBytes: existingFileSize); // 断点位置
IDownloadFileRequest request = backend.CreateFileRequest(args);
request.SendRequest();
// ... 轮询等待完成
```
### 看门狗保护
```csharp
var args = new DownloadFileRequestArgs(
url: url,
savePath: path,
timeout: 30,
watchdogTime: 30); // 30秒无数据自动中止
IDownloadFileRequest request = backend.CreateFileRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
// 检查是否因看门狗超时而中止
if (request.Status == EDownloadRequestStatus.Aborted)
{
Debug.LogWarning("下载超时,已自动中止");
}
```
### HEAD 请求获取文件信息
```csharp
var args = new DownloadDataRequestArgs(
url: "https://example.com/file.zip",
timeout: 30,
watchdogTime: 0);
IDownloadHeadRequest request = backend.CreateHeadRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
if (request.Status == EDownloadRequestStatus.Succeed)
{
long fileSize = request.ContentLength;
string etag = request.ETag;
string lastModified = request.LastModified;
Debug.Log($"文件大小: {fileSize}, ETag: {etag}");
}
```
### 下载字节数据
```csharp
var args = new DownloadDataRequestArgs(
url: "https://example.com/data.json",
timeout: 30,
watchdogTime: 0);
IDownloadBytesRequest request = backend.CreateBytesRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
if (request.Status == EDownloadRequestStatus.Succeed)
{
byte[] data = request.Result;
// 处理数据...
}
```
---
## 设计模式
### 工厂模式
`IDownloadBackend` 作为工厂接口,创建各类下载请求对象:
```
IDownloadBackend
├── CreateHeadRequest() ──► IDownloadHeadRequest
├── CreateFileRequest() ──► IDownloadFileRequest
├── CreateBytesRequest() ──► IDownloadBytesRequest
├── CreateTextRequest() ──► IDownloadTextRequest
├── CreateAssetBundleRequest() ──► IDownloadAssetBundleRequest
└── CreateSimulateRequest() ──► IDownloadFileRequest
```
### 策略模式
通过实现 `IDownloadBackend` 接口,可以替换底层网络库:
```
IDownloadBackend (接口)
├── UnityWebRequestBackend (默认实现)
├── BestHTTPBackend (可扩展)
└── CustomBackend (自定义)
```
### 状态机模式
请求生命周期通过状态机管理:
```
┌──────┐ SendRequest() ┌─────────┐
│ None │ ──────────────────► │ Running │
└──────┘ └────┬────┘
│ PollingRequest()
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Succeed │ │ Failed │ │ Aborted │
└─────────┘ └──────────┘ └─────────┘
```
### 看门狗模式
监控数据接收,防止网络卡顿导致请求无限等待:
```
每帧轮询 PollingRequest()
├── 收到新数据 ──► 重置计时器
└── 未收到数据 ──► 计时器累加
└── 超过 WatchdogTime ──► AbortRequest()
```
---
## 类继承关系
```
IDownloadRequest (基础接口)
├── IDownloadHeadRequest (HEAD 请求)
├── IDownloadFileRequest (文件下载)
├── IDownloadBytesRequest (字节下载)
├── IDownloadTextRequest (文本下载)
└── IDownloadAssetBundleRequest (AssetBundle 下载)
UnityWebRequestDownloaderBase (抽象基类)
├── UnityWebRequestFileDownloader ──► IDownloadFileRequest
├── UnityWebRequestHeadDownloader ──► IDownloadHeadRequest
├── UnityWebRequestBytesDownloader ──► IDownloadBytesRequest
├── UnityWebRequestTextDownloader ──► IDownloadTextRequest
└── UnityWebRequestAssetBundleDownloader ──► IDownloadAssetBundleRequest
VirtualFileDownloader (独立实现) ──► IDownloadFileRequest
```
---
## 工具类
### DownloadSystemHelper
提供跨平台的工具函数:
| 方法 | 说明 |
|------|------|
| `ConvertToWWWPath()` | 转换本地路径为 WWW 协议 URL |
| `IsRequestLocalFile()` | 判断是否本地文件请求 |
### WebRequestCounter
请求失败计数器,用于诊断统计:
- 线程安全:内部使用 `Dictionary` 且未加锁,约定只在主线程调用;如需多线程统计请在外层加锁或改造实现
- Key 规则:`$"{packageName}_{eventName}"`
- 统计口径:**仅统计网络请求失败**`IDownloadRequest.Status != Succeed` 时记录),不统计内容为空、校验失败、解析失败等业务层失败
```csharp
// 记录失败
WebRequestCounter.RecordRequestFailed(packageName, eventName);
// 查询失败次数
int count = WebRequestCounter.GetRequestFailedCount(packageName, eventName);
```
---
## 注意事项
1. **资源释放**:使用完毕后务必调用 `Dispose()` 释放资源
- `AbortRequest()` 仅用于中止请求与切换状态,不等同于释放资源;无论成功/失败/中止都需要 `Dispose()`
- 推荐使用 `try/finally` 确保释放(尤其是上层可能提前中止的场景)
2. **断点续传**:需要服务器支持 `Range` 请求头和 `206 Partial Content` 响应
- 若服务端不支持 Range 仍返回 200全量内容可能会被追加写入导致文件损坏
3. **看门狗超时**:设置为 0 表示禁用,建议根据网络环境设置合理值
4. **内存下载**`IDownloadBytesRequest` 会将整个响应体加载到内存,不适合大文件
5. **驱动更新**:部分第三方网络库实现的 backend 可能需要每帧调用 `IDownloadBackend.Update()` 进行驱动
6. **中止语义**`Aborted` 可能来自用户主动 `AbortRequest()` 或看门狗超时;中止场景下 `HttpCode/Error` 可能为默认值(例如 0/空)
7. **线程安全**:所有下载请求的创建和轮询应在主线程进行

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2c65d3ba8da65004b8d36dbeecc6c6be
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,9 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset
{
/// <summary>
/// 网络请求失败计数器(诊断用)
/// </summary>
/// <remarks>
/// 线程安全:内部使用 Dictionary 且未加锁,约定只在 Unity 主线程调用。
/// 如需在多线程/回调线程调用,请在外层加锁或改为并发容器实现。
/// </remarks>
internal class WebRequestCounter
{
#if UNITY_EDITOR
@@ -15,12 +20,12 @@ namespace YooAsset
#endif
/// <summary>
/// 记录网络请求失败事件的次数
/// 失败计数记录表key = $"{packageName}_{eventName}"
/// </summary>
private static readonly Dictionary<string, int> _requestFailedRecorder = new Dictionary<string, int>(1000);
/// <summary>
/// 记录请求失败事件
/// 记录一次失败
/// </summary>
public static void RecordRequestFailed(string packageName, string eventName)
{
@@ -31,14 +36,14 @@ namespace YooAsset
}
/// <summary>
/// 获取请求失败次数
/// 获取失败次数
/// </summary>
public static int GetRequestFailedCount(string packageName, string eventName)
{
string key = $"{packageName}_{eventName}";
if (_requestFailedRecorder.ContainsKey(key) == false)
_requestFailedRecorder.Add(key, 0);
return _requestFailedRecorder[key];
if (_requestFailedRecorder.TryGetValue(key, out int count))
return count;
return 0;
}
}
}
}

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -107,7 +107,7 @@ namespace YooAsset
internal override void InternalWaitForAsyncComplete()
{
//注意:场景加载不支持异步转同步,为了支持同步加载方法需要实现该方法!
InternalUpdate();
RunOnceExecution();
}
public override void UnSuspendLoad()
{

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -109,14 +109,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -88,14 +88,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -113,7 +113,7 @@ namespace YooAsset
internal override void InternalWaitForAsyncComplete()
{
//注意:场景加载不支持异步转同步,为了支持同步加载方法需要实现该方法!
InternalUpdate();
RunOnceExecution();
}
public override void UnSuspendLoad()
{

View File

@@ -100,14 +100,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -1,7 +1,7 @@

namespace YooAsset
{
internal class CatalogDefine
internal class CatalogFileDefine
{
/// <summary>
/// 文件极限大小100MB

View File

@@ -5,14 +5,14 @@ using UnityEngine;
namespace YooAsset
{
internal static class CatalogTools
internal static class CatalogFileHelper
{
#if UNITY_EDITOR
/// <summary>
/// 生成包裹的内置资源目录文件
/// 说明:根据指定目录下的文件生成清单文件。
/// </summary>
public static bool CreateCatalogFile(IManifestRestoreServices services, string packageName, string packageDirectory)
public static bool CreateFile(IManifestRestoreServices services, string packageName, string packageDirectory)
{
// 获取资源清单版本
string packageVersion;
@@ -40,7 +40,7 @@ namespace YooAsset
}
var binaryData = FileUtility.ReadAllBytes(manifestFilePath);
packageManifest = ManifestTools.DeserializeFromBinary(binaryData, services);
packageManifest = PackageManifestTools.DeserializeFromBinary(binaryData, services);
}
// 获取文件名映射关系
@@ -54,7 +54,7 @@ namespace YooAsset
// 创建内置清单实例
var buildinFileCatalog = new DefaultBuildinFileCatalog();
buildinFileCatalog.FileVersion = CatalogDefine.FileVersion;
buildinFileCatalog.FileVersion = CatalogFileDefine.FileVersion;
buildinFileCatalog.PackageName = packageName;
buildinFileCatalog.PackageVersion = packageVersion;
@@ -122,11 +122,11 @@ namespace YooAsset
/// <summary>
/// 生成空的包裹内置资源目录文件
/// </summary>
public static bool CreateEmptyCatalogFile(string packageName, string packageVersion, string outputPath)
public static bool CreateEmptyFile(string packageName, string packageVersion, string outputPath)
{
// 创建内置清单实例
var buildinFileCatalog = new DefaultBuildinFileCatalog();
buildinFileCatalog.FileVersion = CatalogDefine.FileVersion;
buildinFileCatalog.FileVersion = CatalogFileDefine.FileVersion;
buildinFileCatalog.PackageName = packageName;
buildinFileCatalog.PackageVersion = packageVersion;
@@ -173,13 +173,13 @@ namespace YooAsset
using (FileStream fs = new FileStream(savePath, FileMode.Create))
{
// 创建缓存器
BufferWriter buffer = new BufferWriter(CatalogDefine.FileMaxSize);
BufferWriter buffer = new BufferWriter(CatalogFileDefine.FileMaxSize);
// 写入文件标记
buffer.WriteUInt32(CatalogDefine.FileSign);
buffer.WriteUInt32(CatalogFileDefine.FileSign);
// 写入文件版本
buffer.WriteUTF8(CatalogDefine.FileVersion);
buffer.WriteUTF8(CatalogFileDefine.FileVersion);
// 写入文件头信息
buffer.WriteUTF8(catalog.PackageName);
@@ -205,18 +205,21 @@ namespace YooAsset
/// </summary>
public static DefaultBuildinFileCatalog DeserializeFromBinary(byte[] binaryData)
{
if (binaryData == null || binaryData.Length == 0)
throw new Exception("Catalog file data is null or empty !");
// 创建缓存器
BufferReader buffer = new BufferReader(binaryData);
// 读取文件标记
uint fileSign = buffer.ReadUInt32();
if (fileSign != CatalogDefine.FileSign)
if (fileSign != CatalogFileDefine.FileSign)
throw new Exception("Invalid catalog file !");
// 读取文件版本
string fileVersion = buffer.ReadUTF8();
if (fileVersion != CatalogDefine.FileVersion)
throw new Exception($"The catalog file version are not compatible : {fileVersion} != {CatalogDefine.FileVersion}");
if (fileVersion != CatalogFileDefine.FileVersion)
throw new Exception($"The catalog file version are not compatible : {fileVersion} != {CatalogFileDefine.FileVersion}");
DefaultBuildinFileCatalog catalog = new DefaultBuildinFileCatalog();
{

View File

@@ -25,6 +25,11 @@ namespace YooAsset
protected IFileSystem _unpackFileSystem;
protected string _packageRoot;
/// <summary>
/// 下载后台接口
/// </summary>
public IDownloadBackend DownloadBackend { private set; get; }
/// <summary>
/// 包裹名称
/// </summary>
@@ -53,6 +58,11 @@ namespace YooAsset
}
#region
/// <summary>
/// 自定义参数UnityWebRequest 创建委托
/// </summary>
public UnityWebRequestCreator WebRequestCreator { private set; get; }
/// <summary>
/// 自定义参数:覆盖安装缓存清理模式
/// </summary>
@@ -66,7 +76,7 @@ namespace YooAsset
/// <summary>
/// 自定义参数:初始化的时候缓存文件校验最大并发数
/// </summary>
public int FileVerifyMaxConcurrency { private set; get; } = int.MaxValue;
public int FileVerifyMaxConcurrency { private set; get; } = 32;
/// <summary>
/// 自定义参数:数据文件追加文件格式
@@ -97,12 +107,12 @@ namespace YooAsset
/// <summary>
/// 自定义参数:解密服务接口的实例类
/// </summary>
public IDecryptionServices DecryptionServices { private set; get; }
public IBundleDecryptionServices BundleDecryptionServices { private set; get; }
/// <summary>
/// 自定义参数:资源清单服务类
/// </summary>
public IManifestRestoreServices ManifestServices { private set; get; }
public IManifestRestoreServices ManifestRestoreServices { private set; get; }
/// <summary>
/// 自定义参数:拷贝内置文件接口的实例类
@@ -119,43 +129,51 @@ namespace YooAsset
var operation = new DBFSInitializeOperation(this);
return operation;
}
public virtual FSLoadPackageManifestOperation LoadPackageManifestAsync(string packageVersion, int timeout)
{
var operation = new DBFSLoadPackageManifestOperation(this, packageVersion);
return operation;
}
public virtual FSRequestPackageVersionOperation RequestPackageVersionAsync(bool appendTimeTicks, int timeout)
public virtual FSRequestPackageVersionOperation RequestPackageVersionAsync(RequestPackageVersionOptions options)
{
var operation = new DBFSRequestPackageVersionOperation(this);
return operation;
}
public virtual FSClearCacheFilesOperation ClearCacheFilesAsync(PackageManifest manifest, ClearCacheFilesOptions options)
public virtual FSLoadPackageManifestOperation LoadPackageManifestAsync(LoadPackageManifestOptions options)
{
return _unpackFileSystem.ClearCacheFilesAsync(manifest, options);
var operation = new DBFSLoadPackageManifestOperation(this, options.PackageVersion);
return operation;
}
public virtual FSDownloadFileOperation DownloadFileAsync(PackageBundle bundle, DownloadFileOptions options)
public virtual FSClearCacheFilesOperation ClearCacheFilesAsync(ClearCacheFilesOptions options)
{
return _unpackFileSystem.ClearCacheFilesAsync(options);
}
public virtual FSDownloadFileOperation DownloadFileAsync(DownloadFileOptions options)
{
// 注意:业务层的解压器会依赖该方法
options.ImportFilePath = GetBuildinFileLoadPath(bundle);
return _unpackFileSystem.DownloadFileAsync(bundle, options);
options.ImportFilePath = GetBuildinFileLoadPath(options.Bundle);
return _unpackFileSystem.DownloadFileAsync(options);
}
public virtual FSLoadBundleOperation LoadBundleFile(PackageBundle bundle)
public virtual FSLoadBundleOperation LoadBundleAsync(LoadBundleOptions options)
{
PackageBundle bundle = options.Bundle;
if (IsUnpackBundleFile(bundle))
{
return _unpackFileSystem.LoadBundleFile(bundle);
return _unpackFileSystem.LoadBundleAsync(options);
}
if (bundle.BundleType == (int)EBuildBundleType.AssetBundle)
if (bundle.BundleType == (int)EBundleType.AssetBundle)
{
var operation = new DBFSLoadAssetBundleOperation(this, bundle);
return operation;
}
else if (bundle.BundleType == (int)EBuildBundleType.RawBundle)
else if (bundle.BundleType == (int)EBundleType.RawBundle)
{
var operation = new DBFSLoadRawBundleOperation(this, bundle);
return operation;
}
#if TUANJIE_1_7_OR_NEWER
else if (bundle.BundleType == (int)EBuildBundleType.InstantBundle)
{
var operation = new DBFSLoadInstantBundleOperation(this, bundle);
return operation;
}
#endif
else
{
string error = $"{nameof(DefaultBuildinFileSystem)} not support load bundle type : {bundle.BundleType}";
@@ -166,7 +184,15 @@ namespace YooAsset
public virtual void SetParameter(string name, object value)
{
if (name == FileSystemParametersDefine.INSTALL_CLEAR_MODE)
if (name == FileSystemParametersDefine.DOWNLOAD_BACKEND)
{
DownloadBackend = (IDownloadBackend)value;
}
else if (name == FileSystemParametersDefine.UNITY_WEB_REQUEST_CREATOR)
{
WebRequestCreator = (UnityWebRequestCreator)value;
}
else if (name == FileSystemParametersDefine.INSTALL_CLEAR_MODE)
{
InstallClearMode = (EOverwriteInstallClearMode)value;
}
@@ -199,13 +225,13 @@ namespace YooAsset
{
UnpackFileSystemRoot = (string)value;
}
else if (name == FileSystemParametersDefine.DECRYPTION_SERVICES)
else if (name == FileSystemParametersDefine.BUNDLE_DECRYPTION_SERVICES)
{
DecryptionServices = (IDecryptionServices)value;
BundleDecryptionServices = (IBundleDecryptionServices)value;
}
else if (name == FileSystemParametersDefine.MANIFEST_SERVICES)
else if (name == FileSystemParametersDefine.MANIFEST_RESTORE_SERVICES)
{
ManifestServices = (IManifestRestoreServices)value;
ManifestRestoreServices = (IManifestRestoreServices)value;
}
else if (name == FileSystemParametersDefine.COPY_LOCAL_FILE_SERVICES)
{
@@ -225,20 +251,37 @@ namespace YooAsset
else
_packageRoot = packageRoot;
// 创建默认的下载后台接口
if (DownloadBackend == null)
DownloadBackend = new UnityWebRequestBackend(WebRequestCreator);
// 创建解压文件系统
var remoteServices = new DefaultUnpackRemoteServices(_packageRoot);
_unpackFileSystem = new DefaultUnpackFileSystem();
_unpackFileSystem.SetParameter(FileSystemParametersDefine.REMOTE_SERVICES, remoteServices);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.DOWNLOAD_BACKEND, DownloadBackend);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.UNITY_WEB_REQUEST_CREATOR, WebRequestCreator);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.INSTALL_CLEAR_MODE, InstallClearMode);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.FILE_VERIFY_LEVEL, FileVerifyLevel);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY, FileVerifyMaxConcurrency);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.APPEND_FILE_EXTENSION, AppendFileExtension);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.DECRYPTION_SERVICES, DecryptionServices);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.BUNDLE_DECRYPTION_SERVICES, BundleDecryptionServices);
_unpackFileSystem.SetParameter(FileSystemParametersDefine.COPY_LOCAL_FILE_SERVICES, CopyLocalFileServices);
_unpackFileSystem.OnCreate(packageName, UnpackFileSystemRoot);
}
public virtual void OnDestroy()
{
if (_unpackFileSystem != null)
{
_unpackFileSystem.OnDestroy();
_unpackFileSystem = null;
}
if (DownloadBackend != null)
{
DownloadBackend.Dispose();
DownloadBackend = null;
}
}
public virtual bool Belong(PackageBundle bundle)
@@ -367,7 +410,7 @@ namespace YooAsset
if (bundle.Encrypted)
return true;
if (bundle.BundleType == (int)EBuildBundleType.RawBundle)
if (bundle.BundleType == (int)EBundleType.RawBundle)
return true;
return false;
@@ -437,32 +480,32 @@ namespace YooAsset
/// <summary>
/// 加载加密的资源文件
/// </summary>
public DecryptResult LoadEncryptedAssetBundle(PackageBundle bundle)
public DecryptSyncResult LoadEncryptedBundleSync(PackageBundle bundle)
{
string filePath = GetBuildinFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.LoadAssetBundle(fileInfo);
return BundleDecryptionServices.LoadAssetBundleSync(bundleInfo);
}
/// <summary>
/// 加载加密的资源文件
/// </summary>
public DecryptResult LoadEncryptedAssetBundleAsync(PackageBundle bundle)
public DecryptAsyncResult LoadEncryptedBundleAsync(PackageBundle bundle)
{
string filePath = GetBuildinFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.LoadAssetBundleAsync(fileInfo);
return BundleDecryptionServices.LoadAssetBundleAsync(bundleInfo);
}
#endregion
}
}
}

View File

@@ -78,7 +78,7 @@ namespace YooAsset
string packageVersion = _requestBuildinPackageVersionOp.PackageVersion;
string destFilePath = GetCopyPackageHashDestPath(packageVersion);
string sourceFilePath = _fileSystem.GetBuildinPackageHashFilePath(packageVersion);
_copyBuildinHashFileOp = new CopyBuildinFileOperation(sourceFilePath, destFilePath);
_copyBuildinHashFileOp = new CopyBuildinFileOperation(_fileSystem, sourceFilePath, destFilePath);
_copyBuildinHashFileOp.StartOperation();
AddChildOperation(_copyBuildinHashFileOp);
}
@@ -106,7 +106,7 @@ namespace YooAsset
string packageVersion = _requestBuildinPackageVersionOp.PackageVersion;
string destFilePath = GetCopyPackageManifestDestPath(packageVersion);
string sourceFilePath = _fileSystem.GetBuildinPackageManifestFilePath(packageVersion);
_copyBuildinManifestFileOp = new CopyBuildinFileOperation(sourceFilePath, destFilePath);
_copyBuildinManifestFileOp = new CopyBuildinFileOperation(_fileSystem, sourceFilePath, destFilePath);
_copyBuildinManifestFileOp.StartOperation();
AddChildOperation(_copyBuildinManifestFileOp);
}

View File

@@ -41,6 +41,205 @@ namespace YooAsset
return;
if (_steps == ESteps.LoadAssetBundle)
{
if (_bundle.Encrypted)
{
if (_fileSystem.BundleDecryptionServices == null)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"The {nameof(IBundleDecryptionServices)} is null !";
YooLogger.Error(Error);
return;
}
}
if (IsWaitForAsyncComplete)
{
if (_bundle.Encrypted)
{
var decryptResult = _fileSystem.LoadEncryptedBundleSync(_bundle);
_assetBundle = decryptResult.Result;
_managedStream = decryptResult.ManagedStream;
}
else
{
string filePath = _fileSystem.GetBuildinFileLoadPath(_bundle);
_assetBundle = AssetBundle.LoadFromFile(filePath);
}
}
else
{
if (_bundle.Encrypted)
{
var decryptResult = _fileSystem.LoadEncryptedBundleAsync(_bundle);
_createRequest = decryptResult.CreateRequest;
_managedStream = decryptResult.ManagedStream;
}
else
{
string filePath = _fileSystem.GetBuildinFileLoadPath(_bundle);
_createRequest = AssetBundle.LoadFromFileAsync(filePath);
}
}
_steps = ESteps.CheckResult;
}
if (_steps == ESteps.CheckResult)
{
if (_createRequest != null)
{
if (IsWaitForAsyncComplete)
{
// 强制挂起主线程(注意:该操作会很耗时)
YooLogger.Warning("Suspend the main thread to load unity bundle.");
_assetBundle = _createRequest.assetBundle;
}
else
{
if (_createRequest.isDone == false)
return;
_assetBundle = _createRequest.assetBundle;
}
}
if (_assetBundle == null)
{
if (_bundle.Encrypted)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Failed to load encrypted buildin asset bundle file : {_bundle.BundleName}";
YooLogger.Error(Error);
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Failed to load buildin asset bundle file : {_bundle.BundleName}";
YooLogger.Error(Error);
}
}
else
{
_steps = ESteps.Done;
Result = new AssetBundleResult(_fileSystem, _bundle, _assetBundle, _managedStream);
Status = EOperationStatus.Succeed;
}
}
}
internal override void InternalWaitForAsyncComplete()
{
RunBatchExecution();
}
}
/// <summary>
/// 加载原生文件
/// </summary>
internal class DBFSLoadRawBundleOperation : FSLoadBundleOperation
{
private enum ESteps
{
None,
LoadBuildinRawBundle,
Done,
}
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly PackageBundle _bundle;
private ESteps _steps = ESteps.None;
internal DBFSLoadRawBundleOperation(DefaultBuildinFileSystem fileSystem, PackageBundle bundle)
{
_fileSystem = fileSystem;
_bundle = bundle;
}
internal override void InternalStart()
{
DownloadProgress = 1f;
DownloadedBytes = _bundle.FileSize;
_steps = ESteps.LoadBuildinRawBundle;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.LoadBuildinRawBundle)
{
string filePath = _fileSystem.GetBuildinFileLoadPath(_bundle);
#if UNITY_ANDROID
//TODO : 安卓平台内置文件属于APK压缩包内的文件。
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Can not load android buildin raw bundle file : {filePath}";
YooLogger.Error(Error);
#else
if (File.Exists(filePath))
{
_steps = ESteps.Done;
Result = new RawBundleResult(_fileSystem, _bundle);
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Can not found buildin raw bundle file : {filePath}";
YooLogger.Error(Error);
}
#endif
}
}
internal override void InternalWaitForAsyncComplete()
{
RunBatchExecution();
}
}
#if TUANJIE_1_7_OR_NEWER
/// <summary>
/// 加载团结文件
/// </summary>
internal class DBFSLoadInstantBundleOperation : FSLoadBundleOperation
{
private enum ESteps
{
None,
LoadInstantBundle,
CheckResult,
Done,
}
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly PackageBundle _bundle;
private AssetBundleCreateRequest _createRequest;
private AssetBundle _assetBundle;
private Stream _managedStream;
private ESteps _steps = ESteps.None;
internal DBFSLoadInstantBundleOperation(DefaultBuildinFileSystem fileSystem, PackageBundle bundle)
{
_fileSystem = fileSystem;
_bundle = bundle;
}
internal override void InternalStart()
{
DownloadProgress = 1f;
DownloadedBytes = _bundle.FileSize;
_steps = ESteps.LoadInstantBundle;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.LoadInstantBundle)
{
if (_bundle.Encrypted)
{
@@ -131,87 +330,8 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
/// <summary>
/// 加载原生文件
/// </summary>
internal class DBFSLoadRawBundleOperation : FSLoadBundleOperation
{
private enum ESteps
{
None,
LoadBuildinRawBundle,
Done,
}
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly PackageBundle _bundle;
private ESteps _steps = ESteps.None;
internal DBFSLoadRawBundleOperation(DefaultBuildinFileSystem fileSystem, PackageBundle bundle)
{
_fileSystem = fileSystem;
_bundle = bundle;
}
internal override void InternalStart()
{
DownloadProgress = 1f;
DownloadedBytes = _bundle.FileSize;
_steps = ESteps.LoadBuildinRawBundle;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.LoadBuildinRawBundle)
{
string filePath = _fileSystem.GetBuildinFileLoadPath(_bundle);
#if UNITY_ANDROID
//TODO : 安卓平台内置文件属于APK压缩包内的文件。
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Can not load android buildin raw bundle file : {filePath}";
YooLogger.Error(Error);
#else
if (File.Exists(filePath))
{
_steps = ESteps.Done;
Result = new RawBundleResult(_fileSystem, _bundle);
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Can not found buildin raw bundle file : {filePath}";
YooLogger.Error(Error);
}
#endif
}
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
}
}
}

View File

@@ -14,13 +14,15 @@ namespace YooAsset
Done,
}
private UnityWebFileRequestOperation _webFileRequestOp;
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly string _sourceFilePath;
private readonly string _destFilePath;
private IDownloadFileRequest _webFileRequestOp;
private ESteps _steps = ESteps.None;
public CopyBuildinFileOperation(string sourceFilePath, string destFilePath)
public CopyBuildinFileOperation(DefaultBuildinFileSystem fileSystem, string sourceFilePath, string destFilePath)
{
_fileSystem = fileSystem;
_sourceFilePath = sourceFilePath;
_destFilePath = destFilePath;
}
@@ -76,16 +78,15 @@ namespace YooAsset
if (_webFileRequestOp == null)
{
string url = DownloadSystemHelper.ConvertToWWWPath(_sourceFilePath);
_webFileRequestOp = new UnityWebFileRequestOperation(url, _destFilePath, 60);
_webFileRequestOp.StartOperation();
AddChildOperation(_webFileRequestOp);
var args = new DownloadFileRequestArgs(url, _destFilePath, 60, 0);
_webFileRequestOp = _fileSystem.DownloadBackend.CreateFileRequest(args);
_webFileRequestOp.SendRequest();
}
_webFileRequestOp.UpdateOperation();
if (_webFileRequestOp.IsDone == false)
return;
if (_webFileRequestOp.Status == EOperationStatus.Succeed)
if (_webFileRequestOp.Status == EDownloadRequestStatus.Succeed)
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;

View File

@@ -15,7 +15,7 @@ namespace YooAsset
}
private readonly DefaultBuildinFileSystem _fileSystem;
private UnityWebDataRequestOperation _webDataRequestOp;
private IDownloadBytesRequest _webDataRequestOp;
private byte[] _fileData;
private ESteps _steps = ESteps.None;
@@ -57,16 +57,15 @@ namespace YooAsset
{
string filePath = _fileSystem.GetCatalogBinaryFileLoadPath();
string url = DownloadSystemHelper.ConvertToWWWPath(filePath);
_webDataRequestOp = new UnityWebDataRequestOperation(url, 60);
_webDataRequestOp.StartOperation();
AddChildOperation(_webDataRequestOp);
var args = new DownloadDataRequestArgs(url, 60, 0);
_webDataRequestOp = _fileSystem.DownloadBackend.CreateBytesRequest(args);
_webDataRequestOp.SendRequest();
}
_webDataRequestOp.UpdateOperation();
if (_webDataRequestOp.IsDone == false)
return;
if (_webDataRequestOp.Status == EOperationStatus.Succeed)
if (_webDataRequestOp.Status == EDownloadRequestStatus.Succeed)
{
_fileData = _webDataRequestOp.Result;
_steps = ESteps.LoadCatalog;
@@ -83,15 +82,15 @@ namespace YooAsset
{
try
{
Catalog = CatalogTools.DeserializeFromBinary(_fileData);
Catalog = CatalogFileHelper.DeserializeFromBinary(_fileData);
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
catch (Exception e)
catch (Exception ex)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Failed to load catalog file : {e.Message}";
Error = $"Failed to load catalog file : {ex.Message}";
}
}
}

View File

@@ -17,7 +17,7 @@ namespace YooAsset
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly string _packageVersion;
private readonly string _packageHash;
private UnityWebDataRequestOperation _webDataRequestOp;
private IDownloadBytesRequest _webDataRequestOp;
private DeserializeManifestOperation _deserializer;
private byte[] _fileData;
private ESteps _steps = ESteps.None;
@@ -63,16 +63,15 @@ namespace YooAsset
{
string filePath = _fileSystem.GetBuildinPackageManifestFilePath(_packageVersion);
string url = DownloadSystemHelper.ConvertToWWWPath(filePath);
_webDataRequestOp = new UnityWebDataRequestOperation(url, 60);
_webDataRequestOp.StartOperation();
AddChildOperation(_webDataRequestOp);
var args = new DownloadDataRequestArgs(url, 60, 0);
_webDataRequestOp = _fileSystem.DownloadBackend.CreateBytesRequest(args);
_webDataRequestOp.SendRequest();
}
_webDataRequestOp.UpdateOperation();
if (_webDataRequestOp.IsDone == false)
return;
if (_webDataRequestOp.Status == EOperationStatus.Succeed)
if (_webDataRequestOp.Status == EDownloadRequestStatus.Succeed)
{
_fileData = _webDataRequestOp.Result;
_steps = ESteps.VerifyFileData;
@@ -87,7 +86,7 @@ namespace YooAsset
if (_steps == ESteps.VerifyFileData)
{
if (ManifestTools.VerifyManifestData(_fileData, _packageHash))
if (PackageManifestTools.VerifyManifestData(_fileData, _packageHash))
{
_steps = ESteps.LoadManifest;
}
@@ -103,7 +102,7 @@ namespace YooAsset
{
if (_deserializer == null)
{
_deserializer = new DeserializeManifestOperation(_fileSystem.ManifestServices, _fileData);
_deserializer = new DeserializeManifestOperation(_fileSystem.ManifestRestoreServices, _fileData);
_deserializer.StartOperation();
AddChildOperation(_deserializer);
}

View File

@@ -15,7 +15,7 @@ namespace YooAsset
private readonly DefaultBuildinFileSystem _fileSystem;
private readonly string _packageVersion;
private UnityWebTextRequestOperation _webTextRequestOp;
private IDownloadTextRequest _webTextRequestOp;
private ESteps _steps = ESteps.None;
/// <summary>
@@ -58,16 +58,15 @@ namespace YooAsset
{
string filePath = _fileSystem.GetBuildinPackageHashFilePath(_packageVersion);
string url = DownloadSystemHelper.ConvertToWWWPath(filePath);
_webTextRequestOp = new UnityWebTextRequestOperation(url, 60);
_webTextRequestOp.StartOperation();
AddChildOperation(_webTextRequestOp);
var args = new DownloadDataRequestArgs(url, 60, 0);
_webTextRequestOp = _fileSystem.DownloadBackend.CreateTextRequest(args);
_webTextRequestOp.SendRequest();
}
_webTextRequestOp.UpdateOperation();
if (_webTextRequestOp.IsDone == false)
return;
if (_webTextRequestOp.Status == EOperationStatus.Succeed)
if (_webTextRequestOp.Status == EDownloadRequestStatus.Succeed)
{
PackageHash = _webTextRequestOp.Result;
_steps = ESteps.CheckResult;

View File

@@ -14,7 +14,7 @@ namespace YooAsset
}
private readonly DefaultBuildinFileSystem _fileSystem;
private UnityWebTextRequestOperation _webTextRequestOp;
private IDownloadTextRequest _webTextRequestOp;
private ESteps _steps = ESteps.None;
/// <summary>
@@ -56,16 +56,15 @@ namespace YooAsset
{
string filePath = _fileSystem.GetBuildinPackageVersionFilePath();
string url = DownloadSystemHelper.ConvertToWWWPath(filePath);
_webTextRequestOp = new UnityWebTextRequestOperation(url, 60);
_webTextRequestOp.StartOperation();
AddChildOperation(_webTextRequestOp);
var args = new DownloadDataRequestArgs(url, 60, 0);
_webTextRequestOp = _fileSystem.DownloadBackend.CreateTextRequest(args);
_webTextRequestOp.SendRequest();
}
_webTextRequestOp.UpdateOperation();
if (_webTextRequestOp.IsDone == false)
return;
if (_webTextRequestOp.Status == EOperationStatus.Succeed)
if (_webTextRequestOp.Status == EDownloadRequestStatus.Succeed)
{
PackageVersion = _webTextRequestOp.Result;
_steps = ESteps.CheckResult;

View File

@@ -0,0 +1,488 @@
# DefaultBuildinFileSystem 内置文件系统
## 模块概述
DefaultBuildinFileSystem 是 YooAsset 的**内置资源文件系统**用于管理打包到应用程序中的资源文件StreamingAssets。该文件系统支持 AssetBundle 和原生文件的加载,并内置解压文件系统以处理 Android/OpenHarmony 平台的特殊需求。
### 核心特性
- **内置资源管理**:管理 StreamingAssets 目录下的资源文件
- **Catalog 目录系统**:使用目录文件快速查询内置资源
- **自动解压机制**Android/OpenHarmony 平台自动解压加密和原生文件
- **清单拷贝功能**:支持将内置清单拷贝到沙盒目录
- **加密资源支持**:通过解密服务接口支持加密资源加载
---
## 设计目标
| 目标 | 说明 |
|------|------|
| **跨平台支持** | 统一处理各平台 StreamingAssets 的访问差异 |
| **高效查询** | 通过 Catalog 文件快速判断资源是否内置 |
| **自动解压** | 自动处理 Android 平台无法直接访问的资源 |
| **灵活配置** | 支持多种参数配置适应不同需求 |
---
## 文件结构
```
DefaultBuildinFileSystem/
├── DefaultBuildinFileSystem.cs # 文件系统主类
├── DefaultBuildinFileSystemDefine.cs # 常量定义
├── DefaultBuildinFileCatalog.cs # 内置资源目录结构
├── CatalogDefine.cs # Catalog 文件格式定义
├── CatalogTools.cs # Catalog 序列化工具
└── Operation/ # 操作类
├── DBFSInitializeOperation.cs # 初始化操作
├── DBFSRequestPackageVersionOperation.cs # 请求版本操作
├── DBFSLoadPackageManifestOperation.cs # 加载清单操作
├── DBFSLoadBundleOperation.cs # 加载资源包操作
└── internal/ # 内部操作类
├── CopyBuildinFileOperation.cs # 拷贝内置文件操作
├── LoadBuildinCatalogFileOperation.cs # 加载 Catalog 文件操作
├── LoadBuildinPackageManifestOperation.cs# 加载清单文件操作
├── RequestBuildinPackageHashOperation.cs # 请求哈希文件操作
└── RequestBuildinPackageVersionOperation.cs # 请求版本文件操作
```
---
## 核心类说明
### DefaultBuildinFileSystem
内置文件系统的主类,实现 `IFileSystem` 接口。
#### 基本属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `PackageName` | `string` | 包裹名称 |
| `FileRoot` | `string` | 文件根目录StreamingAssets 下的包裹目录) |
| `FileCount` | `int` | 已记录的内置文件数量 |
| `DownloadBackend` | `IDownloadBackend` | 下载后台接口 |
#### 自定义参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `InstallClearMode` | `EOverwriteInstallClearMode` | `ClearAllManifestFiles` | 覆盖安装时的缓存清理模式 |
| `FileVerifyLevel` | `EFileVerifyLevel` | `Middle` | 文件校验级别 |
| `FileVerifyMaxConcurrency` | `int` | `32` | 文件校验最大并发数 |
| `AppendFileExtension` | `bool` | `false` | 是否追加文件扩展名 |
| `DisableCatalogFile` | `bool` | `false` | 禁用 Catalog 目录文件 |
| `CopyBuildinPackageManifest` | `bool` | `false` | 是否拷贝内置清单到沙盒 |
| `CopyBuildinPackageManifestDestRoot` | `string` | `null` | 清单拷贝目标目录 |
| `UnpackFileSystemRoot` | `string` | `null` | 解压文件系统根目录 |
| `DecryptionServices` | `IDecryptionServices` | `null` | 解密服务接口 |
| `ManifestServices` | `IManifestRestoreServices` | `null` | 清单恢复服务接口 |
| `CopyLocalFileServices` | `ICopyLocalFileServices` | `null` | 本地文件拷贝服务接口 |
#### 核心方法
```csharp
// 生命周期
void OnCreate(string packageName, string packageRoot);
void OnDestroy();
void SetParameter(string name, object value);
// 异步操作
FSInitializeFileSystemOperation InitializeFileSystemAsync();
FSRequestPackageVersionOperation RequestPackageVersionAsync(bool appendTimeTicks, int timeout);
FSLoadPackageManifestOperation LoadPackageManifestAsync(string packageVersion, int timeout);
FSLoadBundleOperation LoadBundleFile(PackageBundle bundle);
FSDownloadFileOperation DownloadFileAsync(PackageBundle bundle, DownloadFileOptions options);
FSClearCacheFilesOperation ClearCacheFilesAsync(PackageManifest manifest, ClearCacheFilesOptions options);
// 文件查询
bool Belong(PackageBundle bundle); // 检查是否属于内置文件
bool Exists(PackageBundle bundle); // 检查文件是否存在
bool NeedDownload(PackageBundle bundle);// 始终返回 false
bool NeedUnpack(PackageBundle bundle); // 检查是否需要解压
bool NeedImport(PackageBundle bundle); // 始终返回 false
// 文件访问
string GetBundleFilePath(PackageBundle bundle);
byte[] ReadBundleFileData(PackageBundle bundle);
string ReadBundleFileText(PackageBundle bundle);
```
---
## Catalog 目录系统
### DefaultBuildinFileCatalog
内置资源目录结构,记录所有内置资源文件的信息。
```csharp
[Serializable]
internal class DefaultBuildinFileCatalog
{
[Serializable]
public class FileWrapper
{
public string BundleGUID; // 资源包 GUID
public string FileName; // 文件名
}
public string FileVersion; // 文件版本
public string PackageName; // 包裹名称
public string PackageVersion; // 包裹版本
public List<FileWrapper> Wrappers; // 文件列表
}
```
### CatalogDefine
Catalog 文件格式常量定义。
```csharp
internal class CatalogDefine
{
public const int FileMaxSize = 104857600; // 文件极限大小100MB
public const uint FileSign = 0x133C5EE; // 文件头标记
public const string FileVersion = "1.0.0"; // 文件格式版本
}
```
### CatalogTools
Catalog 文件的序列化和反序列化工具。
| 方法 | 说明 |
|------|------|
| `CreateCatalogFile()` | 生成包裹的内置资源目录文件(编辑器) |
| `CreateEmptyCatalogFile()` | 生成空的内置资源目录文件(编辑器) |
| `SerializeToJson()` | 序列化为 JSON 文件 |
| `DeserializeFromJson()` | 从 JSON 文件反序列化 |
| `SerializeToBinary()` | 序列化为二进制文件 |
| `DeserializeFromBinary()` | 从二进制文件反序列化 |
---
## 操作类说明
### DBFSInitializeOperation
初始化操作,执行以下步骤:
```
状态流程:
┌─────────────────────────────────────────────────────────────┐
│ CopyBuildinPackageManifest = true ? │
│ │ │
│ ├── Yes ──► LoadBuildinPackageVersion │
│ │ └── RequestBuildinPackageVersionOp │
│ │ ↓ │
│ │ CopyBuildinPackageHash │
│ │ └── CopyBuildinFileOperation │
│ │ ↓ │
│ │ CopyBuildinPackageManifest │
│ │ └── CopyBuildinFileOperation │
│ │ ↓ │
│ └── No ─────────────────┘ │
│ ↓ │
│ InitUnpackFileSystem │
│ └── DefaultUnpackFileSystem.Init │
│ ↓ │
│ DisableCatalogFile = true ? │
│ ├── Yes ──► Done (Succeed) │
│ └── No ──► LoadCatalogFile │
│ └── LoadBuildinCatalog │
│ ↓ │
│ RecordCatalogFile │
│ ↓ │
│ Done (Succeed) │
└─────────────────────────────────────────────────────────────┘
```
### DBFSLoadBundleOperation
加载资源包操作,支持多种资源类型。
#### DBFSLoadAssetBundleOperation
加载 AssetBundle 文件。
```
状态流程:
LoadAssetBundle
├── 加密资源 ──► DecryptionServices.LoadAssetBundle[Async]
└── 普通资源 ──► AssetBundle.LoadFromFile[Async]
CheckResult
├── 成功 ──► AssetBundleResult
└── 失败 ──► Error
```
#### DBFSLoadRawBundleOperation
加载原生文件。
```
状态流程:
LoadBuildinRawBundle
├── Android 平台 ──► Error不支持直接读取
└── 其他平台 ──► RawBundleResult
```
#### DBFSLoadInstantBundleOperation
加载团结引擎Tuanjie专用资源包需要 `TUANJIE_1_7_OR_NEWER` 宏)。
---
## 内部操作类
### LoadBuildinCatalogFileOperation
加载 Catalog 目录文件。
```
状态流程:
TryLoadFileData
├── 文件存在 ──► File.ReadAllBytes
└── 文件不存在 ──► RequestFileData (UnityWebRequest)
LoadCatalog
└── CatalogTools.DeserializeFromBinary
```
### CopyBuildinFileOperation
拷贝内置文件到目标路径。
```
状态流程:
CheckFileExist
├── 目标已存在 ──► Done (Succeed)
└── 目标不存在 ──► TryCopyFile
TryCopyFile
├── 源文件存在 ──► File.Copy
└── 源文件不存在 ──► UnpackFile (UnityWebRequest)
```
---
## 解压机制
### 自动解压条件
在 Android/OpenHarmony 平台上,以下情况需要解压到沙盒:
```csharp
protected virtual bool IsUnpackBundleFile(PackageBundle bundle)
{
#if UNITY_ANDROID || UNITY_OPENHARMONY
if (bundle.Encrypted) // 加密资源
return true;
if (bundle.BundleType == RawBundle) // 原生文件
return true;
return false;
#else
return false;
#endif
}
```
### 解压文件系统
内置文件系统在创建时会自动创建一个 `DefaultUnpackFileSystem` 实例:
```csharp
public virtual void OnCreate(string packageName, string packageRoot)
{
// 创建解压文件系统
var remoteServices = new DefaultUnpackRemoteServices(_packageRoot);
_unpackFileSystem = new DefaultUnpackFileSystem();
_unpackFileSystem.SetParameter(REMOTE_SERVICES, remoteServices);
_unpackFileSystem.SetParameter(FILE_VERIFY_LEVEL, FileVerifyLevel);
// ... 其他参数
_unpackFileSystem.OnCreate(packageName, UnpackFileSystemRoot);
}
```
---
## 平台差异处理
### Android 平台限制
```
┌─────────────────────────────────────────────────────────────┐
│ Android 平台特殊处理 │
├─────────────────────────────────────────────────────────────┤
│ StreamingAssets 文件位于 APK 压缩包内,无法直接访问: │
│ │
│ ✓ AssetBundle.LoadFromFile 支持Unity 内部处理) │
│ ✗ File.ReadAllBytes 不支持 │
│ ✗ File.Exists 不支持 │
│ ✓ UnityWebRequest 支持jar:file:// 协议) │
│ │
│ 解决方案: │
│ 1. 加密资源 → 自动解压到沙盒 │
│ 2. 原生文件 → 自动解压到沙盒 │
│ 3. Catalog → 使用 UnityWebRequest 读取 │
└─────────────────────────────────────────────────────────────┘
```
### WebGL 平台
```csharp
#if UNITY_WEBGL
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"{nameof(DefaultBuildinFileSystem)} is not support WEBGL platform !";
#endif
```
WebGL 平台不支持 DefaultBuildinFileSystem应使用 `DefaultWebServerFileSystem`
---
## 使用示例
### 基础配置
```csharp
// 创建内置文件系统参数
var buildinParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
// 初始化包裹
var initParams = new OfflinePlayModeParameters();
initParams.BuildinFileSystemParameters = buildinParams;
var initOp = package.InitializeAsync(initParams);
```
### 配置解密服务
```csharp
var buildinParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
// 设置解密服务
buildinParams.AddParameter(
FileSystemParametersDefine.DECRYPTION_SERVICES,
new MyDecryptionServices()
);
```
### 配置清单拷贝
```csharp
var buildinParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
// 启用清单拷贝(用于离线模式切换到联机模式)
buildinParams.AddParameter(
FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST,
true
);
// 可选:指定拷贝目标目录
buildinParams.AddParameter(
FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST_DEST_ROOT,
"/custom/path"
);
```
### 禁用 Catalog 文件
```csharp
var buildinParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
// 禁用 Catalog所有资源视为内置
buildinParams.AddParameter(
FileSystemParametersDefine.DISABLE_CATALOG_FILE,
true
);
```
### 配置解压文件系统根目录
```csharp
var buildinParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
// 设置解压文件系统的根目录
buildinParams.AddParameter(
FileSystemParametersDefine.UNPACK_FILE_SYSTEM_ROOT,
"/custom/unpack/path"
);
```
---
## 参数常量
```csharp
// 安装清理
FileSystemParametersDefine.INSTALL_CLEAR_MODE // EOverwriteInstallClearMode
// 文件校验
FileSystemParametersDefine.FILE_VERIFY_LEVEL // EFileVerifyLevel
FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY // int
// 文件配置
FileSystemParametersDefine.APPEND_FILE_EXTENSION // bool
FileSystemParametersDefine.DISABLE_CATALOG_FILE // bool
// 清单拷贝
FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST // bool
FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST_DEST_ROOT // string
// 解压配置
FileSystemParametersDefine.UNPACK_FILE_SYSTEM_ROOT // string
// 服务接口
FileSystemParametersDefine.DECRYPTION_SERVICES // IDecryptionServices
FileSystemParametersDefine.MANIFEST_SERVICES // IManifestRestoreServices
FileSystemParametersDefine.COPY_LOCAL_FILE_SERVICES // ICopyLocalFileServices
```
---
## 类继承关系
```
IFileSystem
└── DefaultBuildinFileSystem
└── (内部持有) DefaultUnpackFileSystem
FSInitializeFileSystemOperation
└── DBFSInitializeOperation
FSRequestPackageVersionOperation
└── DBFSRequestPackageVersionOperation
FSLoadPackageManifestOperation
└── DBFSLoadPackageManifestOperation
FSLoadBundleOperation
├── DBFSLoadAssetBundleOperation
├── DBFSLoadRawBundleOperation
└── DBFSLoadInstantBundleOperation (Tuanjie)
AsyncOperationBase
├── LoadBuildinCatalogFileOperation
├── CopyBuildinFileOperation
├── LoadBuildinPackageManifestOperation
├── RequestBuildinPackageHashOperation
└── RequestBuildinPackageVersionOperation
BundleResult
├── AssetBundleResult ← AssetBundle 资源
└── RawBundleResult ← 原生文件
```
---
## 注意事项
1. **WebGL 不支持**DefaultBuildinFileSystem 不支持 WebGL 平台
2. **Android 限制**Android 平台无法直接读取 StreamingAssets 中的原生文件
3. **Catalog 文件**:构建时需要生成 Catalog 文件,否则需要禁用 Catalog 功能
4. **解压目录**:解压的文件存储在 `UnpackFileSystemRoot` 指定的目录
5. **加密资源**:加密资源在 Android/OpenHarmony 平台会自动解压到沙盒
6. **清单拷贝**:启用清单拷贝可以支持从离线模式平滑切换到联机模式

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7ec531875d1515b4496e0e9035e63661
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -23,10 +23,14 @@ namespace YooAsset
protected string _cacheManifestFilesRoot;
/// <summary>
/// 下载中心
/// 说明:当异步操作任务终止的时候,所有下载子任务都会一同被终止!
/// 下载调度器
/// </summary>
public DownloadCenterOperation DownloadCenter { set; get; }
public DownloadSchedulerOperation DownloadScheduler { set; get; }
/// <summary>
/// 下载后台接口
/// </summary>
public IDownloadBackend DownloadBackend { private set; get; }
/// <summary>
/// 包裹名称
@@ -56,6 +60,11 @@ namespace YooAsset
}
#region
/// <summary>
/// 自定义参数UnityWebRequest 创建委托
/// </summary>
public UnityWebRequestCreator WebRequestCreator { private set; get; }
/// <summary>
/// 自定义参数:远程服务接口的实例类
/// </summary>
@@ -73,8 +82,10 @@ namespace YooAsset
/// <summary>
/// 自定义参数:初始化的时候缓存文件校验最大并发数
/// 默认值32推荐范围 1-128
/// 说明:过大的值可能导致线程池任务过多,影响系统稳定性
/// </summary>
public int FileVerifyMaxConcurrency { private set; get; } = int.MaxValue;
public int FileVerifyMaxConcurrency { private set; get; } = 32;
/// <summary>
/// 自定义参数:数据文件追加文件格式
@@ -88,18 +99,22 @@ namespace YooAsset
/// <summary>
/// 自定义参数:最大并发连接数
/// 默认值10推荐范围 1-32
/// 说明:过大的并发数可能被服务器限流,也会增加本地资源消耗
/// </summary>
public int DownloadMaxConcurrency { private set; get; } = int.MaxValue;
public int DownloadMaxConcurrency { private set; get; } = 10;
/// <summary>
/// 自定义参数:每帧发起的最大请求数
/// 默认值5推荐范围 1-10
/// 说明:避免单帧发起过多请求导致卡顿
/// </summary>
public int DownloadMaxRequestPerFrame { private set; get; } = int.MaxValue;
public int DownloadMaxRequestPerFrame { private set; get; } = 5;
/// <summary>
/// 自定义参数:下载任务的看门狗机制监控时间
/// </summary>
public int DownloadWatchDogTime { private set; get; } = int.MaxValue;
public int DownloadWatchDogTime { private set; get; } = 0;
/// <summary>
/// 自定义参数:启用断点续传的最小尺寸
@@ -114,12 +129,12 @@ namespace YooAsset
/// <summary>
/// 自定义参数:解密服务接口的实例类
/// </summary>
public IDecryptionServices DecryptionServices { private set; get; }
public IBundleDecryptionServices BundleDecryptionServices { private set; get; }
/// <summary>
/// 自定义参数:资源清单服务类
/// </summary>
public IManifestRestoreServices ManifestServices { private set; get; }
public IManifestRestoreServices ManifestRestoreServices { private set; get; }
/// <summary>
/// 自定义参数:拷贝内置文件接口的实例类
@@ -136,17 +151,17 @@ namespace YooAsset
var operation = new DCFSInitializeOperation(this);
return operation;
}
public virtual FSLoadPackageManifestOperation LoadPackageManifestAsync(string packageVersion, int timeout)
public virtual FSRequestPackageVersionOperation RequestPackageVersionAsync(RequestPackageVersionOptions options)
{
var operation = new DCFSLoadPackageManifestOperation(this, packageVersion, timeout);
var operation = new DCFSRequestPackageVersionOperation(this, options.AppendTimeTicks, options.Timeout);
return operation;
}
public virtual FSRequestPackageVersionOperation RequestPackageVersionAsync(bool appendTimeTicks, int timeout)
public virtual FSLoadPackageManifestOperation LoadPackageManifestAsync(LoadPackageManifestOptions options)
{
var operation = new DCFSRequestPackageVersionOperation(this, appendTimeTicks, timeout);
var operation = new DCFSLoadPackageManifestOperation(this, options.PackageVersion, options.Timeout);
return operation;
}
public virtual FSClearCacheFilesOperation ClearCacheFilesAsync(PackageManifest manifest, ClearCacheFilesOptions options)
public virtual FSClearCacheFilesOperation ClearCacheFilesAsync(ClearCacheFilesOptions options)
{
if (options.ClearMode == EFileClearMode.ClearAllBundleFiles.ToString())
{
@@ -155,17 +170,17 @@ namespace YooAsset
}
else if (options.ClearMode == EFileClearMode.ClearUnusedBundleFiles.ToString())
{
var operation = new ClearUnusedCacheBundleFilesOperation(this, manifest);
var operation = new ClearUnusedCacheBundleFilesOperation(this, options.Manifest);
return operation;
}
else if (options.ClearMode == EFileClearMode.ClearBundleFilesByLocations.ToString())
{
var operation = new ClearCacheBundleFilesByLocationsOperaiton(this, manifest, options.ClearParam);
var operation = new ClearCacheBundleFilesByLocationsOperaiton(this, options.Manifest, options.ClearParam);
return operation;
}
else if (options.ClearMode == EFileClearMode.ClearBundleFilesByTags.ToString())
{
var operation = new ClearCacheBundleFilesByTagsOperaiton(this, manifest, options.ClearParam);
var operation = new ClearCacheBundleFilesByTagsOperaiton(this, options.Manifest, options.ClearParam);
return operation;
}
else if (options.ClearMode == EFileClearMode.ClearAllManifestFiles.ToString())
@@ -175,7 +190,7 @@ namespace YooAsset
}
else if (options.ClearMode == EFileClearMode.ClearUnusedManifestFiles.ToString())
{
var operation = new ClearUnusedCacheManifestFilesOperation(this, manifest);
var operation = new ClearUnusedCacheManifestFilesOperation(this, options.Manifest);
return operation;
}
else
@@ -185,9 +200,10 @@ namespace YooAsset
return operation;
}
}
public virtual FSDownloadFileOperation DownloadFileAsync(PackageBundle bundle, DownloadFileOptions options)
public virtual FSDownloadFileOperation DownloadFileAsync(DownloadFileOptions options)
{
// 获取下载地址
PackageBundle bundle = options.Bundle;
if (string.IsNullOrEmpty(options.ImportFilePath))
{
// 注意:如果是解压文件系统类,这里会返回本地内置文件的下载路径
@@ -202,17 +218,18 @@ namespace YooAsset
options.SetURL(mainURL, mainURL);
}
var downloader = new DownloadPackageBundleOperation(this, bundle, options);
var downloader = new DownloadPackageBundleOperation(this, options);
return downloader;
}
public virtual FSLoadBundleOperation LoadBundleFile(PackageBundle bundle)
public virtual FSLoadBundleOperation LoadBundleAsync(LoadBundleOptions options)
{
if (bundle.BundleType == (int)EBuildBundleType.AssetBundle)
PackageBundle bundle = options.Bundle;
if (bundle.BundleType == (int)EBundleType.AssetBundle)
{
var operation = new DCFSLoadAssetBundleOperation(this, bundle);
return operation;
}
else if (bundle.BundleType == (int)EBuildBundleType.RawBundle)
else if (bundle.BundleType == (int)EBundleType.RawBundle)
{
var operation = new DCFSLoadRawBundleOperation(this, bundle);
return operation;
@@ -227,7 +244,15 @@ namespace YooAsset
public virtual void SetParameter(string name, object value)
{
if (name == FileSystemParametersDefine.REMOTE_SERVICES)
if (name == FileSystemParametersDefine.DOWNLOAD_BACKEND)
{
DownloadBackend = (IDownloadBackend)value;
}
else if (name == FileSystemParametersDefine.UNITY_WEB_REQUEST_CREATOR)
{
WebRequestCreator = (UnityWebRequestCreator)value;
}
else if (name == FileSystemParametersDefine.REMOTE_SERVICES)
{
RemoteServices = (IRemoteServices)value;
}
@@ -242,7 +267,14 @@ namespace YooAsset
else if (name == FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY)
{
int convertValue = Convert.ToInt32(value);
FileVerifyMaxConcurrency = Mathf.Clamp(convertValue, 1, int.MaxValue);
if (convertValue > 256)
{
YooLogger.Warning($"FILE_VERIFY_MAX_CONCURRENCY value {convertValue} is too large, clamped to 256. Recommended range: 1 - 128.");
}
// 限制在合理范围内1-256
// 超过 256 的并发数对于文件验证来说没有意义,反而会增加线程池压力
FileVerifyMaxConcurrency = Mathf.Clamp(convertValue, 1, 256);
}
else if (name == FileSystemParametersDefine.APPEND_FILE_EXTENSION)
{
@@ -255,17 +287,27 @@ namespace YooAsset
else if (name == FileSystemParametersDefine.DOWNLOAD_MAX_CONCURRENCY)
{
int convertValue = Convert.ToInt32(value);
DownloadMaxConcurrency = Mathf.Clamp(convertValue, 1, int.MaxValue);
if (convertValue > 64)
{
YooLogger.Warning($"DOWNLOAD_MAX_CONCURRENCY value {convertValue} is too large, clamped to 64. Recommended range: 1 - 32.");
}
DownloadMaxConcurrency = Mathf.Clamp(convertValue, 1, 64);
}
else if (name == FileSystemParametersDefine.DOWNLOAD_MAX_REQUEST_PER_FRAME)
{
int convertValue = Convert.ToInt32(value);
DownloadMaxRequestPerFrame = Mathf.Clamp(convertValue, 1, int.MaxValue);
if (convertValue > 20)
{
YooLogger.Warning($"DOWNLOAD_MAX_REQUEST_PER_FRAME value {convertValue} is too large, clamped to 20. Recommended range: 1 - 10.");
}
DownloadMaxRequestPerFrame = Mathf.Clamp(convertValue, 1, 20);
}
else if (name == FileSystemParametersDefine.DOWNLOAD_WATCH_DOG_TIME)
{
int convertValue = Convert.ToInt32(value);
DownloadWatchDogTime = Mathf.Clamp(convertValue, 1, int.MaxValue);
DownloadWatchDogTime = Mathf.Clamp(convertValue, 0, int.MaxValue);
}
else if (name == FileSystemParametersDefine.RESUME_DOWNLOAD_MINMUM_SIZE)
{
@@ -275,13 +317,13 @@ namespace YooAsset
{
ResumeDownloadResponseCodes = (List<long>)value;
}
else if (name == FileSystemParametersDefine.DECRYPTION_SERVICES)
else if (name == FileSystemParametersDefine.BUNDLE_DECRYPTION_SERVICES)
{
DecryptionServices = (IDecryptionServices)value;
BundleDecryptionServices = (IBundleDecryptionServices)value;
}
else if (name == FileSystemParametersDefine.MANIFEST_SERVICES)
else if (name == FileSystemParametersDefine.MANIFEST_RESTORE_SERVICES)
{
ManifestServices = (IManifestRestoreServices)value;
ManifestRestoreServices = (IManifestRestoreServices)value;
}
else if (name == FileSystemParametersDefine.COPY_LOCAL_FILE_SERVICES)
{
@@ -304,13 +346,23 @@ namespace YooAsset
_cacheBundleFilesRoot = PathUtility.Combine(_packageRoot, DefaultCacheFileSystemDefine.BundleFilesFolderName);
_cacheManifestFilesRoot = PathUtility.Combine(_packageRoot, DefaultCacheFileSystemDefine.ManifestFilesFolderName);
_tempFilesRoot = PathUtility.Combine(_packageRoot, DefaultCacheFileSystemDefine.TempFilesFolderName);
// 创建默认的下载后台接口
if (DownloadBackend == null)
DownloadBackend = new UnityWebRequestBackend(WebRequestCreator);
}
public virtual void OnDestroy()
{
if (DownloadCenter != null)
if (DownloadScheduler != null)
{
DownloadCenter.AbortOperation();
DownloadCenter = null;
DownloadScheduler.Dispose();
DownloadScheduler = null;
}
if (DownloadBackend != null)
{
DownloadBackend.Dispose();
DownloadBackend = null;
}
}
@@ -353,20 +405,20 @@ namespace YooAsset
if (bundle.Encrypted)
{
if (DecryptionServices == null)
if (BundleDecryptionServices == null)
{
YooLogger.Error($"The {nameof(IDecryptionServices)} is null !");
YooLogger.Error($"The {nameof(IBundleDecryptionServices)} is null !");
return null;
}
string filePath = GetCacheBundleFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.ReadFileData(fileInfo);
return BundleDecryptionServices.ReadFileData(bundleInfo);
}
else
{
@@ -381,20 +433,20 @@ namespace YooAsset
if (bundle.Encrypted)
{
if (DecryptionServices == null)
if (BundleDecryptionServices == null)
{
YooLogger.Error($"The {nameof(IDecryptionServices)} is null !");
YooLogger.Error($"The {nameof(IBundleDecryptionServices)} is null !");
return null;
}
string filePath = GetCacheBundleFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.ReadFileText(fileInfo);
return BundleDecryptionServices.ReadFileText(bundleInfo);
}
else
{
@@ -476,7 +528,7 @@ namespace YooAsset
{
if (_records.ContainsKey(bundle.BundleGUID))
{
throw new Exception("Should never get here !");
throw new YooInternalException();
}
string infoFilePath = GetBundleInfoFilePath(bundle);
@@ -498,9 +550,9 @@ namespace YooAsset
// 写入文件信息
WriteBundleInfoFile(infoFilePath, bundle.FileCRC, bundle.FileSize);
}
catch (Exception e)
catch (Exception ex)
{
YooLogger.Error($"Failed to write cache file ! {e.Message}");
YooLogger.Error($"Failed to write cache file ! {ex.Message}");
return false;
}
@@ -599,47 +651,47 @@ namespace YooAsset
/// <summary>
/// 加载加密资源文件
/// </summary>
public DecryptResult LoadEncryptedAssetBundle(PackageBundle bundle)
public DecryptSyncResult LoadEncryptedBundleSync(PackageBundle bundle)
{
string filePath = GetCacheBundleFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.LoadAssetBundle(fileInfo);
return BundleDecryptionServices.LoadAssetBundleSync(bundleInfo);
}
/// <summary>
/// 加载加密资源文件
/// </summary>
public DecryptResult LoadEncryptedAssetBundleAsync(PackageBundle bundle)
public DecryptAsyncResult LoadEncryptedBundleAsync(PackageBundle bundle)
{
string filePath = GetCacheBundleFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.LoadAssetBundleAsync(fileInfo);
return BundleDecryptionServices.LoadAssetBundleAsync(bundleInfo);
}
/// <summary>
/// 加载加密资源文件
/// </summary>
public DecryptResult LoadEncryptedAssetBundleFallback(PackageBundle bundle)
public DecryptSyncResult LoadEncryptedBundleFallback(PackageBundle bundle)
{
string filePath = GetCacheBundleFileLoadPath(bundle);
var fileInfo = new DecryptFileInfo()
var bundleInfo = new DecryptBundleInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
};
return DecryptionServices.LoadAssetBundleFallback(fileInfo);
return BundleDecryptionServices.LoadAssetBundleFallback(bundleInfo);
}
#endregion
}
}
}

View File

@@ -37,9 +37,9 @@ namespace YooAsset
return false;
}
}
catch (Exception e)
catch (Exception ex)
{
YooLogger.Error($"Failed to delete cache file ! {e.Message}");
YooLogger.Error($"Failed to delete cache file ! {ex.Message}");
return false;
}
}

View File

@@ -33,9 +33,9 @@ namespace YooAsset
{
Directory.Delete(FileRootPath, true);
}
catch (System.Exception e)
catch (System.Exception ex)
{
YooLogger.Warning($"Failed to delete cache bundle folder : {e}");
YooLogger.Warning($"Failed to delete cache bundle folder : {ex}");
}
}
}

View File

@@ -9,7 +9,7 @@ namespace YooAsset
CheckAppFootPrint,
SearchCacheFiles,
VerifyCacheFiles,
CreateDownloadCenter,
CreateDownloadScheduler,
Done,
}
@@ -110,7 +110,7 @@ namespace YooAsset
if (_verifyCacheFilesOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.CreateDownloadCenter;
_steps = ESteps.CreateDownloadScheduler;
YooLogger.Log($"Package '{_fileSystem.PackageName}' '{_fileSystem.GetType().Name}' cached files count : {_fileSystem.FileCount}");
}
else
@@ -121,13 +121,13 @@ namespace YooAsset
}
}
if (_steps == ESteps.CreateDownloadCenter)
if (_steps == ESteps.CreateDownloadScheduler)
{
// 注意:下载中心作为独立任务运行!
if (_fileSystem.DownloadCenter == null)
if (_fileSystem.DownloadScheduler == null)
{
_fileSystem.DownloadCenter = new DownloadCenterOperation(_fileSystem);
OperationSystem.StartOperation(_fileSystem.PackageName, _fileSystem.DownloadCenter);
_fileSystem.DownloadScheduler = new DownloadSchedulerOperation(_fileSystem);
OperationSystem.StartOperation(_fileSystem.PackageName, _fileSystem.DownloadScheduler);
}
_steps = ESteps.Done;

View File

@@ -79,8 +79,8 @@ namespace YooAsset
{
if (_downloadFileOp == null)
{
DownloadFileOptions options = new DownloadFileOptions(int.MaxValue);
_downloadFileOp = _fileSystem.DownloadFileAsync(_bundle, options);
DownloadFileOptions options = new DownloadFileOptions(_bundle, int.MaxValue);
_downloadFileOp = _fileSystem.DownloadFileAsync(options);
_downloadFileOp.StartOperation();
AddChildOperation(_downloadFileOp);
}
@@ -127,11 +127,11 @@ namespace YooAsset
{
if (_bundle.Encrypted)
{
if (_fileSystem.DecryptionServices == null)
if (_fileSystem.BundleDecryptionServices == null)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"The {nameof(IDecryptionServices)} is null !";
Error = $"The {nameof(IBundleDecryptionServices)} is null !";
YooLogger.Error(Error);
return;
}
@@ -141,7 +141,7 @@ namespace YooAsset
{
if (_bundle.Encrypted)
{
var decryptResult = _fileSystem.LoadEncryptedAssetBundle(_bundle);
var decryptResult = _fileSystem.LoadEncryptedBundleSync(_bundle);
_assetBundle = decryptResult.Result;
_managedStream = decryptResult.ManagedStream;
}
@@ -155,7 +155,7 @@ namespace YooAsset
{
if (_bundle.Encrypted)
{
var decryptResult = _fileSystem.LoadEncryptedAssetBundleAsync(_bundle);
var decryptResult = _fileSystem.LoadEncryptedBundleAsync(_bundle);
_createRequest = decryptResult.CreateRequest;
_managedStream = decryptResult.ManagedStream;
}
@@ -202,7 +202,7 @@ namespace YooAsset
{
if (_bundle.Encrypted)
{
var decryptResult = _fileSystem.LoadEncryptedAssetBundleFallback(_bundle);
var decryptResult = _fileSystem.LoadEncryptedBundleFallback(_bundle);
_assetBundle = decryptResult.Result;
if (_assetBundle != null)
{
@@ -262,14 +262,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
@@ -320,11 +313,11 @@ namespace YooAsset
File.Move(recordFileElement.DataFilePath, filePath);
_steps = ESteps.LoadCacheRawBundle;
}
catch (Exception e)
catch (Exception ex)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"Faild rename raw data file : {e.Message}";
Error = $"Faild rename raw data file : {ex.Message}";
}
}
else
@@ -355,8 +348,8 @@ namespace YooAsset
{
if (_downloadFileOp == null)
{
DownloadFileOptions options = new DownloadFileOptions(int.MaxValue);
_downloadFileOp = _fileSystem.DownloadFileAsync(_bundle, options);
DownloadFileOptions options = new DownloadFileOptions(_bundle, int.MaxValue);
_downloadFileOp = _fileSystem.DownloadFileAsync(options);
_downloadFileOp.StartOperation();
AddChildOperation(_downloadFileOp);
}
@@ -419,14 +412,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -47,7 +47,7 @@ namespace YooAsset
string bundleGUID = _allBundleGUIDs[i];
_fileSystem.DeleteCacheBundleFile(bundleGUID);
_allBundleGUIDs.RemoveAt(i);
if (OperationSystem.IsBusy)
if (IsBusy)
break;
}

View File

@@ -101,7 +101,7 @@ namespace YooAsset
string bundleGUID = _clearBundleGUIDs[i];
_fileSystem.DeleteCacheBundleFile(bundleGUID);
_clearBundleGUIDs.RemoveAt(i);
if (OperationSystem.IsBusy)
if (IsBusy)
break;
}

View File

@@ -99,7 +99,7 @@ namespace YooAsset
string bundleGUID = _clearBundleGUIDs[i];
_fileSystem.DeleteCacheBundleFile(bundleGUID);
_clearBundleGUIDs.RemoveAt(i);
if (OperationSystem.IsBusy)
if (IsBusy)
break;
}

View File

@@ -64,7 +64,7 @@ namespace YooAsset
string bundleGUID = _unusedBundleGUIDs[i];
_fileSystem.DeleteCacheBundleFile(bundleGUID);
_unusedBundleGUIDs.RemoveAt(i);
if (OperationSystem.IsBusy)
if (IsBusy)
break;
}

View File

@@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
namespace YooAsset
{
internal class DownloadCenterOperation : AsyncOperationBase
{
private readonly DefaultCacheFileSystem _fileSystem;
protected readonly Dictionary<string, UnityDownloadFileOperation> _downloaders = new Dictionary<string, UnityDownloadFileOperation>(1000);
protected readonly List<string> _removeList = new List<string>(1000);
public DownloadCenterOperation(DefaultCacheFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
internal override void InternalStart()
{
}
internal override void InternalUpdate()
{
// 获取可移除的下载器集合
_removeList.Clear();
foreach (var valuePair in _downloaders)
{
var downloader = valuePair.Value;
downloader.UpdateOperation();
if (downloader.IsDone)
{
_removeList.Add(valuePair.Key);
continue;
}
// 注意:主动终止引用计数为零的下载任务
if (downloader.RefCount <= 0)
{
_removeList.Add(valuePair.Key);
downloader.AbortOperation();
continue;
}
}
// 移除下载器
foreach (var key in _removeList)
{
if (_downloaders.TryGetValue(key, out var downloader))
{
Childs.Remove(downloader);
_downloaders.Remove(key);
}
}
// 最大并发数检测
int processCount = GetProcessingOperationCount();
if (processCount != _downloaders.Count)
{
if (processCount < _fileSystem.DownloadMaxConcurrency)
{
int startCount = _fileSystem.DownloadMaxConcurrency - processCount;
if (startCount > _fileSystem.DownloadMaxRequestPerFrame)
startCount = _fileSystem.DownloadMaxRequestPerFrame;
foreach (var operationPair in _downloaders)
{
var operation = operationPair.Value;
if (operation.Status == EOperationStatus.None)
{
operation.StartOperation();
startCount--;
if (startCount <= 0)
break;
}
}
}
}
}
/// <summary>
/// 创建下载任务
/// </summary>
public UnityDownloadFileOperation DownloadFileAsync(PackageBundle bundle, string url)
{
// 查询旧的下载器
if (_downloaders.TryGetValue(bundle.BundleGUID, out var oldDownloader))
{
oldDownloader.Reference();
return oldDownloader;
}
// 创建新的下载器
UnityDownloadFileOperation newDownloader;
bool isRequestLocalFile = DownloadSystemHelper.IsRequestLocalFile(url);
if (isRequestLocalFile)
{
newDownloader = new UnityDownloadLocalFileOperation(_fileSystem, bundle, url);
AddChildOperation(newDownloader);
_downloaders.Add(bundle.BundleGUID, newDownloader);
}
else
{
if (bundle.FileSize >= _fileSystem.ResumeDownloadMinimumSize)
{
newDownloader = new UnityDownloadResumeFileOperation(_fileSystem, bundle, url);
AddChildOperation(newDownloader);
_downloaders.Add(bundle.BundleGUID, newDownloader);
}
else
{
newDownloader = new UnityDownloadNormalFileOperation(_fileSystem, bundle, url);
AddChildOperation(newDownloader);
_downloaders.Add(bundle.BundleGUID, newDownloader);
}
}
newDownloader.Reference();
return newDownloader;
}
/// <summary>
/// 获取正在进行中的下载器总数
/// </summary>
private int GetProcessingOperationCount()
{
int count = 0;
foreach (var operationPair in _downloaders)
{
var operation = operationPair.Value;
if (operation.Status != EOperationStatus.None)
count++;
}
return count;
}
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using UnityEngine;
using System.IO;
using UnityEngine;
namespace YooAsset
{
@@ -17,7 +18,7 @@ namespace YooAsset
// 下载参数
protected readonly DefaultCacheFileSystem _fileSystem;
protected readonly DownloadFileOptions _options;
private UnityDownloadFileOperation _unityDownloadFileOp;
private DownloadAndCacheFileOperation _downloadFileOp;
protected int _requestCount = 0;
protected float _tryAgainTimer = 0;
@@ -25,7 +26,7 @@ namespace YooAsset
private ESteps _steps = ESteps.None;
internal DownloadPackageBundleOperation(DefaultCacheFileSystem fileSystem, PackageBundle bundle, DownloadFileOptions options) : base(bundle)
internal DownloadPackageBundleOperation(DefaultCacheFileSystem fileSystem, DownloadFileOptions options) : base(options.Bundle)
{
_fileSystem = fileSystem;
_options = options;
@@ -67,7 +68,7 @@ namespace YooAsset
}
string url = GetRequestURL();
_unityDownloadFileOp = _fileSystem.DownloadCenter.DownloadFileAsync(Bundle, url);
_downloadFileOp = _fileSystem.DownloadScheduler.DownloadAndCacheFileAsync(Bundle, url);
_steps = ESteps.CheckRequest;
}
@@ -75,20 +76,16 @@ namespace YooAsset
if (_steps == ESteps.CheckRequest)
{
if (IsWaitForAsyncComplete)
_unityDownloadFileOp.WaitForAsyncComplete();
_downloadFileOp.WaitForAsyncComplete();
// 因为并发数量限制,下载器可能被挂起!
if (_unityDownloadFileOp.Status == EOperationStatus.None)
_downloadFileOp.UpdateOperation();
Progress = _downloadFileOp.Progress;
DownloadedBytes = _downloadFileOp.DownloadedBytes;
DownloadProgress = _downloadFileOp.DownloadProgress;
if (_downloadFileOp.IsDone == false)
return;
_unityDownloadFileOp.UpdateOperation();
Progress = _unityDownloadFileOp.Progress;
DownloadedBytes = _unityDownloadFileOp.DownloadedBytes;
DownloadProgress = _unityDownloadFileOp.DownloadProgress;
if (_unityDownloadFileOp.IsDone == false)
return;
if (_unityDownloadFileOp.Status == EOperationStatus.Succeed)
if (_downloadFileOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
@@ -98,13 +95,13 @@ namespace YooAsset
if (IsWaitForAsyncComplete == false && _failedTryAgain > 0)
{
_steps = ESteps.TryAgain;
YooLogger.Warning($"Failed download : {_unityDownloadFileOp.URL} Try again !");
YooLogger.Warning($"Failed download : {_downloadFileOp.URL} Try again !");
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = _unityDownloadFileOp.Error;
Error = _downloadFileOp.Error;
YooLogger.Error(Error);
}
}
@@ -127,23 +124,16 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
internal override void InternalAbort()
{
// 注意:取消下载任务的时候引用计数减一
if (_steps != ESteps.Done)
{
if (_unityDownloadFileOp != null)
if (_downloadFileOp != null)
{
_unityDownloadFileOp.Release();
_downloadFileOp.Release();
}
}
}

View File

@@ -15,7 +15,7 @@ namespace YooAsset
private readonly DefaultCacheFileSystem _fileSystem;
private readonly string _packageVersion;
private readonly int _timeout;
private UnityWebFileRequestOperation _webFileRequestOp;
private IDownloadFileRequest _webFileRequestOp;
private int _requestCount = 0;
private ESteps _steps = ESteps.None;
@@ -57,16 +57,16 @@ namespace YooAsset
string savePath = _fileSystem.GetCachePackageHashFilePath(_packageVersion);
string fileName = YooAssetSettingsData.GetPackageHashFileName(_fileSystem.PackageName, _packageVersion);
string webURL = GetWebRequestURL(fileName);
_webFileRequestOp = new UnityWebFileRequestOperation(webURL, savePath, _timeout);
_webFileRequestOp.StartOperation();
AddChildOperation(_webFileRequestOp);
int watchdogTime = _fileSystem.DownloadWatchDogTime;
var args = new DownloadFileRequestArgs(webURL, savePath, _timeout, watchdogTime);
_webFileRequestOp = _fileSystem.DownloadBackend.CreateFileRequest(args);
_webFileRequestOp.SendRequest();
}
_webFileRequestOp.UpdateOperation();
if (_webFileRequestOp.IsDone == false)
return;
if (_webFileRequestOp.Status == EOperationStatus.Succeed)
if (_webFileRequestOp.Status == EDownloadRequestStatus.Succeed)
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;

View File

@@ -15,7 +15,7 @@ namespace YooAsset
private readonly DefaultCacheFileSystem _fileSystem;
private readonly string _packageVersion;
private readonly int _timeout;
private UnityWebFileRequestOperation _webFileRequestOp;
private IDownloadFileRequest _webFileRequestOp;
private int _requestCount = 0;
private ESteps _steps = ESteps.None;
@@ -57,16 +57,16 @@ namespace YooAsset
string savePath = _fileSystem.GetCachePackageManifestFilePath(_packageVersion);
string fileName = YooAssetSettingsData.GetManifestBinaryFileName(_fileSystem.PackageName, _packageVersion);
string webURL = GetDownloadRequestURL(fileName);
_webFileRequestOp = new UnityWebFileRequestOperation(webURL, savePath, _timeout);
_webFileRequestOp.StartOperation();
AddChildOperation(_webFileRequestOp);
int watchdogTime = _fileSystem.DownloadWatchDogTime;
var args = new DownloadFileRequestArgs(webURL, savePath, _timeout, watchdogTime);
_webFileRequestOp = _fileSystem.DownloadBackend.CreateFileRequest(args);
_webFileRequestOp.SendRequest();
}
_webFileRequestOp.UpdateOperation();
if (_webFileRequestOp.IsDone == false)
return;
if (_webFileRequestOp.Status == EOperationStatus.Succeed)
if (_webFileRequestOp.Status == EDownloadRequestStatus.Succeed)
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;

View File

@@ -59,7 +59,7 @@ namespace YooAsset
if (_steps == ESteps.VerifyFileData)
{
if (ManifestTools.VerifyManifestData(_fileData, _packageHash))
if (PackageManifestTools.VerifyManifestData(_fileData, _packageHash))
{
_steps = ESteps.LoadManifest;
}
@@ -75,7 +75,7 @@ namespace YooAsset
{
if (_deserializer == null)
{
_deserializer = new DeserializeManifestOperation(_fileSystem.ManifestServices, _fileData);
_deserializer = new DeserializeManifestOperation(_fileSystem.ManifestRestoreServices, _fileData);
_deserializer.StartOperation();
AddChildOperation(_deserializer);
}

View File

@@ -13,7 +13,7 @@ namespace YooAsset
private readonly DefaultCacheFileSystem _fileSystem;
private readonly bool _appendTimeTicks;
private readonly int _timeout;
private UnityWebTextRequestOperation _webTextRequestOp;
private IDownloadTextRequest _webTextRequestOp;
private int _requestCount = 0;
private ESteps _steps = ESteps.None;
@@ -45,17 +45,17 @@ namespace YooAsset
{
string fileName = YooAssetSettingsData.GetPackageVersionFileName(_fileSystem.PackageName);
string url = GetWebRequestURL(fileName);
_webTextRequestOp = new UnityWebTextRequestOperation(url, _timeout);
_webTextRequestOp.StartOperation();
AddChildOperation(_webTextRequestOp);
int watchDogTime = _fileSystem.DownloadWatchDogTime;
var args = new DownloadDataRequestArgs(url, _timeout, watchDogTime);
_webTextRequestOp = _fileSystem.DownloadBackend.CreateTextRequest(args);
_webTextRequestOp.SendRequest();
}
_webTextRequestOp.UpdateOperation();
Progress = _webTextRequestOp.Progress;
Progress = _webTextRequestOp.DownloadProgress;
if (_webTextRequestOp.IsDone == false)
return;
if (_webTextRequestOp.Status == EOperationStatus.Succeed)
if (_webTextRequestOp.Status == EDownloadRequestStatus.Succeed)
{
PackageVersion = _webTextRequestOp.Result;
if (string.IsNullOrEmpty(PackageVersion))

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: a8f060786f8775b4a82cc3f55d9135e2
guid: bf9991076b60f0f459846f54b0ca6698
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,51 @@

namespace YooAsset
{
internal abstract class DownloadAndCacheFileOperation : AsyncOperationBase
{
/// <summary>
/// 引用计数
/// </summary>
public int RefCount { private set; get; }
/// <summary>
/// 下载地址
/// </summary>
public readonly string URL;
/// <summary>
/// 下载进度
/// </summary>
public float DownloadProgress { get; protected set; }
/// <summary>
/// 下载字节
/// </summary>
public long DownloadedBytes { get; protected set; }
public DownloadAndCacheFileOperation(string url)
{
URL = url;
}
internal override string InternalGetDesc()
{
return $"RefCount : {RefCount}";
}
/// <summary>
/// 减少引用计数
/// </summary>
public void Release()
{
RefCount--;
}
/// <summary>
/// 增加引用计数
/// </summary>
public void Reference()
{
RefCount++;
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More