介绍
打包时需要使用两次 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 版本依然存在此问题
共有 0 条评论