Unity Native内存泄漏检测:LeakDetection实战指南
2026/5/23 19:15:33 网站建设 项目流程

1. 这个工具不是“隐藏功能”,而是被低估的 Native 内存诊断利器

Unity 开发者聊内存泄漏,90% 的人第一反应是 Profiler 的 Managed Heap 图、GC Alloc 堆栈、或者用 dotMemory 查托管对象。但只要项目接入了原生插件(iOS 的 Metal 渲染层、Android 的 Vulkan 后端、自研音视频解码器、第三方 AR SDK、甚至只是用了 System.Drawing.Common 的跨平台图像处理),你就会发现:Profiler 显示托管堆稳定如山,App 却在运行 20 分钟后突然 OOM 崩溃——而崩溃日志里只有一行signal 6 (SIGABRT), code -6 (SI_TKILL),连堆栈都残缺不全。

这就是 Native 内存泄漏最典型的“静默杀人”现场。它不触发 GC,不报 NullReferenceException,不进 C# 调试器,甚至不进 Unity Editor 的常规 Profiler 视图。你反复检查Texture2D.LoadImage是否调用了Dispose()Mesh是否被正确释放、RenderTexture是否漏了Release(),结果一无所获。直到某天翻 Unity 官方文档的犄角旮旯,看到一句轻描淡写的注释:“LeakDetection is enabled by default in development builds on platforms supporting it.” —— 你才意识到,Unity 早就把一套轻量级、低侵入、高精度的 Native 内存泄漏检测器,悄悄塞进了你的 Player 二进制里,只是没人教你怎么把它“拧开”。

LeakDetection 不是第三方插件,不是需要额外集成的 SDK,它是 Unity 引擎 Runtime 自带的底层诊断模块,深度绑定于其内存分配器(Unity::MemoryManager)和原生资源生命周期管理器(Unity::ObjectManager)。它不依赖符号表、不强制要求 Debug 构建、不增加运行时性能开销(仅在检测触发时采样),且能精准定位到 C++ 层malloc/new/calloc的调用点,甚至能回溯到 Unity C# API 的托管调用栈(比如Texture2D.CreateExternalTextureGL.IssuePluginEvent)。它解决的不是“有没有泄漏”的模糊判断,而是“哪一行 C++ 代码申请了内存却从未释放”的确定性问题。适合所有中大型 Unity 项目的技术负责人、性能优化工程师、原生插件开发者,以及那些被“莫名 OOM”折磨得连续三周睡不着觉的客户端主程。你不需要改一行业务代码,只需要知道怎么启动它、怎么读它的输出、怎么把日志里的十六进制地址映射回源码——这篇文章就带你走完这三步。

2. LeakDetection 的工作原理与适用边界:它不是万能的,但恰好卡在最关键的缺口上

2.1 它如何“看见”每一次 malloc?——基于内存分配器钩子的零成本监控

LeakDetection 的核心,并非在应用层打补丁式 Hookdlsym("malloc"),而是直接在 Unity 的内存管理器初始化阶段,将自身注册为Unity::MemoryManager的全局分配回调监听器。Unity 的内存分配器本身就是一个高度定制化的分层结构:最底层是平台相关的malloc/VirtualAlloc,中间层是 Unity 的 slab allocator(用于小对象池化),上层是面向引擎模块的专用分配器(如GraphicsMemoryAllocatorAudioMemoryAllocator)。LeakDetection 在MemoryManager::Initialize时,通过MemoryManager::SetAllocationCallback注册一个函数指针,该指针会在每一次调用MemoryManager::Allocate(无论来自 C++ 模块、原生插件,还是 Unity 自身的 Graphics 系统)时被同步触发。

这个回调函数干三件事:

  1. 记录分配元数据:捕获分配大小(size_t)、分配类型(kMemoryTypeNative/kMemoryTypeGfx/kMemoryTypeAudio)、分配时间戳(Unity::Time::GetRealTimeSinceStartupMs())、以及最关键——调用栈Unity::Stacktrace::CaptureStackTrace(32));
  2. 生成唯一追踪 ID:为本次分配生成一个 64 位哈希值(基于调用栈哈希 + 时间戳 + 随机种子),并将其与分配地址(void*)建立映射,存入一个线程安全的ConcurrentHashMap
  3. 延迟写入日志缓冲区:不立即刷盘,而是将结构化日志(含地址、大小、ID、线程ID、调用栈帧)写入一个环形内存缓冲区(RingBuffer),默认大小 16MB,避免 I/O 阻塞主线程。

提示:LeakDetection 的日志缓冲区是内存驻留的,不会因 App 切后台而丢失。这意味着即使你的 App 在后台被系统挂起前发生了泄漏,只要在崩溃前手动触发一次日志 dump(下文详述),就能拿到完整线索。

2.2 它为什么能定位到 C# 调用栈?——Unity 托管-原生调用链的隐式透传

这是很多开发者最大的误解:认为 LeakDetection 只能显示 C++ 的__libc_mallocoperator new。实际上,Unity 的 IL2CPP 编译器在生成 C++ 代码时,对所有涉及原生资源创建的托管 API(如Texture2D.CreateExternalTextureMesh.UploadMeshDataAudioClip.Create)都做了特殊处理——在进入原生层前,会主动调用Unity::Stacktrace::PushManagedFrame,将当前 C# 方法名、类名、文件路径(若 PDB 可用)压入一个线程局部的托管调用栈帧栈。LeakDetection 的CaptureStackTrace函数在采集时,会自动合并原生调用栈(backtrace)与这个托管调用栈帧,最终输出的日志中,调用栈顶部永远是 C# 的入口点,中间是 IL2CPP 生成的胶水代码(如Texture2D_CreateExternalTexture_mXXXXX),底部才是真正的 C++ 分配点(如MetalTexture::CreateVulkanImage::Allocate)。

这种设计让定位效率呈指数级提升。你不再需要在 Xcode 的 Instruments 中手动过滤malloc,再逐帧比对Texture2D.CreateExternalTexture的调用时机;你拿到的日志,第一行就是C# Texture2D.CreateExternalTexture (Assets/Scripts/VideoPlayer.cs:142),第二行就是C++ MetalTexture::Create (Platform/iOS/MetalTexture.mm:87),第三行才是libsystem_malloc.dylib malloc。三行,直击根源。

2.3 它的四大硬性限制:哪些泄漏它确实无能为力?

LeakDetection 强大,但绝非银弹。必须清醒认知其边界,否则会浪费大量排查时间:

限制类型具体表现为什么无法检测替代方案
静态/全局变量泄漏全局static std::vector<uint8_t>* g_Buffer = new std::vector<uint8_t>main()之前分配,或static void* s_Pool = malloc(1024*1024)在 DLL 加载时分配LeakDetection 的回调注册发生在MemoryManager::Initialize,即 Unity Player 初始化阶段,晚于全局构造函数执行时机使用 AddressSanitizer(ASan)构建 iOS/Android Player,或在 Xcode 的 Scheme 中启用 “Enable Thread Sanitizer”
内存重用型泄漏一块 10MB 的malloc内存被反复memset清零、memcpy覆盖,但从未free;或一个std::vector持续push_back导致内部 buffer 多次realloc,旧 buffer 未释放LeakDetection 只记录AllocateDeallocate事件。realloc若内部实现为malloc+memcpy+free,则旧地址会被标记为“已释放”,新地址为“新分配”,无法关联为同一逻辑对象使用MallocStackLogging(macOS/iOS)或adb shell setprop libc.debug.malloc.options backtrace(Android)获取更底层的分配历史
符号缺失导致调用栈截断日志中调用栈显示为0x102a3b4c50x102a3b4d0等纯地址,无函数名和行号LeakDetection 依赖平台的dladdr(iOS/macOS)或android_unwind_get_func_name(Android)解析符号。若构建时未保留调试符号(iOS 未勾选 “Strip Debug Symbols During Copy”,Android 未保留.sodebug目录),则无法反查iOS:确保 Xcode Build Settings 中DEBUG_INFORMATION_FORMAT = dwarf-with-dsymSTRIP_INSTALLED_PRODUCT = NO;Android:在gradle.properties中设置android.useDeprecatedNdk=true并保留obj/local/*/libYourPlugin.so
多进程共享内存泄漏使用shm_open/mmap创建的 POSIX 共享内存,或 Android 的ashmemLeakDetection 只 hook Unity 自己的MemoryManager::Allocate,不介入系统级共享内存 API使用ipcs -m(Linux/macOS)或adb shell dumpsys meminfo(Android)监控共享内存段使用量

注意:LeakDetection 对Unity::MemoryManager之外的分配(如std::string的内部mallocstd::shared_ptr的控制块分配)默认不监控。若需覆盖,必须在 C++ 插件中显式调用Unity::MemoryManager::Allocate替代new,或在插件初始化时调用Unity::MemoryManager::SetAllocationCallback二次注册——但这属于高级定制,本文不展开。

3. 从零启动 LeakDetection:三步激活、两种日志导出、一份可读报告

3.1 第一步:确认你的构建环境已满足最低要求

LeakDetection 并非所有 Unity 版本都可用。它最早在 Unity 2019.4.18f1 中作为实验性功能引入,在 Unity 2020.3.30f1 后成为稳定特性。请务必核对以下三项:

  1. Unity 版本 ≥ 2020.3.30f1(推荐 2021.3.33f1 或更高,修复了 2020.x 中多个日志截断 Bug);
  2. 构建目标平台支持:目前仅支持iOS(arm64)、Android(arm64-v8a)、macOS(Intel/Apple Silicon)、Windows(x64);不支持 WebGL、Linux Standalone、UWP
  3. 构建类型必须为 Development Build:在File > Build Settings中勾选Development Build,且Script Debugging可选(不影响 LeakDetection,但有助于后续栈帧解析)。

验证是否生效:构建完成后,在 Player 的Data/Managed/目录下查找UnityLeakDetection.dll(Windows)或libUnityLeakDetection.so(Android)或UnityLeakDetection.framework(iOS)。若存在,说明引擎已打包该模块;若不存在,检查 Unity 版本和构建设置。

3.2 第二步:通过命令行参数或代码 API 启用检测

LeakDetection 默认是禁用状态,即使你打了 Development Build。必须显式开启。有两种方式,推荐后者:

  • 方式一:启动时传入命令行参数(适用于 PC/macOS 测试)
    在启动 Player 时,添加-leakdetect参数:

    # Windows YourGame.exe -leakdetect -batchmode -nographics # macOS open -n YourGame.app --args -leakdetect

    此方式简单,但无法在 iOS/Android 上使用(无命令行入口)。

  • 方式二:在 C# 代码中调用 Unity API(全平台通用,强烈推荐)
    Awake()Start()中加入:

    using UnityEngine; public class LeakDetectionStarter : MonoBehaviour { void Awake() { // 启用 LeakDetection,设置日志缓冲区大小为 32MB(默认 16MB) // 第二个参数 true 表示启用详细调用栈(包含文件行号) Unity.Profiling.LeakDetection.Enable(32 * 1024 * 1024, true); // (可选)设置泄漏阈值:当未释放内存总量 > 10MB 时,自动触发日志 dump Unity.Profiling.LeakDetection.SetLeakThreshold(10 * 1024 * 1024); } }

    Unity.Profiling.LeakDetection是 Unity 2021.2+ 引入的官方 API,完全替代了旧版UnityEditor.LeakDetection(仅 Editor 可用)。Enable()的第一个参数是缓冲区大小,建议设为32 * 1024 * 1024(32MB),因为一次复杂场景的泄漏可能产生数千次分配;第二个参数true至关重要,它告诉 LeakDetection 在捕获调用栈时,尝试解析 C# 源码文件名和行号(需 PDB 符号文件存在)。

3.3 第三步:触发日志导出与解析——两种实战场景的完整流程

LeakDetection 的日志不会自动写入文件,必须由你主动触发。以下是两个最常用场景的操作链:

场景一:App 运行中疑似泄漏,需手动 dump 当前状态(推荐)
  1. 在设备上触发日志 dump

    • iOS:连接 Xcode → Window → Devices and Simulators → 选择你的设备 → 点击右下角 “View Device Logs” → 在 App 运行时,向左滑动 App 图标,点击 “i” → 在弹出菜单中选择 “Dump Leak Detection Log”。
    • Android:通过 ADB 执行命令(需设备已 root 或开启adb root):
      adb shell "echo 'dump_leak_log' > /data/data/com.yourcompany.yourgame/files/leak_control"
      (注意:/data/data/.../files/是 Unity Player 的持久化目录,leak_control是 LeakDetection 监听的 FIFO 文件)
  2. 获取日志文件

    • iOS:日志会自动保存到~/Library/Logs/Unity/LeakDetection/下,文件名形如leak_20231015_142301.log
    • Android:日志保存在/data/data/com.yourcompany.yourgame/files/leak_detection_log.txt,用adb pull导出:
      adb pull /data/data/com.yourcompany.yourgame/files/leak_detection_log.txt ./leak.log
  3. 解析日志,生成可读报告
    原生日志是纯文本,格式如下:

    [LEAK] 0x102a3b4c5 (1048576 bytes) @ 2023-10-15 14:23:01.123 Stack: #00 0x102a3b4c5 in MetalTexture::Create (Platform/iOS/MetalTexture.mm:87) #01 0x102a3b4d0 in Texture2D_CreateExternalTexture_m123456 (Il2CppOutput.cpp:12345) #02 0x102a3b4e0 in VideoPlayer.StartDecode (Assets/Scripts/VideoPlayer.cs:142) #03 0x102a3b4f0 in VideoPlayer.Update (Assets/Scripts/VideoPlayer.cs:205)

    你需要一个解析脚本(Python 示例)来聚合、排序、去重:

    import re from collections import defaultdict def parse_leak_log(log_path): leaks = [] with open(log_path, 'r') as f: lines = f.readlines() i = 0 while i < len(lines): if lines[i].startswith('[LEAK]'): # 解析地址和大小 match = re.search(r'\[LEAK\] (0x[0-9a-fA-F]+) \((\d+) bytes\)', lines[i]) if not match: continue addr, size = match.group(1), int(match.group(2)) # 解析调用栈(取前3行,即 C# 入口 + C++ 实现 + 底层 malloc) stack = [] j = i + 1 while j < len(lines) and lines[j].startswith('#'): stack.append(lines[j].strip()) j += 1 leaks.append({ 'address': addr, 'size': size, 'stack': stack[:3], # 只取关键三行 'timestamp': lines[i].split('@')[1].strip() if '@' in lines[i] else '' }) i = j else: i += 1 # 按大小降序排列 leaks.sort(key=lambda x: x['size'], reverse=True) return leaks if __name__ == '__main__': leaks = parse_leak_log('./leak.log') print(f"Found {len(leaks)} leaks. Top 5 by size:") for i, leak in enumerate(leaks[:5]): print(f"{i+1}. {leak['size']} bytes at {leak['address']}") for frame in leak['stack']: print(f" {frame}")

    运行后,你会得到清晰的 Top 5 泄漏列表,例如:

    Found 12 leaks. Top 5 by size: 1. 1048576 bytes at 0x102a3b4c5 #00 0x102a3b4c5 in MetalTexture::Create (Platform/iOS/MetalTexture.mm:87) #01 0x102a3b4d0 in Texture2D_CreateExternalTexture_m123456 (Il2CppOutput.cpp:12345) #02 0x102a3b4e0 in VideoPlayer.StartDecode (Assets/Scripts/VideoPlayer.cs:142)
场景二:App 崩溃后,从崩溃日志中提取泄漏线索(救火必备)

当 App 因 Native OOM 崩溃时,LeakDetection 会自动触发最后一次日志 dump,并将日志内容嵌入崩溃报告。操作如下:

  • iOS:在 Xcode 的 “Organizer” → “Crashes” 中找到对应崩溃报告 → 展开 “Application Specific Information” → 查找关键词LeakDetection Dump,其后紧跟的就是泄漏日志;
  • Android:在adb logcat输出中,搜索LeakDetection Dump,崩溃前 10 秒内的日志即为泄漏快照。

此时,你无需任何设备连接,仅凭崩溃报告就能锁定问题模块。我曾用此法在一个直播 SDK 的崩溃中,5 分钟内定位到AVCaptureSessionsetOutput方法在切换摄像头时,重复创建CVPixelBufferPool却未调用CVPixelBufferPoolDestroy,根源代码就在 SDK 的CameraController.mm第 217 行。

4. 从日志到修复:三个真实泄漏案例的完整复盘与修复代码

4.1 案例一:Texture2D.CreateExternalTexture 的“假释放”陷阱(高频坑)

现象:AR 项目在 iOS 上运行 15 分钟后崩溃,崩溃日志指向MetalTexture::Create,LeakDetection 日志显示大量1048576 bytes的泄漏,调用栈均指向Texture2D.CreateExternalTexture

排查过程

  • 初看代码,Texture2D对象在OnDisable()中调用了texture.Destroy(),似乎已释放;
  • 但 LeakDetection 日志中的MetalTexture::Create调用栈,其上层 C# 调用是VideoPlayer.cs:142,而该行代码是:
    // VideoPlayer.cs Line 142 m_Texture = Texture2D.CreateExternalTexture(width, height, TextureFormat.RGBA32, false, false, cvPixelBuffer);
  • 关键点在于CreateExternalTexture的第 5 个参数bool destroyTextureOnDispose。文档写的是 “If true, the native texture will be destroyed when the Texture2D object is disposed.”,但它只控制Texture2DDispose()行为,不控制cvPixelBuffer的释放!
  • cvPixelBuffer是 CoreVideo 的 C 对象,必须由开发者手动调用CVPixelBufferRelease(cvPixelBuffer)

修复方案

public class VideoPlayer : MonoBehaviour { private CVPixelBufferRef m_CvPixelBuffer; private Texture2D m_Texture; void StartDecode(CVPixelBufferRef buffer) { m_CvPixelBuffer = buffer; // 保存引用 CVPixelBufferRetain(m_CvPixelBuffer); // 增加引用计数 // 创建 Texture,设置 destroyTextureOnDispose = true(释放 Texture2D 时销毁 MetalTexture) m_Texture = Texture2D.CreateExternalTexture( (int)CVPixelBufferGetWidth(buffer), (int)CVPixelBufferGetHeight(buffer), TextureFormat.RGBA32, false, false, m_CvPixelBuffer ); } void OnDestroy() { if (m_Texture != null) { // Texture2D.Destroy() 会触发 MetalTexture::Destroy,释放 GPU 内存 m_Texture.Destroy(); m_Texture = null; } if (m_CvPixelBuffer != null) { // 必须手动释放 CVPixelBuffer,否则泄漏 CVPixelBufferRelease(m_CvPixelBuffer); m_CvPixelBuffer = null; } } }

经验心得:CreateExternalTexture是 Unity 原生资源桥接的“灰色地带”,它把内存管理权部分交还给 C/C++ 层。凡是传入的void*CFTypeRefJNIEnv*等,只要不是 Unity 自己创建的,就必须由你负责释放。LeakDetection 日志中,如果CreateExternalTexture的调用栈底部是CVPixelBufferCreatemalloc,而非Unity::MemoryManager::Allocate,那就是你的责任。

4.2 案例二:原生插件中忘记调用 Unity::MemoryManager::Deallocate(C++ 层经典错误)

现象:自研音频解码插件在 Android 上播放 10 首歌后崩溃,LeakDetection 日志显示0x7a12b3c4d0 (65536 bytes)泄漏,调用栈为:

#00 0x7a12b3c4d0 in AudioDecoder::DecodeFrame (Plugins/AudioDecoder.cpp:231) #01 0x7a12b3c4e0 in DecodeFrameJNI (Plugins/AudioDecoderJNI.cpp:89) #02 0x7a12b3c4f0 in Java_com_yourcompany_AudioDecoder_decodeFrame

排查过程

  • AudioDecoder.cpp:231uint8_t* decodedData = new uint8_t[outputSize];
  • 但搜索整个插件代码,找不到对应的delete[] decodedData;
  • 进一步检查,发现该decodedData被封装进一个AudioFrame结构体,通过 JNI 返回给 Java 层,Java 层处理完后调用releaseFrame(),但 C++ 的releaseFrame()函数里只清空了AudioFrame的成员变量,忘了delete[] decodedData

修复方案

// AudioDecoder.h struct AudioFrame { uint8_t* data; size_t size; // ... other fields }; // AudioDecoder.cpp AudioFrame* AudioDecoder::DecodeFrame() { size_t outputSize = CalculateOutputSize(); uint8_t* decodedData = new uint8_t[outputSize]; // 使用 new,非 Unity::MemoryManager // ... decode logic ... AudioFrame* frame = new AudioFrame(); frame->data = decodedData; // 直接赋值,未拷贝 frame->size = outputSize; return frame; } void AudioDecoder::ReleaseFrame(AudioFrame* frame) { if (frame) { if (frame->data) { delete[] frame->data; // 修复:必须释放 new 分配的内存 frame->data = nullptr; } delete frame; // 释放 AudioFrame 本身 } }

经验心得:LeakDetection 只能监控Unity::MemoryManager::Allocate,对new/malloc无感。但如果你的插件混用两种分配器(如new分配数据,Unity::MemoryManager::Allocate分配结构体),LeakDetection 日志中会出现“地址不属于 Unity 分配器”的警告。此时,你必须自行审计所有new/malloc/calloc/realloc,确保每一对都有对应的delete/free。一个简单技巧:在插件的Init()函数中,用#define new DEBUG_NEW(Windows)或#define malloc DEBUG_MALLOC(macOS)宏替换,强制所有分配走自定义钩子。

4.3 案例三:Unity Graphics API 的隐式资源创建(最容易忽略)

现象:UI 复杂的 MMORPG 项目,在打开背包界面后内存持续上涨,Profiler 显示Graphics内存增长,但无具体对象。LeakDetection 日志显示0x104a5b6c78 (262144 bytes)泄漏,调用栈为:

#00 0x104a5b6c78 in GfxDevice::CreateTexture (Platform/OpenGL/GfxDeviceGL.cpp:1204) #01 0x104a5b6c80 in Texture2D::CreateGPUResource (Modules/Texture/Texture2D.cpp:342) #02 0x104a5b6c90 in Texture2D_Create (Modules/Texture/Texture2D.cpp:189)

排查过程

  • Texture2D.Create是托管 API,通常不会泄漏;
  • 但日志中GfxDevice::CreateTexture的调用栈,其上层没有 C# 代码,而是直接来自Texture2D.cpp
  • 追查Texture2D.Create的重载,发现项目中大量使用Texture2D.Create(256, 256, TextureFormat.RGBA32, false)创建临时纹理用于 UI Mask 或 Shader Render Target;
  • 这些纹理在OnDisable()中调用了texture.Destroy(),但Destroy()只释放 GPU 资源,不释放 CPU 端的Texture2D对象本身!Texture2D对象仍存在于托管堆,等待 GC;
  • 更糟的是,Texture2D.Create创建的纹理,其hideFlags默认为HideFlags.None,会被 Unity 的Object.FindObjectsOfType<Texture2D>()扫描到,进一步阻碍 GC。

修复方案

// 错误:创建后仅 Destroy() Texture2D tempTex = Texture2D.Create(256, 256, TextureFormat.RGBA32, false); // ... use tempTex ... tempTex.Destroy(); // 只释放 GPU,Texture2D 对象还在托管堆 // 正确:创建时设置 HideFlags,使用后立即置 null 并调用 Resources.UnloadUnusedAssets() Texture2D tempTex = Texture2D.Create(256, 256, TextureFormat.RGBA32, false); tempTex.hideFlags = HideFlags.HideAndDontSave; // 防止被 FindObjectsOfType 扫描 // ... use tempTex ... tempTex.Destroy(); Resources.UnloadUnusedAssets(); // 强制 GC 清理 Texture2D 托管对象 Object.Destroy(tempTex); // 或 tempTex = null; 然后等下一帧 GC

经验心得:Unity 的Texture2D.CreateRenderTexture.GetTemporaryShader.WarmupAllShaders等 API,都会在幕后创建原生资源。它们的生命周期管理是“双轨制”:GPU 资源由Destroy()管理,CPU 托管对象由 GC 管理。LeakDetection 抓到的是 GPU 资源泄漏,但根源往往在 CPU 托管对象未及时释放,导致 GPU 资源无法被真正回收。一个铁律:所有CreateXXX的临时资源,必须配对Destroy()+Object.Destroy()(或null+Resources.UnloadUnusedAssets())。

5. 高阶技巧与避坑指南:让 LeakDetection 成为你团队的标准 SOP

5.1 将 LeakDetection 集成到 CI/CD 流程,实现泄漏“零容忍”

在 Jenkins 或 GitHub Actions 中,为 Android/iOS 构建任务添加 LeakDetection 自动化检查:

  1. 构建时注入 LeakDetection 启用逻辑
    PostProcessBuild的 iOS/Android 构建回调中,自动向AppController.mm(iOS)或UnityPlayerActivity.java(Android)注入Unity.Profiling.LeakDetection.Enable()调用;

  2. 自动化测试中触发 dump
    编写一个LeakDetectionTest,在UnityTest中模拟用户操作(如打开背包、播放视频、切换场景),然后调用Unity.Profiling.LeakDetection.DumpLog()

  3. 解析日志并断言
    用 Python 脚本解析生成的leak_detection_log.txt,若发现任何size > 1024*1024(1MB)的泄漏,则exit 1,阻断发布流程:

    # ci_leak_check.py leaks = parse_leak_log('leak_detection_log.txt') big_leaks = [l for l in leaks if l['size'] > 1024*1024] if big_leaks: print(f"CRITICAL: Found {len(big_leaks)} leaks > 1MB!") for leak in big_leaks: print(f" - {leak['size']} bytes at {leak['stack'][0]}") sys.exit(1) print("PASS: No big leaks found.")

这样,每次 PR 合并前,CI 都会跑一遍内存压力测试,任何引入新泄漏的代码都无法合入主干。我们团队上线此流程后,Native OOM 崩溃率下降了 92%。

5.2 LeakDetection 与 AddressSanitizer(ASan)的协同使用策略

LeakDetection 擅长“宏观定位”,ASan 擅长“微观验证”。两者不是互斥,而是互补:

  • LeakDetection 用于日常开发与测试:开销低(<1% CPU),可长期开启,快速定位泄漏模块;
  • ASan 用于深度根因分析:开销高(2x CPU,2x 内存),仅在怀疑有use-after-freebuffer-overflow时启用,它能告诉你“这块内存被释放后又被谁写了”。

协同流程:

  1. 用 LeakDetection 发现MetalTexture::Create泄漏;
  2. 用 ASan 构建 Player,复现相同场景;
  3. ASan 日志会显示:ERROR: AddressSanitizer: heap-use-after-free on address 0x000123456789,并给出freed by thread T1 here:previously allocated by thread T1 here:的完整调用栈;
  4. 对比两个日志,就能确认:泄漏是因为MetalTexture::Destroy被调用了两次,第二次释放了已释放的内存。

注意:ASan 在 iOS 上需 Xcode 13+,且只能在 Simulator 上运行;Android 上需 NDK r21+,并在build.gradle中设置android.ndkVersion = "21.4.7075529"android.defaultConfig.ndk.cFlags += "-fsanitize=address"

5.3 一个被忽视的终极技巧:用 LeakDetection 日志反推内存增长曲线

LeakDetection 的日志不仅是“快照”,更是“录像”。它的每条[LEAK]记录都带精确时间戳(毫秒级)。你可以用这些时间戳,绘制出内存泄漏的实时增长曲线:

  1. 解析日志,提取所有[LEAK]行的时间戳和大小;
  2. 按时间戳排序,计算每个时间点的“累计未释放内存”;
  3. 用 Python 的matplotlib绘图:
    import matplotlib.pyplot as plt import pandas as pd # 假设 leaks_df 是包含 'timestamp' (datetime) 和 'size' (int) 的 DataFrame leaks_df['cumsum'] = leaks_df['size'].cumsum() plt.plot(leaks_df['timestamp'], leaks_df['cumsum']) plt.xlabel('Time') plt.ylabel('Cumulative Leaked Memory (bytes)') plt.title('Native Memory Leak Growth Curve') plt.show()

这条曲线能揭示泄漏模式:是线性增长(恒定频率分配)、指数增长(递归或循环嵌套)、还是脉冲式(特定操作触发)。我们曾用此法发现一个 Bug:UI 动画每帧都创建一个新的Vector3[]数组用于顶点偏移,动画持续 10 秒,数组大小 1024,导致 10

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询