Spine 插件导致 Unity 打包耗时过长

介绍

打包时需要使用两次 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 版本依然存在此问题

版权声明:
作者:Alex
链接:https://www.techfm.club/p/234926.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>