YooAsset源码阅读-资源加载与卸载

YooAsset源码阅读-资源加载卸载

继续 YooAsset 的源码研究,本文将详细分析资源加载系统的核心机制,包括ResourcePackage、ResourceManager以及各种资源加载Provider的工作原理。

资源加载系统架构概述

YooAsset的资源加载系统采用分层架构设计,主要包含以下核心组件:

核心组件关系图

graph TB
    A[ResourcePackage资源包裹入口] --> B[ResourceManager资源管理器]
    B --> C[ProviderOperation提供者基类]
    C --> D[AssetProvider单资源提供者]
    C --> E[SubAssetsProvider子资源提供者]
    C --> F[AllAssetsProvider所有资源提供者]
    C --> G[RawFileProvider原生文件提供者]
    C --> H[SceneProvider场景提供者]
    
    B --> I[LoadBundleFileOperation Bundle加载器]
    I --> J[BundleInfo Bundle信息]
    J --> K[FileSystem文件系统]
    
    D --> L[AssetHandle资源句柄]
    E --> M[SubAssetsHandle子资源句柄]
    F --> N[AllAssetsHandle所有资源句柄]
    G --> O[RawFileHandle原生文件句柄]
    H --> P[SceneHandle场景句柄]

    %% 暗色主题样式
    style A fill:#ff6b6b,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style B fill:#4ecdc4,stroke:#ffffff,stroke-width:3px,color:#000000
    style C fill:#45b7d1,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style D fill:#f9ca24,stroke:#ffffff,stroke-width:3px,color:#000000
    style E fill:#6c5ce7,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style F fill:#a29bfe,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style G fill:#fd79a8,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style H fill:#00d2d3,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style I fill:#ff9ff3,stroke:#ffffff,stroke-width:3px,color:#000000
    style J fill:#00aaff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style K fill:#ff3838,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style L fill:#2ed573,stroke:#ffffff,stroke-width:3px,color:#000000
    style M fill:#ffa502,stroke:#ffffff,stroke-width:3px,color:#000000
    style N fill:#74b9ff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style O fill:#e17055,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style P fill:#81ecec,stroke:#ffffff,stroke-width:3px,color:#000000

ResourcePackage - 资源包裹入口

ResourcePackage 是用户接触资源加载的主要入口点,它提供了统一的资源加载接口。

关键类

  • ResourcePackage.cs (Line 8-1209)
  • ResourceManager.cs
  • AssetInfo.cs

核心加载方法分析

ResourcePackage 提供了四种主要的资源加载方式:

1. LoadAssetAsync - 单资源加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ResourcePackage.cs Line 689-732
public AssetHandle LoadAssetAsync(AssetInfo assetInfo, uint priority = 0)
{
DebugCheckInitialize();
return LoadAssetInternal(assetInfo, false, priority);
}

private AssetHandle LoadAssetInternal(AssetInfo assetInfo, bool waitForAsyncComplete, uint priority)
{
DebugCheckAssetLoadType(assetInfo.AssetType);
assetInfo.LoadMethod = AssetInfo.ELoadMethod.LoadAsset;
var handle = _resourceManager.LoadAssetAsync(assetInfo, priority);
if (waitForAsyncComplete)
handle.WaitForAsyncComplete();
return handle;
}

2. LoadSubAssetsAsync - 子资源加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ResourcePackage.cs Line 799-853
public SubAssetsHandle LoadSubAssetsAsync(AssetInfo assetInfo, uint priority = 0)
{
DebugCheckInitialize();
return LoadSubAssetsInternal(assetInfo, false, priority);
}

private SubAssetsHandle LoadSubAssetsInternal(AssetInfo assetInfo, bool waitForAsyncComplete, uint priority)
{
DebugCheckAssetLoadType(assetInfo.AssetType);
assetInfo.LoadMethod = AssetInfo.ELoadMethod.LoadSubAssets;
var handle = _resourceManager.LoadSubAssetsAsync(assetInfo, priority);
if (waitForAsyncComplete)
handle.WaitForAsyncComplete();
return handle;
}

3. LoadAllAssetsAsync - 加载包内所有资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ResourcePackage.cs Line 909-963
public AllAssetsHandle LoadAllAssetsAsync(AssetInfo assetInfo, uint priority = 0)
{
DebugCheckInitialize();
return LoadAllAssetsInternal(assetInfo, false, priority);
}

private AllAssetsHandle LoadAllAssetsInternal(AssetInfo assetInfo, bool waitForAsyncComplete, uint priority)
{
DebugCheckAssetLoadType(assetInfo.AssetType);
assetInfo.LoadMethod = AssetInfo.ELoadMethod.LoadAllAssets;
var handle = _resourceManager.LoadAllAssetsAsync(assetInfo, priority);
if (waitForAsyncComplete)
handle.WaitForAsyncComplete();
return handle;
}

4. LoadRawFileAsync - 原生文件加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ResourcePackage.cs Line 540-566
public RawFileHandle LoadRawFileAsync(AssetInfo assetInfo, uint priority = 0)
{
DebugCheckInitialize();
return LoadRawFileInternal(assetInfo, false, priority);
}

private RawFileHandle LoadRawFileInternal(AssetInfo assetInfo, bool waitForAsyncComplete, uint priority)
{
assetInfo.LoadMethod = AssetInfo.ELoadMethod.LoadRawFile;
var handle = _resourceManager.LoadRawFileAsync(assetInfo, priority);
if (waitForAsyncComplete)
handle.WaitForAsyncComplete();
return handle;
}

资源加载方法对比

加载方法 用途 返回类型 典型场景
LoadAssetAsync 加载单个主资源 AssetHandle 纹理、音频、Prefab等
LoadSubAssetsAsync 加载子资源 SubAssetsHandle Atlas中的Sprite、FBX中的网格等
LoadAllAssetsAsync 加载包内所有资源 AllAssetsHandle 批量加载整个资源包
LoadRawFileAsync 加载原生文件 RawFileHandle 配置文件、二进制数据等

ResourceManager - 资源管理核心

ResourceManager 是资源加载系统的核心管理器,负责Provider的创建、管理和Bundle加载器的协调。

关键数据结构

1
2
3
4
5
6
7
8
9
// ResourceManager.cs Line 12-21
internal readonly Dictionary<string, ProviderOperation> ProviderDic = new Dictionary<string, ProviderOperation>(5000);
internal readonly Dictionary<string, LoadBundleFileOperation> LoaderDic = new Dictionary<string, LoadBundleFileOperation>(5000);
internal readonly List<SceneHandle> SceneHandles = new List<SceneHandle>(100);

// Bundle加载的最大并发数
private int _bundleLoadingMaxConcurrency;
// WebGL平台强制同步加载资源
private bool _webGLForceSyncLoadAsset;

Provider创建机制

ResourceManager为每种资源类型创建对应的Provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ResourceManager.cs Line 157-187 (AssetProvider创建)
public AssetHandle LoadAssetAsync(AssetInfo assetInfo, uint priority)
{
// 检查加载操作是否被锁定
if (LockLoadOperation) { /* 错误处理 */ }

// 检查AssetInfo有效性
if (assetInfo.IsInvalid) { /* 错误处理 */ }

// 生成Provider的唯一标识符
string providerGUID = nameof(LoadAssetAsync) + assetInfo.GUID;

// 尝试获取已存在的Provider(复用机制)
ProviderOperation provider = TryGetAssetProvider(providerGUID);
if (provider == null)
{
// 创建新的AssetProvider
provider = new AssetProvider(this, providerGUID, assetInfo);
provider.InitProviderDebugInfo();
ProviderDic.Add(providerGUID, provider);
OperationSystem.StartOperation(PackageName, provider);
}

provider.Priority = priority;
return provider.CreateHandle<AssetHandle>();
}

Bundle加载器管理

ResourceManager 通过Bundle加载器管理资源包的加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ResourceManager.cs Line 298-313
internal LoadBundleFileOperation CreateMainBundleFileLoader(AssetInfo assetInfo)
{
BundleInfo bundleInfo = _bundleQuery.GetMainBundleInfo(assetInfo);
return CreateBundleFileLoaderInternal(bundleInfo);
}

internal List<LoadBundleFileOperation> CreateDependBundleFileLoaders(AssetInfo assetInfo)
{
List<BundleInfo> bundleInfos = _bundleQuery.GetDependBundleInfos(assetInfo);
List<LoadBundleFileOperation> result = new List<LoadBundleFileOperation>(bundleInfos.Count);
foreach (var bundleInfo in bundleInfos)
{
var bundleLoader = CreateBundleFileLoaderInternal(bundleInfo);
result.Add(bundleLoader);
}
return result;
}

Provider系统 - 资源提供者

Provider是资源加载的具体执行者,每种资源类型都有对应的Provider实现。

AssetProvider - 单资源提供者

AssetProvider 负责加载单个资源对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// AssetProvider.cs Line 11-42
protected override void ProcessBundleResult()
{
if (_loadAssetOp == null)
{
// 通过Bundle结果对象加载资源
_loadAssetOp = BundleResultObject.LoadAssetAsync(MainAssetInfo);
_loadAssetOp.StartOperation();
AddChildOperation(_loadAssetOp);

#if UNITY_WEBGL
// WebGL平台强制同步加载
if (_resManager.WebGLForceSyncLoadAsset())
_loadAssetOp.WaitForAsyncComplete();
#endif
}

if (IsWaitForAsyncComplete)
_loadAssetOp.WaitForAsyncComplete();

_loadAssetOp.UpdateOperation();
Progress = _loadAssetOp.Progress;
if (_loadAssetOp.IsDone == false)
return;

if (_loadAssetOp.Status != EOperationStatus.Succeed)
{
InvokeCompletion(_loadAssetOp.Error, EOperationStatus.Failed);
}
else
{
AssetObject = _loadAssetOp.Result;
InvokeCompletion(string.Empty, EOperationStatus.Succeed);
}
}

Handle系统 - 资源句柄

Handle是用户与加载资源交互的接口,提供了丰富的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// AssetHandle.cs 关键功能
public sealed class AssetHandle : HandleBase
{
// 获取资源对象
public UnityEngine.Object AssetObject { get; }

// 泛型获取资源对象
public TAsset GetAssetObject<TAsset>() where TAsset : UnityEngine.Object

// 同步实例化GameObject
public GameObject InstantiateSync()

// 异步实例化GameObject
public InstantiateOperation InstantiateAsync(bool actived = true)

// 等待异步完成
public void WaitForAsyncComplete()

// 完成事件
public event System.Action<AssetHandle> Completed
}

资源加载完整流程

从用户调用到资源加载完成的调用链

graph TD
    A[用户代码] -->|package.LoadAssetAsync| B[ResourcePackage.LoadAssetAsync]
    B -->|LoadAssetInternal| C[ResourceManager.LoadAssetAsync]
    C -->|创建或复用| D[AssetProvider]
    D -->|OperationSystem.StartOperation| E[Provider.InternalUpdate]
    E -->|检查Bundle状态| F[ProcessBundleResult]
    F -->|BundleResultObject.LoadAssetAsync| G[FSLoadAssetOperation]
    G -->|Unity AssetBundle.LoadAssetAsync| H[UnityEngine.AssetBundle]
    H -->|加载完成| I[AssetProvider.AssetObject]
    I -->|CreateHandle| J[AssetHandle]
    J -->|返回给用户| K[用户获得资源]

    %% 详细说明
    L[关键步骤说明]
    L --> M[ResourcePackage统一入口]
    L --> N[ResourceManager管理Provider]
    L --> O[AssetProvider执行具体加载]
    L --> P[Handle提供用户接口]

    %% 暗色主题样式
    style A fill:#ff6b6b,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style B fill:#4ecdc4,stroke:#ffffff,stroke-width:3px,color:#000000
    style C fill:#45b7d1,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style D fill:#f9ca24,stroke:#ffffff,stroke-width:3px,color:#000000
    style E fill:#6c5ce7,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style F fill:#a29bfe,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style G fill:#fd79a8,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style H fill:#00d2d3,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style I fill:#ff9ff3,stroke:#ffffff,stroke-width:3px,color:#000000
    style J fill:#00aaff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style K fill:#ff3838,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style L fill:#2ed573,stroke:#ffffff,stroke-width:3px,color:#000000
    style M fill:#ffa502,stroke:#ffffff,stroke-width:3px,color:#000000
    style N fill:#74b9ff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style O fill:#e17055,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style P fill:#81ecec,stroke:#ffffff,stroke-width:3px,color:#000000

详细调用步骤解析

1. 用户入口 - ResourcePackage

1
2
// 用户代码
var handle = package.LoadAssetAsync<Texture2D>("UITextures/Background");

2. ResourcePackage统一处理

1
2
3
4
5
6
7
// ResourcePackage.cs
public AssetHandle LoadAssetAsync<TObject>(string location, uint priority = 0)
{
DebugCheckInitialize();
AssetInfo assetInfo = ConvertLocationToAssetInfo(location, typeof(TObject));
return LoadAssetInternal(assetInfo, false, priority);
}

3. ResourceManager创建Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
// ResourceManager.cs
public AssetHandle LoadAssetAsync(AssetInfo assetInfo, uint priority)
{
string providerGUID = nameof(LoadAssetAsync) + assetInfo.GUID;
ProviderOperation provider = TryGetAssetProvider(providerGUID);
if (provider == null)
{
provider = new AssetProvider(this, providerGUID, assetInfo);
ProviderDic.Add(providerGUID, provider);
OperationSystem.StartOperation(PackageName, provider);
}
return provider.CreateHandle<AssetHandle>();
}

4. AssetProvider执行加载

1
2
3
4
5
6
7
// AssetProvider.cs
protected override void ProcessBundleResult()
{
_loadAssetOp = BundleResultObject.LoadAssetAsync(MainAssetInfo);
// ... 处理加载结果
AssetObject = _loadAssetOp.Result;
}

场景加载特殊处理

场景加载与普通资源加载有所不同,每次加载都会创建新的Provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ResourceManager.cs Line 118-151
public SceneHandle LoadSceneAsync(AssetInfo assetInfo, LoadSceneParameters loadSceneParams, bool suspendLoad, uint priority)
{
// 场景的ProviderGUID每次加载都会变化(支持同场景多次加载)
string providerGUID = $"{assetInfo.GUID}-{++_sceneCreateIndex}";
ProviderOperation provider;
{
provider = new SceneProvider(this, providerGUID, assetInfo, loadSceneParams, suspendLoad);
provider.InitProviderDebugInfo();
ProviderDic.Add(providerGUID, provider);
OperationSystem.StartOperation(PackageName, provider);
}

provider.Priority = priority;
var handle = provider.CreateHandle<SceneHandle>();
handle.PackageName = PackageName;
SceneHandles.Add(handle);
return handle;
}

场景加载流程图

graph TD
    A[LoadSceneAsync] -->|生成唯一GUID| B[创建SceneProvider]
    B -->|不复用Provider| C[每次都是新Provider]
    C -->|LoadSceneParameters| D[Unity SceneManager]
    D -->|支持暂停加载| E[suspendLoad=true]
    D -->|正常加载| F[suspendLoad=false]
    E --> G[加载到90%暂停]
    F --> H[完整加载场景]
    G --> I[用户调用ActivateScene]
    H --> J[SceneHandle返回]
    I --> J

    %% 暗色主题样式
    style A fill:#ff6b6b,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style B fill:#4ecdc4,stroke:#ffffff,stroke-width:3px,color:#000000
    style C fill:#45b7d1,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style D fill:#f9ca24,stroke:#ffffff,stroke-width:3px,color:#000000
    style E fill:#6c5ce7,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style F fill:#a29bfe,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style G fill:#fd79a8,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style H fill:#00d2d3,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style I fill:#ff9ff3,stroke:#ffffff,stroke-width:3px,color:#000000
    style J fill:#00aaff,stroke:#ffffff,stroke-width:3px,color:#ffffff

AssetInfo转换和查询机制

Location到AssetInfo的转换

1
2
3
4
5
6
7
8
9
10
// ResourcePackage.cs Line 1153-1160
private AssetInfo ConvertLocationToAssetInfo(string location, System.Type assetType)
{
return _playModeImpl.ActiveManifest.ConvertLocationToAssetInfo(location, assetType);
}

private AssetInfo ConvertAssetGUIDToAssetInfo(string assetGUID, System.Type assetType)
{
return _playModeImpl.ActiveManifest.ConvertAssetGUIDToAssetInfo(assetGUID, assetType);
}

AssetInfo包含的关键信息

1
2
3
4
5
6
7
8
9
10
public class AssetInfo
{
public string AssetPath; // 资源路径
public string GUID; // 资源GUID
public System.Type AssetType; // 资源类型
public PackageAsset Asset; // 包资源信息
public ELoadMethod LoadMethod; // 加载方法
public bool IsInvalid; // 是否无效
public string Error; // 错误信息
}

资源回收和缓存机制

资源回收策略

YooAsset提供了多种资源回收方式:

1. 强制回收所有资源

1
2
3
4
5
6
7
8
// ResourcePackage.cs Line 332-347
public UnloadAllAssetsOperation UnloadAllAssetsAsync(UnloadAllAssetsOptions options)
{
DebugCheckInitialize();
var operation = new UnloadAllAssetsOperation(_resourceManager, options);
OperationSystem.StartOperation(PackageName, operation);
return operation;
}

2. 回收未使用的资源

1
2
3
4
5
6
7
8
// ResourcePackage.cs Line 355-361
public UnloadUnusedAssetsOperation UnloadUnusedAssetsAsync(int loopCount = 10)
{
DebugCheckInitialize();
var operation = new UnloadUnusedAssetsOperation(_resourceManager, loopCount);
OperationSystem.StartOperation(PackageName, operation);
return operation;
}

3. 尝试卸载指定资源

1
2
3
4
5
6
// ResourcePackage.cs Line 367-382
public void TryUnloadUnusedAsset(AssetInfo assetInfo, int loopCount = 10)
{
DebugCheckInitialize();
_resourceManager.TryUnloadUnusedAsset(assetInfo, loopCount);
}

资源回收机制详解

ResourceManager中的回收逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ResourceManager.cs Line 66-111
public void TryUnloadUnusedAsset(AssetInfo assetInfo, int loopCount)
{
while (loopCount > 0)
{
loopCount--;

// 卸载主资源包加载器
string mainBundleName = _bundleQuery.GetMainBundleName(assetInfo.Asset.BundleID);
var mainLoader = TryGetBundleFileLoader(mainBundleName);
if (mainLoader != null)
{
mainLoader.TryDestroyProviders();
if (mainLoader.CanDestroyLoader())
{
mainLoader.DestroyLoader();
LoaderDic.Remove(mainBundleName);
}
}

// 卸载依赖资源包加载器
foreach (var dependID in assetInfo.Asset.DependBundleIDs)
{
string dependBundleName = _bundleQuery.GetMainBundleName(dependID);
var dependLoader = TryGetBundleFileLoader(dependBundleName);
if (dependLoader != null)
{
if (dependLoader.CanDestroyLoader())
{
dependLoader.DestroyLoader();
LoaderDic.Remove(dependBundleName);
}
}
}
}
}

性能优化特性

1. Provider复用机制

ResourceManager 通过 ProviderGUID 实现Provider复用,避免重复加载:

1
2
3
4
5
6
7
string providerGUID = nameof(LoadAssetAsync) + assetInfo.GUID;
ProviderOperation provider = TryGetAssetProvider(providerGUID);
if (provider == null)
{
// 只有不存在时才创建新的Provider
provider = new AssetProvider(this, providerGUID, assetInfo);
}

2. Bundle加载并发控制

1
2
3
4
5
6
7
8
// ResourceManager.cs
private int _bundleLoadingMaxConcurrency; // 最大并发数
public int BundleLoadingCounter = 0; // 当前加载计数

internal bool BundleLoadingIsBusy()
{
return BundleLoadingCounter >= _bundleLoadingMaxConcurrency;
}

3. WebGL平台同步加载优化

1
2
3
4
#if UNITY_WEBGL
if (_resManager.WebGLForceSyncLoadAsset())
_loadAssetOp.WaitForAsyncComplete();
#endif

4. 场景自动清理机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ResourceManager.cs Line 369-387
private void OnSceneUnloaded(Scene scene)
{
List<SceneHandle> removeList = new List<SceneHandle>();
foreach (var sceneHandle in SceneHandles)
{
if (sceneHandle.IsValid && sceneHandle.SceneObject == scene)
{
sceneHandle.Release();
removeList.Add(sceneHandle);
}
}
foreach (var sceneHandle in removeList)
{
SceneHandles.Remove(sceneHandle);
}
}

错误处理和调试支持

1. 初始化状态检查

1
2
3
4
5
6
7
8
9
10
11
[Conditional("DEBUG")]
private void DebugCheckInitialize(bool checkActiveManifest = true)
{
if (_initializeStatus == EOperationStatus.None)
throw new Exception("Package initialize not completed !");
else if (_initializeStatus == EOperationStatus.Failed)
throw new Exception($"Package initialize failed ! {_initializeError}");

if (checkActiveManifest && _playModeImpl.ActiveManifest == null)
throw new Exception("Can not found active package manifest !");
}

2. 资源类型有效性检查

1
2
3
4
5
6
7
8
9
10
11
[Conditional("DEBUG")]
private void DebugCheckAssetLoadType(System.Type type)
{
if (type == null) return;

if (typeof(UnityEngine.Behaviour).IsAssignableFrom(type))
throw new Exception($"Load asset type is invalid : {type.FullName} !");

if (typeof(UnityEngine.Object).IsAssignableFrom(type) == false)
throw new Exception($"Load asset type is invalid : {type.FullName} !");
}

3. 调试信息支持

ResourceManager 提供了丰富的调试接口:

1
2
3
4
// ResourceManager.cs Line 390-433
internal List<DebugProviderInfo> GetDebugProviderInfos()
internal List<DebugBundleInfo> GetDebugBundleInfos()
internal DebugPackageData GetDebugPackageData()

资源未下载时的按需下载机制

YooAsset的一个重要特性是按需下载(On-Demand Download)机制。当用户加载资源时发现资源尚未下载到本地,系统会根据配置自动进行下载,整个过程对用户透明。

资源下载需求检查

在资源加载前,可以通过以下接口检查资源是否需要下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ResourcePackage.cs Line 390-410
public bool IsNeedDownloadFromRemote(string location)
{
DebugCheckInitialize();
AssetInfo assetInfo = ConvertLocationToAssetInfo(location, null);
return IsNeedDownloadFromRemoteInternal(assetInfo);
}

private bool IsNeedDownloadFromRemoteInternal(AssetInfo assetInfo)
{
if (assetInfo.IsInvalid)
{
YooLogger.Warning(assetInfo.Error);
return false;
}

// 检查主Bundle是否需要下载
BundleInfo bundleInfo = _bundleQuery.GetMainBundleInfo(assetInfo);
if (bundleInfo.IsNeedDownloadFromRemote())
return true;

// 检查依赖Bundle是否需要下载
List<BundleInfo> depends = _bundleQuery.GetDependBundleInfos(assetInfo);
foreach (var depend in depends)
{
if (depend.IsNeedDownloadFromRemote())
return true;
}

return false;
}

按需下载的核心逻辑

当资源加载时发现文件不存在,DCFSLoadAssetBundleOperation会自动处理下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// DCFSLoadAssetBundleOperation.cs关键代码段
if (_steps == ESteps.CheckExist)
{
if (_fileSystem.Exists(_bundle))
{
// 文件已存在,直接加载
DownloadProgress = 1f;
DownloadedBytes = _bundle.FileSize;
_steps = ESteps.LoadAssetBundle;
}
else
{
// 文件不存在,检查是否允许按需下载
if (_fileSystem.DisableOnDemandDownload)
{
// 禁用按需下载,直接失败
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"The bundle not cached : {_bundle.BundleName}";
YooLogger.Warning(Error);
}
else
{
// 允许按需下载,转入下载步骤
_steps = ESteps.DownloadFile;
}
}
}

if (_steps == ESteps.DownloadFile)
{
if (_downloadFileOp == null)
{
// 创建下载操作
DownloadFileOptions options = new DownloadFileOptions(int.MaxValue);
_downloadFileOp = _fileSystem.DownloadFileAsync(_bundle, options);
_downloadFileOp.StartOperation();
AddChildOperation(_downloadFileOp);
}

// 更新下载进度
_downloadFileOp.UpdateOperation();
DownloadProgress = _downloadFileOp.DownloadProgress;
DownloadedBytes = _downloadFileOp.DownloadedBytes;

if (_downloadFileOp.IsDone)
{
if (_downloadFileOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.LoadAssetBundle; // 下载成功,继续加载
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = _downloadFileOp.Error; // 下载失败
}
}
}

按需下载流程图

graph TD
    A[开始加载资源] --> B[CheckExist检查文件]
    B --> C{文件是否存在?}
    C -->|存在| D[LoadAssetBundle直接加载]
    C -->|不存在| E{是否禁用按需下载?}
    E -->|是| F[加载失败返回错误]
    E -->|否| G[DownloadFile开始下载]
    G --> H[创建下载操作]
    H --> I[更新下载进度]
    I --> J{下载是否完成?}
    J -->|否| I
    J -->|是| K{下载是否成功?}
    K -->|成功| D
    K -->|失败| L[加载失败返回下载错误]
    D --> M[CheckResult检查加载结果]
    M --> N[加载完成]
    F --> O[结束]
    L --> O
    N --> O

    %% 暗色主题样式
    style A fill:#ff6b6b,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style B fill:#4ecdc4,stroke:#ffffff,stroke-width:3px,color:#000000
    style C fill:#f9ca24,stroke:#ffffff,stroke-width:3px,color:#000000
    style D fill:#6c5ce7,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style E fill:#a29bfe,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style F fill:#fd79a8,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style G fill:#00d2d3,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style H fill:#ff9ff3,stroke:#ffffff,stroke-width:3px,color:#000000
    style I fill:#00aaff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style J fill:#ff3838,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style K fill:#2ed573,stroke:#ffffff,stroke-width:3px,color:#000000
    style L fill:#ffa502,stroke:#ffffff,stroke-width:3px,color:#000000
    style M fill:#74b9ff,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style N fill:#e17055,stroke:#ffffff,stroke-width:3px,color:#ffffff
    style O fill:#81ecec,stroke:#ffffff,stroke-width:3px,color:#000000

配置控制选项

YooAsset提供了DisableOnDemandDownload配置项来控制按需下载行为:

1
2
3
4
5
6
// 在初始化参数中配置
var initParameters = new HostPlayModeParameters()
{
// 其他配置...
DisableOnDemandDownload = false // 允许按需下载(默认)
};

配置选项对比:

场景 DisableOnDemandDownload 行为 适用情况
在线游戏 false 自动下载未缓存资源 网络环境良好,允许动态下载
离线模式 true 只加载已缓存资源 网络受限或要求完全离线运行
预下载验证 true 确保资源完整性 发布前验证所有资源已就绪

用户代码的最佳实践

1. 预检查模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 检查资源是否需要下载
bool needDownload = package.IsNeedDownloadFromRemote("UI/MainPanel");
if (needDownload)
{
// 提示用户资源需要下载
ShowDownloadTip("资源需要下载,是否继续?");

// 创建专门的下载器进行下载
var downloader = package.CreateBundleDownloader("UI/MainPanel", 3, 3);
downloader.BeginDownload();

// 等待下载完成后再加载
yield return downloader;
if (downloader.Status == EOperationStatus.Succeed)
{
var handle = package.LoadAssetAsync<GameObject>("UI/MainPanel");
yield return handle;
}
}
else
{
// 资源已存在,直接加载
var handle = package.LoadAssetAsync<GameObject>("UI/MainPanel");
yield return handle;
}

2. 透明加载模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 直接加载,系统会自动处理未下载的情况
var handle = package.LoadAssetAsync<GameObject>("UI/MainPanel");

// 监听加载进度(包含下载进度)
while (!handle.IsDone)
{
// 这里的进度包含了可能的下载进度
Debug.Log($"Loading Progress: {handle.Progress}");
yield return null;
}

if (handle.Status == EOperationStatus.Succeed)
{
var gameObject = handle.InstantiateSync();
// 使用资源...
}
else
{
Debug.LogError($"Failed to load asset: {handle.Error}");
}

3. 批量预下载模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 在适当的时机(如场景加载前)批量下载
public IEnumerator PredownloadSceneAssets(string sceneTag)
{
// 创建基于Tag的下载器
var downloader = package.CreateResourceDownloader(sceneTag, 5, 3);

if (downloader.TotalDownloadCount > 0)
{
// 显示下载UI
ShowDownloadProgress();

downloader.BeginDownload();
while (!downloader.IsDone)
{
// 更新下载进度UI
UpdateDownloadProgress(downloader.Progress,
downloader.DownloadedBytes,
downloader.TotalDownloadBytes);
yield return null;
}

// 隐藏下载UI
HideDownloadProgress();

if (downloader.Status != EOperationStatus.Succeed)
{
Debug.LogError("Predownload failed!");
yield break;
}
}

// 现在可以直接加载场景资源,无需担心下载问题
LoadSceneAssets();
}

总结

YooAsset的资源加载系统采用了高度模块化的设计:

  1. 分层架构:ResourcePackage → ResourceManager → Provider → Handle 的清晰分层
  2. 类型安全:通过泛型和AssetInfo确保类型安全
  3. 按需下载:透明的按需下载机制,支持在线和离线模式
  4. 性能优化:Provider复用、并发控制、平台特定优化
  5. 错误处理:完善的验证和错误处理机制
  6. 调试支持:丰富的调试接口和状态监控
  7. 灵活配置:支持多种下载策略和配置模式

按需下载机制的核心特性:

  • 透明性:用户调用加载接口时,系统自动检查并处理未下载资源
  • 可配置性:通过DisableOnDemandDownload控制按需下载行为
  • 状态监控:提供完整的下载进度和状态反馈
  • 错误处理:详细的错误信息和失败处理机制
  • 依赖处理:自动处理资源依赖的下载需求

这种设计既保证了高性能,又提供了良好的可维护性和扩展性,同时通过按需下载机制实现了在线和离线模式的完美结合,是一个优秀的资源管理系统实现。


YooAsset源码阅读-资源加载与卸载
https://lshgame.com/2025/08/25/YooAsset_Code_Reading_Resource_Loading_and_Unloading/
作者
SuHang
发布于
2025年8月25日
许可协议