解读
国内Unity面试中,这道题出现的频率极高,面试官真正想考察的是“值类型与引用类型的内存模型差异”以及“你是否能在性能敏感场景下正确选型” 。
很多候选人只回答“struct在栈、class在堆”,结果直接被追问“数组呢?闭包呢?IL2CPP下呢?”——如果答不上来,会被判定为“只背了八股文”。
因此,答案必须把CLI规范、Unity实际内存布局、IL2CPP后端行为、Burst限制四层信息逐层展开,体现“工程落地能力”。
知识点
-
CLI规范层面:值类型与引用类型的抽象定义,与具体运行时无关。
-
IL2CPP转译层面:Unity把C#转C++,栈/堆分配决策权在C++编译器,不再由CLR托管。
-
Burst编译器层面:Burst要求所有值类型必须是Blittable且无托管引用,否则回退到托管堆。
-
Unity Profiler验证层面:用Memory Profiler或Deep Profile可直接看到struct被内联到Native Memory或C++栈,而非Mono堆。
-
性能陷阱:
- 值类型数组无论元素是struct还是class,数组本体都在托管堆,元素布局遵循“引用压缩”或“顺序内联”。
- 闭包捕获的局部struct会被装箱到托管堆,因为编译器生成隐藏class。
- 协程yield return struct同样会装箱。
答案
在纯.NET Framework/Mono运行时下,简要结论:
- struct是值类型,未装箱时随所在上下文分配:局部变量随栈帧弹出,字段则内嵌于宿主对象(类或数组)的内存区域。
- class是引用类型,对象本体永远在托管堆,栈上仅存指针。
但在Unity的IL2CPP后端中,上述“栈/堆”概念被重新映射:
- 局部struct若未被闭包捕获,IL2CPP将其生成C++的栈变量,生命周期随C++栈帧销毁,无GC压力。
- 局部class变量,IL2CPP生成
Object_t*指针,对象本体在IL2CPP托管堆(仍受GC管理)。 - struct数组虽在托管堆,但元素顺序紧密排列,遍历缓存友好;class数组元素是指针数组,多一次解引用且随机访问,Cache Miss更高。
- 若struct被装箱(如接口调用、委托、协程),IL2CPP会生成
System_ValueType*指针,对象本体进入托管堆,与class无差异。
因此,不能简单背“struct栈、class堆” ,而应回答:
“在Unity IL2CPP下,struct默认随C++栈帧分配,生命周期可预测;class对象始终在托管堆。但数组、闭包、接口装箱都会让struct进入堆,需用Profiler验证。”
拓展思考
- Job System与Burst:
只有** unmanaged 的struct**(无托管字段)才能被Burst编译为SIMD指令,并完全分配在Native Memory;一旦混有string或object,编译失败或回退托管堆。 - 移动端GC卡顿实战:
某MMO项目把1000个MonsterData从class改为struct,并改用NativeArray<MonsterData>,每帧GC.Alloc从1.2 MB降到0,Android低端机卡顿帧率提升8 FPS。 - 面试反杀技巧:
如果面试官追问“如何证明struct在栈”,可回答:
“用Unity 2021.3 Memory Profiler抓帧,勾选‘Native Memory’视图,能看到局部struct变量出现在PlayerMainThreadStack区域,而class对象出现在IL2CPP Managed Heap区域,直接给出截图即可让面试官闭嘴。”