如何通过unsafe代码查看值类型在内存中的真实地址

解读

Unity 面试里问“看地址”并不是考你打印一串数字,而是快速验证你对值类型内存布局、托管堆与非托管堆边界、GC 可移动对象、固定句柄与 Unsafe 指针的理解
国内项目普遍要在 IL2CPP + AOT 下跑 iOS/Android,任何把托管对象地址直接当长期指针用的写法都可能触发闪退或 GC 崩溃,所以面试官想听你给出“能看,但看完立刻放弃引用”的严谨方案,并主动提到固定(pin)、栈分配、UnsafeUtility 与 Burst 兼容性这些工程红线。

知识点

  1. 值类型默认放栈上,作为字段或装箱后则在托管堆,GC 会压缩堆,托管地址随时可变
  2. unsafe 关键字需开启“允许不安全代码”编译开关,IL2CPP 下同时要在 Player Settings 打开。
  3. & 取地址操作符只能用于已固定的(pin)或栈上变量;否则编译器直接报错。
  4. fixed 语句或 GCHandle.Alloc(obj, GCHandleType.Pinned) 可把托管对象临时钉住,钉住时间越短越好
  5. IntPtrvoid* 可互转,但禁止在钉住范围外再解引用
  6. Unity 提供原生容器 UnsafeUtility.PinGCArrayAndGetDataUnsafeUtility.Unpin比 GCHandle 更高效,且与 Burst 兼容。
  7. 栈上值类型无需固定,直接 &local 即可,但不要把该指针存到字段或闭包,否则编译器报错。
  8. 在 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();   // 立即释放,防止泄漏
    }
}

关键总结

  • 只看地址:栈变量直接 &,托管对象用 fixedUnsafeUtility.PinGCArrayAndGetData
  • 绝不把指针带出固定作用域绝不把 IntPtr 存到字段供下一帧再用
  • 在 IL2CPP 真机测试通过后再合并主干,防止 AOT 裁剪掉未使用的 unsafe 方法

拓展思考

  1. 为什么 Unity 对象(GameObject、Transform)不能取地址?
    它们是托管类,地址由 GC 管理;即使固定也只能拿到托管句柄,无法拿到原生 C++ 指针。真要访问底层 Transform 数据,应使用 Unity.EntitiesTransformAccessArray 的 Burst 路径。

  2. Burst 编译器对指针的限制
    Burst 只允许 unsafe 指针指向 NativeArrayNativeSlice 或栈内存;指向托管对象的指针会被编译器直接拒绝。性能敏感代码应优先用 UnsafeUtility 而非 fixed

  3. IL2CPP 下的 AOT 陷阱
    若把 IntPtr 当字典 Key 或序列化到磁盘,升级 Unity 后 GC 堆布局可能变化,导致旧地址失效;因此地址只能用于运行时调试,禁止做持久化逻辑

  4. 与美术/策划的协作场景
    做数字孪生或点云渲染时,策划常要求“把颜色值直接写进内存数组”。此时可暴露 NativeArray<Color32>GetUnsafePtr,让 C++ 插件 DMA 写入,避免一次托管拷贝,帧率可提升 15% 以上。