Unity Native内存泄漏检测:LeakDetection实战指南 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::vectoruint8_t* g_Buffer new std::vectoruint8_t在main()之前分配或static void* s_Pool malloc(1024*1024)在 DLL 加载时分配LeakDetection 的回调注册发生在MemoryManager::Initialize即 Unity Player 初始化阶段晚于全局构造函数执行时机使用 AddressSanitizerASan构建 iOS/Android Player或在 Xcode 的 Scheme 中启用 “Enable Thread Sanitizer”内存重用型泄漏一块 10MB 的malloc内存被反复memset清零、memcpy覆盖但从未free或一个std::vector持续push_back导致内部 buffer 多次realloc旧 buffer 未释放LeakDetection 只记录Allocate和Deallocate事件。realloc若内部实现为mallocmemcpyfree则旧地址会被标记为“已释放”新地址为“新分配”无法关联为同一逻辑对象使用MallocStackLoggingmacOS/iOS或adb shell setprop libc.debug.malloc.options backtraceAndroid获取更底层的分配历史符号缺失导致调用栈截断日志中调用栈显示为0x102a3b4c5、0x102a3b4d0等纯地址无函数名和行号LeakDetection 依赖平台的dladdriOS/macOS或android_unwind_get_func_nameAndroid解析符号。若构建时未保留调试符号iOS 未勾选 “Strip Debug Symbols During Copy”Android 未保留.so的debug目录则无法反查iOS确保 Xcode Build Settings 中DEBUG_INFORMATION_FORMAT dwarf-with-dsym且STRIP_INSTALLED_PRODUCT NOAndroid在gradle.properties中设置android.useDeprecatedNdktrue并保留obj/local/*/libYourPlugin.so多进程共享内存泄漏使用shm_open/mmap创建的 POSIX 共享内存或 Android 的ashmemLeakDetection 只 hook Unity 自己的MemoryManager::Allocate不介入系统级共享内存 API使用ipcs -mLinux/macOS或adb shell dumpsys meminfoAndroid监控共享内存段使用量注意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构建目标平台支持目前仅支持iOSarm64、Androidarm64-v8a、macOSIntel/Apple Silicon、Windowsx64不支持 WebGL、Linux Standalone、UWP构建类型必须为 Development Build在File Build Settings中勾选Development Build且Script Debugging可选不影响 LeakDetection但有助于后续栈帧解析。验证是否生效构建完成后在 Player 的Data/Managed/目录下查找UnityLeakDetection.dllWindows或libUnityLeakDetection.soAndroid或UnityLeakDetection.frameworkiOS。若存在说明引擎已打包该模块若不存在检查 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 * 102432MB因为一次复杂场景的泄漏可能产生数千次分配第二个参数true至关重要它告诉 LeakDetection 在捕获调用栈时尝试解析 C# 源码文件名和行号需 PDB 符号文件存在。3.3 第三步触发日志导出与解析——两种实战场景的完整流程LeakDetection 的日志不会自动写入文件必须由你主动触发。以下是两个最常用场景的操作链场景一App 运行中疑似泄漏需手动 dump 当前状态推荐在设备上触发日志 dumpiOS连接 Xcode → Window → Devices and Simulators → 选择你的设备 → 点击右下角 “View Device Logs” → 在 App 运行时向左滑动 App 图标点击 “i” → 在弹出菜单中选择 “Dump Leak Detection Log”。Android通过 ADB 执行命令需设备已 root 或开启adb rootadb 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.logAndroid日志保存在/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解析日志生成可读报告原生日志是纯文本格式如下[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(keylambda x: x[size], reverseTrue) return leaks if __name__ __main__: leaks parse_leak_log(./leak.log) print(fFound {len(leaks)} leaks. Top 5 by size:) for i, leak in enumerate(leaks[:5]): print(f{i1}. {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::CreateLeakDetection 日志显示大量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::DeallocateC 层经典错误现象自研音频解码插件在 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_NEWWindows或#define malloc DEBUG_MALLOCmacOS宏替换强制所有分配走自定义钩子。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.FindObjectsOfTypeTexture2D()扫描到进一步阻碍 GC。修复方案// 错误创建后仅 Destroy() Texture2D tempTex Texture2D.Create(256, 256, TextureFormat.RGBA32, false); // ... use tempTex ... tempTex.Destroy(); // 只释放 GPUTexture2D 对象还在托管堆 // 正确创建时设置 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()或nullResources.UnloadUnusedAssets()。5. 高阶技巧与避坑指南让 LeakDetection 成为你团队的标准 SOP5.1 将 LeakDetection 集成到 CI/CD 流程实现泄漏“零容忍”在 Jenkins 或 GitHub Actions 中为 Android/iOS 构建任务添加 LeakDetection 自动化检查构建时注入 LeakDetection 启用逻辑在PostProcessBuild的 iOS/Android 构建回调中自动向AppController.mmiOS或UnityPlayerActivity.javaAndroid注入Unity.Profiling.LeakDetection.Enable()调用自动化测试中触发 dump编写一个LeakDetectionTest在UnityTest中模拟用户操作如打开背包、播放视频、切换场景然后调用Unity.Profiling.LeakDetection.DumpLog()解析日志并断言用 Python 脚本解析生成的leak_detection_log.txt若发现任何size 1024*10241MB的泄漏则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(fCRITICAL: 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 与 AddressSanitizerASan的协同使用策略LeakDetection 擅长“宏观定位”ASan 擅长“微观验证”。两者不是互斥而是互补LeakDetection 用于日常开发与测试开销低1% CPU可长期开启快速定位泄漏模块ASan 用于深度根因分析开销高2x CPU2x 内存仅在怀疑有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 -fsanitizeaddress。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()这条曲线能揭示泄漏模式是线性增长恒定频率分配、指数增长递归或循环嵌套、还是脉冲式特定操作触发。我们曾用此法发现一个 BugUI 动画每帧都创建一个新的Vector3[]数组用于顶点偏移动画持续 10 秒数组大小 1024导致 10