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.CreateExternalTexture或GL.IssuePluginEvent)。它解决的不是“有没有泄漏”的模糊判断,而是“哪一行 C++ 代码申请了内存却从未释放”的确定性问题。适合所有中大型 Unity 项目的技术负责人、性能优化工程师、原生插件开发者,以及那些被“莫名 OOM”折磨得连续三周睡不着觉的客户端主程。你不需要改一行业务代码,只需要知道怎么启动它、怎么读它的输出、怎么把日志里的十六进制地址映射回源码——这篇文章就带你走完这三步。
2. LeakDetection 的工作原理与适用边界:它不是万能的,但恰好卡在最关键的缺口上
2.1 它如何“看见”每一次 malloc?——基于内存分配器钩子的零成本监控
LeakDetection 的核心,并非在应用层打补丁式 Hookdlsym("malloc"),而是直接在 Unity 的内存管理器初始化阶段,将自身注册为Unity::MemoryManager的全局分配回调监听器。Unity 的内存分配器本身就是一个高度定制化的分层结构:最底层是平台相关的malloc/VirtualAlloc,中间层是 Unity 的 slab allocator(用于小对象池化),上层是面向引擎模块的专用分配器(如GraphicsMemoryAllocator、AudioMemoryAllocator)。LeakDetection 在MemoryManager::Initialize时,通过MemoryManager::SetAllocationCallback注册一个函数指针,该指针会在每一次调用MemoryManager::Allocate(无论来自 C++ 模块、原生插件,还是 Unity 自身的 Graphics 系统)时被同步触发。
这个回调函数干三件事:
- 记录分配元数据:捕获分配大小(
size_t)、分配类型(kMemoryTypeNative/kMemoryTypeGfx/kMemoryTypeAudio)、分配时间戳(Unity::Time::GetRealTimeSinceStartupMs())、以及最关键——调用栈(Unity::Stacktrace::CaptureStackTrace(32)); - 生成唯一追踪 ID:为本次分配生成一个 64 位哈希值(基于调用栈哈希 + 时间戳 + 随机种子),并将其与分配地址(
void*)建立映射,存入一个线程安全的ConcurrentHashMap; - 延迟写入日志缓冲区:不立即刷盘,而是将结构化日志(含地址、大小、ID、线程ID、调用栈帧)写入一个环形内存缓冲区(RingBuffer),默认大小 16MB,避免 I/O 阻塞主线程。
提示:LeakDetection 的日志缓冲区是内存驻留的,不会因 App 切后台而丢失。这意味着即使你的 App 在后台被系统挂起前发生了泄漏,只要在崩溃前手动触发一次日志 dump(下文详述),就能拿到完整线索。
2.2 它为什么能定位到 C# 调用栈?——Unity 托管-原生调用链的隐式透传
这是很多开发者最大的误解:认为 LeakDetection 只能显示 C++ 的__libc_malloc或operator new。实际上,Unity 的 IL2CPP 编译器在生成 C++ 代码时,对所有涉及原生资源创建的托管 API(如Texture2D.CreateExternalTexture、Mesh.UploadMeshData、AudioClip.Create)都做了特殊处理——在进入原生层前,会主动调用Unity::Stacktrace::PushManagedFrame,将当前 C# 方法名、类名、文件路径(若 PDB 可用)压入一个线程局部的托管调用栈帧栈。LeakDetection 的CaptureStackTrace函数在采集时,会自动合并原生调用栈(backtrace)与这个托管调用栈帧,最终输出的日志中,调用栈顶部永远是 C# 的入口点,中间是 IL2CPP 生成的胶水代码(如Texture2D_CreateExternalTexture_mXXXXX),底部才是真正的 C++ 分配点(如MetalTexture::Create或VulkanImage::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 只记录Allocate和Deallocate事件。realloc若内部实现为malloc+memcpy+free,则旧地址会被标记为“已释放”,新地址为“新分配”,无法关联为同一逻辑对象 | 使用MallocStackLogging(macOS/iOS)或adb shell setprop libc.debug.malloc.options backtrace(Android)获取更底层的分配历史 |
| 符号缺失导致调用栈截断 | 日志中调用栈显示为0x102a3b4c5、0x102a3b4d0等纯地址,无函数名和行号 | LeakDetection 依赖平台的dladdr(iOS/macOS)或android_unwind_get_func_name(Android)解析符号。若构建时未保留调试符号(iOS 未勾选 “Strip Debug Symbols During Copy”,Android 未保留.so的debug目录),则无法反查 | iOS:确保 Xcode Build Settings 中DEBUG_INFORMATION_FORMAT = dwarf-with-dsym且STRIP_INSTALLED_PRODUCT = NO;Android:在gradle.properties中设置android.useDeprecatedNdk=true并保留obj/local/*/libYourPlugin.so |
| 多进程共享内存泄漏 | 使用shm_open/mmap创建的 POSIX 共享内存,或 Android 的ashmem | LeakDetection 只 hook Unity 自己的MemoryManager::Allocate,不介入系统级共享内存 API | 使用ipcs -m(Linux/macOS)或adb shell dumpsys meminfo(Android)监控共享内存段使用量 |
注意:LeakDetection 对
Unity::MemoryManager之外的分配(如std::string的内部malloc、std::shared_ptr的控制块分配)默认不监控。若需覆盖,必须在 C++ 插件中显式调用Unity::MemoryManager::Allocate替代new,或在插件初始化时调用Unity::MemoryManager::SetAllocationCallback二次注册——但这属于高级定制,本文不展开。
3. 从零启动 LeakDetection:三步激活、两种日志导出、一份可读报告
3.1 第一步:确认你的构建环境已满足最低要求
LeakDetection 并非所有 Unity 版本都可用。它最早在 Unity 2019.4.18f1 中作为实验性功能引入,在 Unity 2020.3.30f1 后成为稳定特性。请务必核对以下三项:
- Unity 版本 ≥ 2020.3.30f1(推荐 2021.3.33f1 或更高,修复了 2020.x 中多个日志截断 Bug);
- 构建目标平台支持:目前仅支持iOS(arm64)、Android(arm64-v8a)、macOS(Intel/Apple Silicon)、Windows(x64);不支持 WebGL、Linux Standalone、UWP;
- 构建类型必须为 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 当前状态(推荐)
在设备上触发日志 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 文件)
获取日志文件:
- 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
- iOS:日志会自动保存到
解析日志,生成可读报告:
原生日志是纯文本,格式如下:[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 分钟内定位到AVCaptureSession的setOutput方法在切换摄像头时,重复创建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.”,但它只控制Texture2D的Dispose()行为,不控制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*、CFTypeRef、JNIEnv*等,只要不是 Unity 自己创建的,就必须由你负责释放。LeakDetection 日志中,如果CreateExternalTexture的调用栈底部是CVPixelBufferCreate或malloc,而非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:231是uint8_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.Create、RenderTexture.GetTemporary、Shader.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 自动化检查:
构建时注入 LeakDetection 启用逻辑:
在PostProcessBuild的 iOS/Android 构建回调中,自动向AppController.mm(iOS)或UnityPlayerActivity.java(Android)注入Unity.Profiling.LeakDetection.Enable()调用;自动化测试中触发 dump:
编写一个LeakDetectionTest,在UnityTest中模拟用户操作(如打开背包、播放视频、切换场景),然后调用Unity.Profiling.LeakDetection.DumpLog();解析日志并断言:
用 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-free或buffer-overflow时启用,它能告诉你“这块内存被释放后又被谁写了”。
协同流程:
- 用 LeakDetection 发现
MetalTexture::Create泄漏; - 用 ASan 构建 Player,复现相同场景;
- ASan 日志会显示:
ERROR: AddressSanitizer: heap-use-after-free on address 0x000123456789,并给出freed by thread T1 here:和previously allocated by thread T1 here:的完整调用栈; - 对比两个日志,就能确认:泄漏是因为
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]记录都带精确时间戳(毫秒级)。你可以用这些时间戳,绘制出内存泄漏的实时增长曲线:
- 解析日志,提取所有
[LEAK]行的时间戳和大小; - 按时间戳排序,计算每个时间点的“累计未释放内存”;
- 用 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