一、ScriptableObject:序列化不是银弹,滥用就是定时炸弹
1.1 把配置当“小数据库”——为什么 20 MB 的 ScriptableObject 会让 Editor 打开即卡死
错误姿势:
[CreateAssetMenu(menuName="Game/AllItems")]
public class AllItems : ScriptableObject
{
[SerializeField] private List<ItemConfig> items; // 2 万条数据
}
Unity 会为每个元素在 YAML 里写完整类型名,2 万条直接 20 MB+。一打开 Inspector 就全量反序列化,主线程卡死。
正确姿势:
[CreateAssetMenu(menuName="Game/ItemSheet")]
public class ItemSheet : ScriptableObject, ISerializationCallbackReceiver
{
// Editor 下用字典方便查表
public Dictionary<int, ItemConfig> id2Cfg = new();
// 真正序列化的是这两个数组
[SerializeField] private int[] _keys;
[SerializeField] private ItemConfig[] _values;
public void OnBeforeSerialize()
{
_keys = id2Cfg.Keys.ToArray();
_values = id2Cfg.Values.ToArray();
}
public void OnAfterDeserialize()
{
id2Cfg.Clear();
for (int i = 0; i < _keys.Length; i++)
id2Cfg[_keys[i]] = _values[i];
}
}
把字典拆成平行数组,YAML 体积瞬间降到 2 MB,Inspector 首次打开 < 200 ms。
1.2 ScriptableObject 的引用链陷阱
-
不要在 ScriptableObject 里直接引用
Texture2D、AudioClip等重量级资源。 -
建议用
AssetReference(Addressable)或ResourcePath字符串 + 延迟加载。
[System.Serializable]
public class IconRef
{
[SerializeField] private string _address;
private Sprite _cache;
public Sprite Icon => _cache ??= Addressables.LoadAssetAsync<Sprite>(_address).WaitForCompletion();
}
二、AssetBundle 热更:依赖地狱与内存“双杀”
2.1 依赖收集不完整 → 运行时粉色材质
错误代码:
BuildPipeline.BuildAssetBundles(output,
new AssetBundleBuild[]
{
new AssetBundleBuild
{
assetBundleName = "ui",
assetNames = new[] { "Assets/Prefabs/UI/PanelA.prefab" }
}
},
BuildAssetBundleOptions.ChunkBasedCompression,
EditorUserBuildSettings.activeBuildTarget);
PanelA.prefab 引用了公共图集 SpriteAtlas,但 SpriteAtlas 没打进来,真机运行全粉。
修复:
[MenuItem("Build/CollectBundle")]
static void Collect()
{
var map = new Dictionary<string, string>(); // assetPath -> bundleName
Collect("Assets/Prefabs/UI", "ui", map);
Collect("Assets/Atlas", "common", map);
var builds = map.GroupBy(kv => kv.Value)
.Select(g => new AssetBundleBuild
{
assetBundleName = g.Key,
assetNames = g.Select(x=>x.Key).ToArray()
}).ToArray();
BuildPipeline.BuildAssetBundles(output, builds,
BuildAssetBundleOptions.ChunkBasedCompression,
EditorUserBuildSettings.activeBuildTarget);
}
2.2 重复加载导致内存爆炸
永远用 AssetBundleRequest 的缓存封装:
public static class AssetBundleCache
{
private static readonly Dictionary<string, AssetBundle> _cache = new();
public static AssetBundle Load(string path)
{
if (_cache.TryGetValue(path, out var ab)) return ab;
return _cache[path] = AssetBundle.LoadFromFile(path);
}
public static void Unload(string path, bool unloadAll = false)
{
if (!_cache.Remove(path, out var ab)) return;
ab.Unload(unloadAll);
}
}
三、UGUI:看似自动回收,实则“内存钉子户”
3.1 事件监听忘记移除
public class BagPanel : MonoBehaviour
{
private void OnEnable() => BagModel.OnItemChanged += Refresh;
private void OnDisable() => BagModel.OnItemChanged -= Refresh;
}
很多开发者把面板做成 DontDestroyOnLoad 的“常驻节点”,切场景后 OnDisable 不触发,事件一直挂着。解决:
-
在
OnDestroy里再解一次; -
或者使用弱事件:
public static class WeakEvent<T> where T : class
{
private static readonly List<WeakReference<T>> _listeners = new();
public static void Add(T t) => _listeners.Add(new WeakReference<T>(t));
public static void Invoke(Action<T> action)
{
_listeners.RemoveAll(r => !r.TryGetTarget(out var t) || t == null);
foreach (var r in _listeners) action(r.Target);
}
}
3.2 图集常驻内存
SpriteAtlasManager.atlasRequested 动态加载的图集要在场景卸载时主动 Resources.UnloadAsset(atlas),否则图集会常驻显存。
四、DOTS/ECS:不是“换个 API”,而是“换个脑子”
4.1 把 MonoBehaviour 直接塞进 IComponentData
错误:
public struct Health : IComponentData
{
public MonoBehaviour fx; // 不能放托管类型
}
正确:
public struct Health : IComponentData
{
public Entity fxEntity; // 用 Entity 指向特效实体
}
4.2 System 中每帧 Entities.ForEach 查询写逻辑
partial struct DamageSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
new ProcessJob { ECB = ecb }.Schedule();
}
[BurstCompile]
partial struct ProcessJob : IJobEntity
{
public EntityCommandBuffer ECB;
void Execute(Entity e, ref Health hp, in Damage dmg)
{
hp.Value -= dmg.Value;
if (hp.Value <= 0)
ECB.DestroyEntity(e);
}
}
}
-
用 ECB 解决并行写回问题;
-
用
[BurstCompile]+IJobEntity保证 Burst 优化。
4.3 内存布局陷阱
public struct TransformData : IComponentData
{
public float3 position; // 12 字节
public int layer; // 4 字节
// 这里对齐到 16 字节,浪费 0 字节;但如果把 layer 换成 bool,会填充 3 字节,导致 SIMD 不友好
}
最佳实践:把 float3/float4 放一起,int/uint 放一起,减少 padding。
五、IL2CPP 崩溃:如何读懂天书堆栈
5.1 符号缺失
-
Windows:
Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\il2cpp\Release\Symbols -
macOS:
Unity.app/Contents/PlaybackEngines/AndroidPlayer/Variations/il2cpp/Release/Symbols
把libil2cpp.sym.so上传到 CrashSight / Firebase,就能还原函数名。
5.2 常见崩溃 Top3
-
空委托:C# 编译器优化后空委托在 IL2CPP 里直接 deref。解决:所有事件先
?.Invoke()。 -
泛型递归 AOT 没生成:用
[Preserve]或link.xml。 -
多线程访问托管对象:Unity 主线程外的 Job 线程不能访问任何
UnityEngine.Object。
六、性能:Profiler 不是“看热闹”,而是“破案”
6.1 GC 峰值
void Update()
{
// 每帧 1000 次字符串拼接
string s = "";
for (int i = 0; i < 1000; i++) s += i.ToString();
}
换成 ValueStringBuilder(Unity 2022.2+ 内置)或 ZString。
6.2 CPU Stsssssssssssssssallssssss
在 Timeline 里看到 Gfx.WaitForPresent → 说明 GPU 繁忙。
-
检查 Overdraw:Frame Debugger 看有没有透明全屏 UI;
-
检查 SetPassCall:合批失败通常是材质球参数不一致,把颜色放到顶点色而非材质属性。'
6.3 GPU Instancing 失效
-
材质勾了
Enable GPU Instancing但 Shader 里没#pragma multi_compile_instancing。 -
动态合批与 Instancing 互斥,关闭
Dynamic Batching。
七、代码仓库级别的“约定优于配置”
7.1 强制代码格式化
在 Assets/csc.rsp 里加:
-nullable:enable
-warnaserror+
配合 .editorconfig,CI 里用 dotnet format --verify-no-changes。
7.2 自动生成 link.xml
[InitializeOnLoad]
public static class LinkXmlGenerator
{
static LinkXmlGenerator()
{
var sb = new StringBuilder();
sb.AppendLine("<linker>");
foreach (var type in TypeCache.GetTypesWithAttribute<PreserveAttribute>())
sb.AppendLine($" <type fullname=\"{type.FullName}\" preserve=\"all\"/>");
sb.AppendLine("</linker>");
File.WriteAllText("Assets/link.xml", sb.ToString());
}
}
这样再也不用手写 500 行 link.xml。

405

被折叠的 条评论
为什么被折叠?



