第2章 资源标记与管理

第2章 资源标记与管理

资源标记为Addressable

手动标记资源

将资源标记为Addressable是最基本的操作,有以下几种方式:

方式一:Inspector面板标记

  1. 在Project窗口中选择目标资源
  2. 在Inspector面板底部找到”Addressable Asset”组件
  3. 勾选”Addressable”复选框
  4. 系统会自动生成一个默认地址(通常是资源路径)
  5. 可以修改Address字段来自定义地址名称

方式二:Addressables Groups窗口标记

  1. 打开Window > Asset Management > Addressables > Groups
  2. 在Groups窗口中可以看到所有资源
  3. 选中需要标记的资源行
  4. 在Address列中输入地址名称
  5. 或者勾选Address列的复选框使用默认地址

批量标记资源

对于大量资源的标记,可以使用以下方法:

使用脚本批量标记

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;

public class BatchAddressableMarker : EditorWindow
{
[MenuItem("Tools/Addressables/Batch Mark Addressable")]
static void Init()
{
BatchAddressableMarker window = (BatchAddressableMarker)EditorWindow.GetWindow(typeof(BatchAddressableMarker));
window.titleContent = new GUIContent("Batch Mark Addressable");
window.Show();
}

string searchPattern = "*.prefab";
string addressPrefix = "Prefabs/";

void OnGUI()
{
GUILayout.Label("批量标记Addressable资源", EditorStyles.boldLabel);

searchPattern = EditorGUILayout.TextField("搜索模式:", searchPattern);
addressPrefix = EditorGUILayout.TextField("地址前缀:", addressPrefix);

if (GUILayout.Button("标记选中资源"))
{
MarkSelectedAssets();
}

if (GUILayout.Button("标记指定路径资源"))
{
MarkAssetsAtPath();
}
}

void MarkSelectedAssets()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
var selectedObjects = Selection.objects;

foreach (var obj in selectedObjects)
{
string assetPath = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(assetPath))
{
var entry = settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(assetPath),
settings.DefaultGroup, false, true);
entry.address = addressPrefix + obj.name;
}
}

settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification,
settings.groups, true, true);
}

void MarkAssetsAtPath()
{
string[] guids = AssetDatabase.FindAssets(searchPattern);
var settings = AddressableAssetSettingsDefaultObject.Settings;

foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false, true);
string fileName = System.IO.Path.GetFileNameWithoutExtension(path);
entry.address = addressPrefix + fileName;
}

settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification,
settings.groups, true, true);
}
}

Address命名规范与最佳实践

命名原则

1. 清晰性原则

  • 地址应该能够清晰地表达资源的用途和类型
  • 避免使用模糊或过于简单的地址名称
  • 使用有意义的描述性名称
1
2
3
4
5
6
7
8
9
// 好的命名
"Characters/Player/HeroPrefab"
"UI/Sprites/ButtonTexture"
"Audio/Music/BattleMusic"

// 不好的命名
"Asset123"
"Character"
"Music"

2. 结构化原则

  • 使用分层结构来组织地址
  • 类似于文件夹路径的格式
  • 按功能、类型或场景进行分组

3. 一致性原则

  • 在整个项目中保持命名风格一致
  • 团队成员使用相同的命名约定
  • 建立命名规范文档

命名模式

按资源类型分组

1
2
3
4
5
6
Prefabs/Characters/Player.prefab
Prefabs/Characters/Enemy.prefab
Textures/Characters/PlayerTexture.png
Textures/Environment/BackgroundTexture.png
Materials/Characters/PlayerMaterial.mat
Materials/Environment/BackgroundMaterial.mat

按功能模块分组

1
2
3
4
Gameplay/Player/Character.prefab
Gameplay/Player/Weapon.prefab
UI/MainMenu/Button.prefab
UI/Inventory/Slot.prefab

按场景分组

1
2
3
4
Scene1/Character.prefab
Scene1/Environment.prefab
Scene2/Character.prefab
Scene2/Props.prefab

命名最佳实践

1. 使用小写字母

  • 统一使用小写字母避免大小写敏感问题
  • 提高跨平台兼容性

2. 避免特殊字符

  • 只使用字母、数字、下划线和斜杠
  • 避免空格和其他特殊字符

3. 版本管理

  • 在地址中包含版本信息(如果需要)
  • 使用时间戳或版本号后缀

Labels(标签)系统的使用

标签的概念

标签(Labels)是Addressables系统中用于分类和筛选资源的机制。与地址不同,一个资源可以有多个标签,这使得资源管理更加灵活。

创建和管理标签

在Addressables Groups窗口中管理标签

  1. 打开Addressables Groups窗口
  2. 点击窗口顶部的”Labels”按钮
  3. 在弹出的标签管理窗口中可以:
    • 添加新标签
    • 删除现有标签
    • 重命名标签

为资源分配标签

  1. 在Groups窗口中选中资源
  2. 在标签列中点击”Edit”按钮
  3. 选择需要的标签
  4. 确认应用

使用标签加载资源

加载带有特定标签的所有资源

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
60
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections.Generic;

public class LabelBasedLoader : MonoBehaviour
{
public string labelName = "Character";

void LoadAssetsByLabel()
{
// 加载所有带有指定标签的资源
AsyncOperationHandle<IList<GameObject>> handle =
Addressables.LoadAssetsAsync<GameObject>(labelName, null);

handle.Completed += (operation) =>
{
if (operation.Status == AsyncOperationStatus.Succeeded)
{
var loadedAssets = operation.Result;
Debug.Log($"加载了 {loadedAssets.Count} 个带有标签 '{labelName}' 的资源");

foreach (GameObject asset in loadedAssets)
{
Debug.Log($"加载的资源: {asset.name}");
}
}
else
{
Debug.LogError($"加载失败: {operation.OperationException}");
}

// 释放操作句柄
Addressables.Release(handle);
};
}

void LoadAssetsByMultipleLabels()
{
// 使用标签查询加载资源
var keys = new List<object> { "Character", "Player" };
AsyncOperationHandle<IList<GameObject>> handle =
Addressables.LoadAssetsAsync<GameObject>(keys, null, Addressables.MergeMode.Intersection);

handle.Completed += (operation) =>
{
if (operation.Status == AsyncOperationStatus.Succeeded)
{
var loadedAssets = operation.Result;
Debug.Log($"加载了同时具有 'Character' 和 'Player' 标签的资源数量: {loadedAssets.Count}");
}
else
{
Debug.LogError($"加载失败: {operation.OperationException}");
}

Addressables.Release(handle);
};
}
}

标签的应用场景

1. 按类型分类

  • “Character”, “Weapon”, “Item” - 按资源类型分类
  • “HighQuality”, “LowQuality” - 按质量等级分类

2. 按功能分类

  • “Essential”, “Optional” - 按重要性分类
  • “Updateable”, “Fixed” - 按更新频率分类

3. 按平台分类

  • “Mobile”, “Desktop”, “Console” - 按平台分类
  • “VR”, “AR”, “Standard” - 按功能特性分类

Addressables Groups窗口详解

Groups窗口界面介绍

Addressables Groups窗口是管理Addressable资源的主要界面,包含以下主要区域:

1. 工具栏

  • Create Group:创建新的资源组
  • Build:构建Addressable资源
  • Profile:切换不同平台的配置文件
  • Play Mode:选择运行模式
  • Settings:打开Addressable设置

2. 组列表区域

  • 显示所有资源组
  • 可以创建、删除、重命名组
  • 查看每个组的基本信息

3. 资源详情区域

  • 显示选中组中的所有资源
  • 编辑资源的地址和标签
  • 查看资源的构建信息

组的类型和配置

1. BundledAssetGroup

这是最常见的组类型,资源会被打包成AssetBundle:

  • BundledAssetGroupSchema:控制打包和加载路径
  • ContentUpdateGroupSchema:控制内容更新行为

2. PlayerDataGroup

资源直接包含在Player中,不打包:

  • 用于必须在构建时包含的核心资源
  • 无法进行内容更新

3. ContentUpdateGroup

专门用于内容更新的组:

  • 支持差量更新
  • 需要特殊的构建流程

Schema系统详解

BundledAssetGroupSchema配置

  • Load Path:运行时加载路径
  • Build Path:构建输出路径
  • Bundle Mode:打包模式
  • Compression:压缩方式

ContentUpdateGroupSchema配置

  • Static Content:是否为静态内容
  • Content Hash:内容哈希设置
  • Use Asset Bundle Caching:启用AssetBundle缓存

资源的批量操作与管理

批量选择和操作

使用选择工具

  • 按住Ctrl键选择多个资源
  • 按住Shift键选择连续资源
  • 使用右键菜单进行批量操作

批量编辑功能

  • 选中多个资源后可以批量修改地址
  • 批量分配标签
  • 批量移动到其他组

资源导入后自动标记

创建Editor脚本来实现资源导入后的自动标记:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;

public class AutoAddressableProcessor : AssetPostprocessor
{
void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
string[] movedAssets, string[] movedFromAssetPaths)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return;

// 处理新导入的资源
foreach (string assetPath in importedAssets)
{
ProcessAsset(assetPath, settings);
}

// 处理移动的资源
for (int i = 0; i < movedAssets.Length; i++)
{
ProcessMovedAsset(movedFromAssetPaths[i], movedAssets[i], settings);
}
}

void ProcessAsset(string assetPath, AddressableAssetSettings settings)
{
// 根据路径模式自动标记资源
if (assetPath.Contains("Assets/Prefabs/"))
{
if (assetPath.EndsWith(".prefab"))
{
string guid = AssetDatabase.AssetPathToGUID(assetPath);
var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false, true);

// 生成地址:移除Assets/前缀,保留相对路径
string address = assetPath.Substring("Assets/".Length);
address = address.Substring(0, address.Length - ".prefab".Length);
entry.address = address;

// 添加标签
entry.SetLabel("Prefab", true, true);
}
}
else if (assetPath.Contains("Assets/Textures/"))
{
if (assetPath.EndsWith(".png") || assetPath.EndsWith(".jpg"))
{
string guid = AssetDatabase.AssetPathToGUID(assetPath);
var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false, true);

string address = assetPath.Substring("Assets/".Length);
address = address.Substring(0, address.LastIndexOf('.'));
entry.address = address;

entry.SetLabel("Texture", true, true);
}
}
}

void ProcessMovedAsset(string oldPath, string newPath, AddressableAssetSettings settings)
{
// 处理资源移动后地址更新
string oldGuid = AssetDatabase.AssetPathToGUID(oldPath);
if (!string.IsNullOrEmpty(oldGuid))
{
var entry = settings.FindAssetEntry(oldGuid);
if (entry != null)
{
// 更新地址为新路径
string newAddress = newPath.Substring("Assets/".Length);
if (newAddress.EndsWith(".prefab"))
{
newAddress = newAddress.Substring(0, newAddress.Length - ".prefab".Length);
}
else
{
newAddress = newAddress.Substring(0, newAddress.LastIndexOf('.'));
}
entry.address = newAddress;
}
}
}
}

实战案例:将现有项目资源迁移到Addressables

迁移前的准备工作

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
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;

public class ResourceAuditor : EditorWindow
{
[MenuItem("Tools/Addressables/Resource Audit")]
static void Init()
{
ResourceAuditor window = (ResourceAuditor)EditorWindow.GetWindow(typeof(ResourceAuditor));
window.titleContent = new GUIContent("Resource Auditor");
window.Show();
}

List<ResourceInfo> resourceList = new List<ResourceInfo>();
Vector2 scrollPosition;

class ResourceInfo
{
public string path;
public long size;
public int referenceCount;
public string type;
}

void OnGUI()
{
if (GUILayout.Button("扫描项目资源"))
{
ScanProjectResources();
}

scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

foreach (var info in resourceList.Take(100)) // 限制显示数量
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(info.path, GUILayout.Width(300));
EditorGUILayout.LabelField(info.type, GUILayout.Width(100));
EditorGUILayout.LabelField($"{info.size / 1024} KB", GUILayout.Width(100));
EditorGUILayout.LabelField($"引用: {info.referenceCount}", GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}

EditorGUILayout.EndScrollView();
}

void ScanProjectResources()
{
resourceList.Clear();

string[] allAssets = AssetDatabase.GetAllAssetPaths();
foreach (string assetPath in allAssets)
{
if (assetPath.StartsWith("Assets/") &&
(assetPath.EndsWith(".prefab") ||
assetPath.EndsWith(".png") ||
assetPath.EndsWith(".jpg") ||
assetPath.EndsWith(".fbx") ||
assetPath.EndsWith(".mat") ||
assetPath.EndsWith(".asset")))
{
var info = new ResourceInfo
{
path = assetPath,
type = System.IO.Path.GetExtension(assetPath),
size = GetFileSize(assetPath)
};

resourceList.Add(info);
}
}

// 按大小排序
resourceList.Sort((a, b) => b.size.CompareTo(a.size));
}

long GetFileSize(string assetPath)
{
string fullPath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), assetPath);
if (System.IO.File.Exists(fullPath))
{
var fileInfo = new System.IO.FileInfo(fullPath);
return fileInfo.Length;
}
return 0;
}
}

2. 制定迁移策略

根据资源审计结果,制定迁移策略:

  • 核心资源:游戏启动必需的资源,考虑放在PlayerDataGroup
  • 场景资源:按场景分组,便于按需加载
  • 通用资源:UI、音效等,可按功能分组
  • 更新资源:经常更新的内容,使用ContentUpdateGroup

迁移实施步骤

步骤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
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;

public class MigrationSetup : EditorWindow
{
[MenuItem("Tools/Addressables/Migration Setup")]
static void Init()
{
MigrationSetup window = (MigrationSetup)EditorWindow.GetWindow(typeof(MigrationSetup));
window.titleContent = new GUIContent("Migration Setup");
window.Show();
}

void OnGUI()
{
GUILayout.Label("创建标准资源组结构", EditorStyles.boldLabel);

if (GUILayout.Button("创建标准组结构"))
{
CreateStandardGroups();
}

if (GUILayout.Button("清理旧的Resources文件夹"))
{
CleanupResourcesFolder();
}
}

void CreateStandardGroups()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) return;

// 创建核心资源组
var coreGroup = settings.CreateGroup("Core", false, false, true, null);
coreGroup.AddSchema<BundledAssetGroupSchema>();
coreGroup.AddSchema<ContentUpdateGroupSchema>();
var coreSchema = coreGroup.GetSchema<BundledAssetGroupSchema>();
coreSchema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;

// 创建场景资源组
var sceneGroup = settings.CreateGroup("Scenes", false, false, true, null);
sceneGroup.AddSchema<BundledAssetGroupSchema>();
sceneGroup.AddSchema<ContentUpdateGroupSchema>();

// 创建UI资源组
var uiGroup = settings.CreateGroup("UI", false, false, true, null);
uiGroup.AddSchema<BundledAssetGroupSchema>();
uiGroup.AddSchema<ContentUpdateGroupSchema>();

// 创建音频资源组
var audioGroup = settings.CreateGroup("Audio", false, false, true, null);
audioGroup.AddSchema<BundledAssetGroupSchema>();
audioGroup.AddSchema<ContentUpdateGroupSchema>();

Debug.Log("标准资源组结构创建完成");
}

void CleanupResourcesFolder()
{
if (System.IO.Directory.Exists("Assets/Resources"))
{
bool confirm = EditorUtility.DisplayDialog(
"确认删除",
"确定要删除Assets/Resources文件夹吗?请确保已备份重要资源。",
"删除",
"取消");

if (confirm)
{
AssetDatabase.DeleteAsset("Assets/Resources");
AssetDatabase.Refresh();
Debug.Log("Resources文件夹已删除");
}
}
else
{
Debug.Log("未找到Resources文件夹");
}
}
}

步骤2:资源迁移脚本

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using System.Collections.Generic;

public class ResourceMigrator : EditorWindow
{
[MenuItem("Tools/Addressables/Migrate Resources")]
static void Init()
{
ResourceMigrator window = (ResourceMigrator)EditorWindow.GetWindow(typeof(ResourceMigrator));
window.titleContent = new GUIContent("Resource Migrator");
window.Show();
}

string sourcePath = "Assets/";
string targetGroup = "Default";
bool includeSubfolders = true;

void OnGUI()
{
GUILayout.Label("资源迁移工具", EditorStyles.boldLabel);

sourcePath = EditorGUILayout.TextField("源路径:", sourcePath);
targetGroup = EditorGUILayout.TextField("目标组:", targetGroup);
includeSubfolders = EditorGUILayout.Toggle("包含子文件夹:", includeSubfolders);

if (GUILayout.Button("开始迁移"))
{
MigrateResources();
}
}

void MigrateResources()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("Addressable Asset Settings not found");
return;
}

// 获取目标组
var targetGroupObj = settings.FindGroup(targetGroup);
if (targetGroupObj == null)
{
Debug.LogError($"Group {targetGroup} not found");
return;
}

// 搜索资源
string searchPattern = includeSubfolders ? sourcePath + "**/*" : sourcePath + "*";
string[] guids = AssetDatabase.FindAssets("t:Object", new[] { sourcePath });

int migratedCount = 0;
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);

// 跳过非必要资源类型
if (assetPath.EndsWith(".cs") || assetPath.EndsWith(".meta") ||
assetPath.Contains("/Resources/") || assetPath.Contains("/Editor/"))
{
continue;
}

// 创建或移动资源到目标组
var entry = settings.CreateOrMoveEntry(guid, targetGroupObj, false, true);

// 生成地址
string relativePath = assetPath.Substring("Assets/".Length);
string address = relativePath.Substring(0, relativePath.LastIndexOf('.'));
entry.address = address;

// 根据文件扩展名添加标签
AddLabelsByType(entry, assetPath);

migratedCount++;
}

settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification,
settings.groups, true, true);

Debug.Log($"成功迁移 {migratedCount} 个资源到组 {targetGroup}");
}

void AddLabelsByType(AddressableAssetEntry entry, string assetPath)
{
string extension = System.IO.Path.GetExtension(assetPath).ToLower();

switch (extension)
{
case ".prefab":
entry.SetLabel("Prefab", true, true);
break;
case ".png":
case ".jpg":
case ".jpeg":
case ".tga":
case ".psd":
entry.SetLabel("Texture", true, true);
break;
case ".fbx":
case ".obj":
entry.SetLabel("Model", true, true);
break;
case ".wav":
case ".mp3":
case ".ogg":
entry.SetLabel("Audio", true, true);
break;
case ".mat":
entry.SetLabel("Material", true, true);
break;
case ".shader":
entry.SetLabel("Shader", true, true);
break;
}
}
}

迁移后的验证

验证脚本

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
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class MigrationValidator : MonoBehaviour
{
public string[] testAddresses = new string[]
{
"Assets/Prefabs/Player.prefab",
"Assets/Textures/UI/Background.png",
"Assets/Materials/PlayerMaterial.mat"
};

void Start()
{
ValidateMigration();
}

async void ValidateMigration()
{
Debug.Log("开始验证迁移结果...");

foreach (string address in testAddresses)
{
try
{
var handle = Addressables.LoadAssetAsync<GameObject>(address);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log($"✓ 成功加载: {address}");
Addressables.Release(handle);
}
else
{
Debug.LogError($"✗ 加载失败: {address} - {handle.OperationException}");
}
}
catch (System.Exception e)
{
Debug.LogError($"✗ 加载异常: {address} - {e.Message}");
}
}

Debug.Log("迁移验证完成");
}
}

通过本章的学习,您已经掌握了Addressables资源标记和管理的完整流程,包括地址命名规范、标签系统使用、Groups窗口操作以及实际的资源迁移方法。下一章我们将深入学习资源的加载和释放机制。