介绍
打包时需要使用两次 BuildPipeline.BuildPlayer 分别构建不同版本的包,只调用一次则打包时间很正常只有 5 分钟,但是打包两次时间则会膨胀到 30 分钟。
通过阅读分析 Unity 打包日志,发现在两次 BuildPipeline.BuildPlayer 中间有大量长时间耗时的操作呢?实际项目中出现了 1300+ 条类似下面的日志,而且耗时达22分钟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
16:43:08.145856 Unloading 1 Unused Serialized files (Serialized files now loaded: 0)
16:43:09.158184 Unloading 17 unused Assets / (24.7 KB). Loaded Objects now: 38901.
16:43:09.158184 Memory consumption went from 0.72 GB to 0.72 GB.
16:43:09.158184 Total: 960.310500 ms (FindLiveObjects: 2.454500 ms CreateObjectMapping: 3.433300 ms MarkObjects: 954.311900 ms DeleteObjects: 0.110200 ms)
16:43:09.158184
16:43:09.158184 Unloading 1 Unused Serialized files (Serialized files now loaded: 0)
16:43:10.170468 Unloading 17 unused Assets / (4.4 KB). Loaded Objects now: 38901.
16:43:10.170468 Memory consumption went from 0.72 GB to 0.72 GB.
16:43:10.170468 Total: 956.463900 ms (FindLiveObjects: 2.442300 ms CreateObjectMapping: 3.146500 ms MarkObjects: 950.779100 ms DeleteObjects: 0.095300 ms)
...
17:05:10.661445 Unloading 2 Unused Serialized files (Serialized files now loaded: 0)
17:05:11.675348 Unloading 13 unused Assets / (6.2 KB). Loaded Objects now: 40923.
17:05:11.675348 Memory consumption went from 0.71 GB to 0.71 GB.
17:05:11.675348 Total: 891.505800 ms (FindLiveObjects: 2.536600 ms CreateObjectMapping: 3.384700 ms MarkObjects: 885.480900 ms DeleteObjects: 0.102900 ms)
17:05:11.675348
17:05:11.675348 Unloading 4 Unused Serialized files (Serialized files now loaded: 0)
17:05:12.688781 Unloading 20 unused Assets / (12.8 KB). Loaded Objects now: 40923.
17:05:12.688781 Memory consumption went from 0.72 GB to 0.72 GB.
17:05:12.688781 Total: 890.204700 ms (FindLiveObjects: 2.589100 ms CreateObjectMapping: 3.347600 ms MarkObjects: 884.123500 ms DeleteObjects: 0.143700 ms)
|
环境
- Unity 2022.3.62f1
- Windows 10 22H2
问题分析
上面的日志是卸载资源,Unity 编辑器下对应的 API 是 EditorUtility.UnloadUnusedAssetsImmediate()
问题出在 Spine 插件的构建预处理器 SpineBuildProcessor.cs。它作为 IPreprocessBuildWithReport(callbackOrder = -2000)在 BuildPipeline.BuildPlayer 之前执行:
SpineBuildProcessor.cs Lines 73-93
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
internal static void PreprocessSpinePrefabMeshes () {
BuildUtilities.IsInSkeletonAssetBuildPreProcessing = true;
try {
AssetDatabase.StartAssetEditing();
prefabsToRestore.Clear();
var prefabAssets = AssetDatabase.FindAssets("t:Prefab");
foreach (var asset in prefabAssets) {
string assetPath = AssetDatabase.GUIDToAssetPath(asset);
GameObject prefabGameObject = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (SpineEditorUtilities.CleanupSpinePrefabMesh(prefabGameObject)) {
prefabsToRestore.Add(assetPath);
}
EditorUtility.UnloadUnusedAssetsImmediate();
}
AssetDatabase.StopAssetEditing();
if (prefabAssets.Length > 0)
AssetDatabase.SaveAssets();
} finally {
BuildUtilities.IsInSkeletonAssetBuildPreProcessing = false;
}
}
|
同样的问题也出现在 PreprocessSpriteAtlases 中:
SpineBuildProcessor.cs Lines 111-131
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
internal static void PreprocessSpriteAtlases () {
BuildUtilities.IsInSpriteAtlasBuildPreProcessing = true;
try {
AssetDatabase.StartAssetEditing();
spriteAtlasTexturesToRestore.Clear();
var spriteAtlasAssets = AssetDatabase.FindAssets("t:SpineSpriteAtlasAsset");
foreach (var asset in spriteAtlasAssets) {
string assetPath = AssetDatabase.GUIDToAssetPath(asset);
SpineSpriteAtlasAsset atlasAsset = AssetDatabase.LoadAssetAtPath<SpineSpriteAtlasAsset>(assetPath);
if (atlasAsset && atlasAsset.materials.Length > 0) {
spriteAtlasTexturesToRestore[assetPath] = AssetDatabase.GetAssetPath(atlasAsset.materials[0].mainTexture);
atlasAsset.materials[0].mainTexture = null;
}
EditorUtility.UnloadUnusedAssetsImmediate();
}
AssetDatabase.StopAssetEditing();
if (spriteAtlasAssets.Length > 0)
AssetDatabase.SaveAssets();
} finally {
BuildUtilities.IsInSpriteAtlasBuildPreProcessing = false;
}
}
|
问题本质
这两个方法的致命问题是:在 foreach 循环的每次迭代内部调用了 EditorUtility.UnloadUnusedAssetsImmediate()。
从日志中可以看到:
| 指标 |
值 |
每次 MarkObjects 耗时 |
~950ms |
| 已加载对象数 |
38,901 ~ 40,923 |
| 每次卸载的资源 |
仅 13~20 个(几 KB) |
| 循环次数 |
1,300+ 次(= 项目中 Prefab 总数 + SpineSpriteAtlasAsset 数量) |
| 总耗时 |
1,300 × ~1s ≈ 22 分钟 |
每次调用 UnloadUnusedAssetsImmediate() 都需要完整遍历内存中所有 38,901+ 个对象来执行 MarkObjects,但每次只卸载了十几个微小资源(几 KB),内存从 0.72GB 到 0.72GB 几乎没有变化——这完全是无效的重复劳动。
修复方案
修改文件: Assets/Plugins/Spine/Editor/spine-unity/Editor/Utility/SpineBuildProcessor.cs
两处改动: 将 EditorUtility.UnloadUnusedAssetsImmediate() 从 foreach 循环内部移到循环结束后(StopAssetEditing 之后)只调用一次。
|
修改前 |
修改后 |
PreprocessSpinePrefabMeshes |
循环内每个 Prefab 都调用一次 |
循环外只调用一次 |
PreprocessSpriteAtlases |
循环内每个 Atlas 都调用一次 |
循环外只调用一次 |
| 预估耗时 |
1,300+ 次 × ~1s = 22 分钟 |
1 次 × ~1s = 1 秒 |
这个修复将这部分构建时间从 22 分钟压缩到约 1 秒。UnloadUnusedAssetsImmediate 的目的是防止循环中加载大量 Prefab 导致内存溢出,但每次循环只加载一个 Prefab 然后立即卸载是极其低效的做法——在循环结束后统一清理一次就足够了。
官方现状
截止到 2026-03-22 最新的 Spine 4.2 版本依然存在此问题