如何通过unsafe代码查看值类型在内存中的真实地址
解读
Unity 面试里问“看地址”并不是考你打印一串数字,而是快速验证你对值类型内存布局、托管堆与非托管堆边界、GC 可移动对象、固定句柄与 Unsafe 指针的理解。
国内项目普遍要在 IL2CPP + AOT 下跑 iOS/Android,任何把托管对象地址直接当长期指针用的写法都可能触发闪退或 GC 崩溃,所以面试官想听你给出“能看,但看完立刻放弃引用”的严谨方案,并主动提到固定(pin)、栈分配、UnsafeUtility 与 Burst 兼容性这些工程红线。
知识点
- 值类型默认放栈上,作为字段或装箱后则在托管堆,GC 会压缩堆,托管地址随时可变。
unsafe关键字需开启“允许不安全代码”编译开关,IL2CPP 下同时要在 Player Settings 打开。&取地址操作符只能用于已固定的(pin)或栈上变量;否则编译器直接报错。fixed语句或GCHandle.Alloc(obj, GCHandleType.Pinned)可把托管对象临时钉住,钉住时间越短越好。IntPtr与void*可互转,但禁止在钉住范围外再解引用。- Unity 提供原生容器
UnsafeUtility.PinGCArrayAndGetData与UnsafeUtility.Unpin,比 GCHandle 更高效,且与 Burst 兼容。 - 栈上值类型无需固定,直接
&local即可,但不要把该指针存到字段或闭包,否则编译器报错。 - 在 Editor 下可用
Debug.Log($"0x{(ulong)p:X}")打印地址;真机 Release 请用 Profiler 或自定义 Native 插件验证,避免日志拖慢帧率。
答案
// 1. 在 Assembly Definition 或 .csproj 里打开 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
// 2. Player Settings → Other Settings → Allow 'unsafe' Code 勾上(IL2CPP 必做)
using UnityEngine;
using System.Runtime.CompilerServices;
public unsafe class AddressDemo : MonoBehaviour
{
void Start()
{
// 场景 A:栈上值类型 —— 无需固定
int stackVal = 42;
int* pStack = &stackVal;
Debug.Log($"栈上地址: 0x{(ulong)pStack:X}");
// 场景 B:托管数组里的值类型 —— 必须固定
int[] managedArr = { 1, 2, 3 };
fixed (int* pArr = &managedArr[0])
{
Debug.Log($"数组首地址: 0x{(ulong)pArr:X}");
// 离开 fixed 块前必须用完指针;禁止保存到字段
}
// 场景 C:Unity 推荐的高性能方案 —— 与 Burst 兼容
var nativeArr = new Unity.Collections.NativeArray<int>(5, Unity.Collections.Allocator.Temp);
void* pNative = nativeArr.GetUnsafePtr();
Debug.Log($"NativeArray 地址: 0x{(ulong)pNative:X}");
nativeArr.Dispose(); // 立即释放,防止泄漏
}
}
关键总结:
- 只看地址:栈变量直接
&,托管对象用fixed或UnsafeUtility.PinGCArrayAndGetData。 - 绝不把指针带出固定作用域;绝不把 IntPtr 存到字段供下一帧再用。
- 在 IL2CPP 真机测试通过后再合并主干,防止 AOT 裁剪掉未使用的 unsafe 方法。
拓展思考
-
为什么 Unity 对象(GameObject、Transform)不能取地址?
它们是托管类,地址由 GC 管理;即使固定也只能拿到托管句柄,无法拿到原生 C++ 指针。真要访问底层 Transform 数据,应使用Unity.Entities或TransformAccessArray的 Burst 路径。 -
Burst 编译器对指针的限制
Burst 只允许unsafe指针指向NativeArray、NativeSlice或栈内存;指向托管对象的指针会被编译器直接拒绝。性能敏感代码应优先用UnsafeUtility而非fixed。 -
IL2CPP 下的 AOT 陷阱
若把IntPtr当字典 Key 或序列化到磁盘,升级 Unity 后 GC 堆布局可能变化,导致旧地址失效;因此地址只能用于运行时调试,禁止做持久化逻辑。 -
与美术/策划的协作场景
做数字孪生或点云渲染时,策划常要求“把颜色值直接写进内存数组”。此时可暴露NativeArray<Color32>的GetUnsafePtr,让 C++ 插件 DMA 写入,避免一次托管拷贝,帧率可提升 15% 以上。