Unity红外热成像开发中RenderTexture内存泄漏的排查与优化实战 1. 项目概述当红外热成像遇上RenderTexture内存泄漏做Unity开发尤其是涉及复杂视觉效果的项目红外热成像效果绝对是一个既酷炫又充满挑战的领域。它不仅仅是把画面调成红黄色那么简单背后涉及到热辐射模拟、温度梯度映射、实时后处理等一系列技术。而在实现这些效果时RenderTexture几乎是我们绕不开的核心组件。无论是用于屏幕抓取、多Pass渲染还是实现自定义的后期特效RenderTexture都扮演着“临时画布”的角色。然而就是这个看似简单的“临时画布”却成了无数项目特别是需要长时间运行或频繁切换场景的项目中一个隐蔽的“内存杀手”——内存泄漏。我见过太多案例一个运行流畅的红外热成像Demo在连续切换几次视角或模式后帧率骤降最终导致应用崩溃。排查下来十有八九是RenderTexture没有被正确释放在内存中不断堆积最终拖垮了整个系统。所以今天我们不空谈理论直接切入实战。这篇指南的核心就是解决在Unity中实现红外热成像效果时如何与RenderTexture“安全共处”彻底避免因它引发的内存泄漏问题。无论你是正在开发军事模拟、工业检测、安防监控还是医疗可视化应用只要你的项目用到了类似的热成像视觉特效并且性能要求苛刻那么本文中的排查思路和优化技巧就是你必须要掌握的“保命技能”。我们会从原理拆解到实操排坑一步步把这个问题讲透。2. RenderTexture内存泄漏的根源与红外热成像场景的特殊性要解决问题必须先理解问题是如何产生的。RenderTexture的内存泄漏在Unity中是一个经典问题但在红外热成像这个特定场景下其触发条件和危害会被进一步放大。2.1 RenderTexture的生命周期管理陷阱Unity使用自动垃圾回收GC来管理托管内存如C#对象但对于像Texture、RenderTexture这样的非托管资源GC是无能为力的。这些资源直接由Unity引擎底层或图形API如OpenGL, DirectX管理占用的是显存和/或系统内存。RenderTexture就是一种典型的非托管资源。核心陷阱在于引用与销毁的分离。在C#脚本中我们声明一个RenderTexture变量比如public RenderTexture irRenderTexture;这个变量本身只是一个指向底层非托管资源的“引用”或“句柄”。当我们创建它new RenderTexture(...)或RenderTexture.GetTemporary时Unity会在底层为我们分配真正的显存。当我们不再需要它正确的做法是调用RenderTexture.Release()或RenderTexture.ReleaseTemporary(rt)或者直接销毁这个资源对象Destroy(rt)。这些操作会通知底层图形API释放对应的显存。然而如果我们只是简单地让C#引用变量超出作用域例如局部变量在函数结束时、设置为null或者寄希望于场景切换时自动清理那么底层的非托管资源并不会被释放。这个“引用”消失了但那个“画布”还实实在在地占着显存。这就是内存泄漏的本质未被释放的非托管资源堆积。在红外热成像效果中我们极有可能在多个地方创建RenderTexture主热成像相机一个独立的相机专门渲染热辐射层到RenderTexture。多阶段后处理可能需要多个RenderTexture进行Blit操作例如先渲染热数据再与场景法线、深度图混合最后进行伪彩色映射。动态分辨率或抗锯齿可能根据性能动态创建不同尺寸的RenderTexture。UI或画中画显示在UI上显示一个小的热成像窗口这又需要一个额外的RenderTexture。如果这些创建点没有配对的、可靠的释放逻辑泄漏就会悄然发生。2.2 红外热成像场景如何加剧泄漏风险红外热成像效果本身的特点使得它更容易踩中RenderTexture管理的雷区高频的创建与更新热成像效果往往是动态的。物体温度变化、相机移动、效果模式切换如白热、黑热、彩虹模式都可能触发重新创建或更新RenderTexture。每一次“可能”的创建如果没有妥善管理旧资源就是一次泄漏。复杂的渲染管线交互无论是内置渲染管线还是URP/HDRP集成热成像效果通常意味着要插入自定义的渲染事件如OnRenderImage,RenderPipelineManager.beginCameraRendering。在这些回调函数中创建临时RenderTexture如果回调被多次触发例如相机每帧渲染前而释放逻辑写在了不匹配的地方泄漏就会指数级增长。多相机与多Pass渲染为了将热感信息与可见光场景融合我们常常使用多相机渲染到RenderTexture然后再合并。每个这样的相机都是一个潜在的泄漏点。特别是当相机被动态启用/禁用时其关联的RenderTexture生命周期管理变得复杂。Shader中对RenderTexture的采样在Shader中采样一个已经被释放但C#引用未置空的RenderTexture可能导致图形错误或崩溃但更隐蔽的是持有引用的脚本可能阻止了资源的及时销毁。理解这些特殊性我们就能有的放矢地设计我们的资源管理策略。接下来我们将进入实战环节看看如何系统地构建一个防泄漏的体系。3. 构建防泄漏的RenderTexture管理体系从创建到销毁避免内存泄漏关键在于建立一套清晰、可靠的生命周期管理规则。我们不能依赖运气或Unity的某些未定义行为而必须主动、显式地控制每一个RenderTexture的生死。3.1 创建策略明确所有权与作用域首先要审视每一个RenderTexture的创建语句问自己三个问题谁需要它需要多久谁来负责清理1. 临时性RenderTexture单次渲染操作内使用这是最常用也最容易出错的情况。例如在OnRenderImage中做一个简单的图像效果。void OnRenderImage(RenderTexture source, RenderTexture destination) { // 错误示范每帧都new一个永远不释放 // RenderTexture tempRT new RenderTexture(source.width, source.height, 0); // 正确做法使用GetTemporary RenderTexture tempRT RenderTexture.GetTemporary(source.width, source.height, 0, source.format); // ... 进行一些Blit操作 ... Graphics.Blit(source, tempRT, someMaterial); Graphics.Blit(tempRT, destination); // 关键使用完毕后立即释放 RenderTexture.ReleaseTemporary(tempRT); }核心要点对于单次渲染Pass内使用的中间RT必须使用RenderTexture.GetTemporary。Unity内部维护了一个RenderTexture对象池GetTemporary从池中获取或创建一个RTReleaseTemporary将其归还池中。这能极大减少重复创建销毁的开销并避免泄漏。绝对不要在频繁调用的渲染函数里使用new RenderTexture。2. 持久性RenderTexture跨帧使用例如你的红外热成像相机需要一直渲染到一个固定的RT上供其他Shader或UI使用。public class ThermalCameraController : MonoBehaviour { private RenderTexture _persistentThermalRT; public Camera thermalCamera; void Start() { // 在明确的初始化点创建 _persistentThermalRT new RenderTexture(1024, 768, 24, RenderTextureFormat.ARGBHalf); _persistentThermalRT.name PersistentThermalRT; // 命名便于调试 thermalCamera.targetTexture _persistentThermalRT; } void OnDestroy() { // 在明确的销毁点释放 ReleasePersistentRT(); } void OnDisable() { // 如果脚本禁用也可能需要释放视逻辑而定 // ReleasePersistentRT(); } private void ReleasePersistentRT() { if (_persistentThermalRT ! null) { thermalCamera.targetTexture null; // 先解除相机绑定 _persistentThermalRT.Release(); // 释放底层资源 Destroy(_persistentThermalRT); // 销毁Unity对象 _persistentThermalRT null; } } // 如果需要动态改变RT尺寸必须先释放旧的 public void ResizeThermalRT(int newWidth, int newHeight) { ReleasePersistentRT(); _persistentThermalRT new RenderTexture(newWidth, newHeight, 24, RenderTextureFormat.ARGBHalf); thermalCamera.targetTexture _persistentThermalRT; } }核心要点对于持久性RT要像管理一个重要的游戏对象一样管理它。创建点Start,Awake, 或某个初始化函数和销毁点OnDestroy,OnDisable, 或某个清理函数必须严格对应。在销毁前务必先将其从任何使用它的地方如camera.targetTexture,Material.SetTexture解绑。3.2 销毁策略不留任何死角创建是开始销毁才是保证安全的终点。以下是必须覆盖的销毁场景脚本销毁时 (OnDestroy)这是最主要的清理点。确保所有该脚本创建的持久性RT都被释放。脚本禁用时 (OnDisable)如果你的热成像效果是可以通过开关控制的那么在禁用时释放RT可以立即回收资源。重新启用时再创建。这是一种“按需加载”的策略对移动端或资源紧张的环境特别有用。场景切换时如果RT是场景级别的资源确保在场景卸载前例如在SceneManager.sceneUnloaded事件中进行清理。不要依赖自动加载新场景时清理旧资源这种行为不可靠。RT尺寸或格式需要变更时在调用new RenderTexture创建新RT之前必须先释放旧的RT。这是ResizeThermalRT方法中展示的“先释后建”原则。3.3 使用对象池管理高频动态RT对于红外热成像中可能需要的多种不同尺寸或格式的临时RT例如用于不同LOD级别或后处理阶段可以建立一个简单的对象池来管理避免GetTemporary和ReleaseTemporary的频繁调用也能更好地控制生命周期。using System.Collections.Generic; public class RenderTexturePool { private Dictionarystring, StackRenderTexture _pool new Dictionarystring, StackRenderTexture(); private string GetKey(int width, int height, int depth, RenderTextureFormat format) { return ${width}x{height}_{depth}_{format}; } public RenderTexture Get(int width, int height, int depth 0, RenderTextureFormat format RenderTextureFormat.ARGB32) { string key GetKey(width, height, depth, format); if (_pool.TryGetValue(key, out StackRenderTexture stack) stack.Count 0) { return stack.Pop(); } // 池中没有创建新的 RenderTexture rt new RenderTexture(width, height, depth, format); rt.name $PooledRT_{key}; return rt; } public void Release(RenderTexture rt) { if (rt null) return; // 重置RT状态可选但建议 rt.DiscardContents(); string key GetKey(rt.width, rt.height, rt.depth, rt.format); if (!_pool.ContainsKey(key)) { _pool[key] new StackRenderTexture(); } _pool[key].Push(rt); } // 在合适的时机如关卡结束、游戏退出清空整个池释放所有资源 public void ClearPool() { foreach (var stack in _pool.Values) { while (stack.Count 0) { RenderTexture rt stack.Pop(); if (rt ! null) { rt.Release(); Destroy(rt); } } } _pool.Clear(); } }这个池子可以在你的热成像管理器中被全局访问确保所有动态RT的创建和回收都经过池子从而完全掌控其生命周期。4. 红外热成像效果实现中的关键检查点与排坑实录有了管理体系我们还需要在具体的红外热成像效果实现代码中嵌入一系列检查点主动防御泄漏。下面结合几个典型场景进行说明。4.1 场景一基于相机与Shader的热成像生成器这是最常见的架构一个独立的ThermalCamera渲染到RenderTexture一个Shader根据物体的“热值”生成灰度图最后用一个后处理Shader将灰度图转为伪彩色。泄漏风险点ThermalCamera脚本中的RenderTexture引用未在OnDestroy中释放。后处理材质Material中引用了这个RT但材质是动态创建的且未管理。切换热成像模式时创建了新的RT和材质旧的没有释放。加固后的代码框架public class AdvancedThermalRenderer : MonoBehaviour { public Shader thermalGenerationShader; public Shader falseColorShader; private Camera _thermalCam; private RenderTexture _thermalRT; private Material _thermalGenMaterial; private Material _falseColorMaterial; void Start() { _thermalCam GetComponentCamera(); InitializeResources(); } void InitializeResources() { // 1. 创建持久RT _thermalRT new RenderTexture(Screen.width / 2, Screen.height / 2, 24, RenderTextureFormat.RHalf); _thermalCam.targetTexture _thermalRT; // 2. 创建材质球并标记为“不保存”防止场景切换时残留 _thermalGenMaterial new Material(thermalGenerationShader); _thermalGenMaterial.hideFlags HideFlags.DontSave; // 重要 _falseColorMaterial new Material(falseColorShader); _falseColorMaterial.hideFlags HideFlags.DontSave; // 3. 将RT传递给后处理材质 _falseColorMaterial.SetTexture(_ThermalTex, _thermalRT); } void OnRenderImage(RenderTexture src, RenderTexture dest) { // 使用临时RT进行后处理融合 RenderTexture compositeRT RenderTexture.GetTemporary(src.width, src.height, 0); // 假设这里是融合热成像图和原始画面的逻辑 Graphics.Blit(src, compositeRT, _falseColorMaterial); Graphics.Blit(compositeRT, dest); RenderTexture.ReleaseTemporary(compositeRT); } void OnDestroy() { CleanupResources(); } void OnDisable() { // 如果效果可关闭禁用时也清理 CleanupResources(); } void CleanupResources() { // 顺序很重要先解绑再释放RT最后销毁材质 if (_thermalCam ! null) _thermalCam.targetTexture null; if (_thermalRT ! null) { _thermalRT.Release(); Destroy(_thermalRT); _thermalRT null; } if (_falseColorMaterial ! null) { Destroy(_falseColorMaterial); _falseColorMaterial null; } if (_thermalGenMaterial ! null) { Destroy(_thermalGenMaterial); _thermalGenMaterial null; } } // 动态切换热成像模式示例 public void SwitchToRainbowMode() { // 加载新的Shader Shader newFalseColorShader Shader.Find(Custom/RainbowThermal); if (newFalseColorShader ! null _falseColorMaterial ! null) { // 创建新材质 Material newMat new Material(newFalseColorShader); newMat.hideFlags HideFlags.DontSave; newMat.SetTexture(_ThermalTex, _thermalRT); // 复用已有的RT // 销毁旧材质 Destroy(_falseColorMaterial); _falseColorMaterial newMat; } } }避坑心得动态创建的Material也是一个资源必须手动管理。为其设置HideFlags.DontSave是个好习惯这能防止它在场景切换时意外地留存在内存中。清理顺序上务必先断开相机或材质对RT的引用再释放RT本身。4.2 场景二在URP中集成热成像后处理在URP中我们通常通过ScriptableRendererFeature来插入自定义渲染通道。这里管理RT的生命周期需要格外小心因为RenderTexture.GetTemporary在CommandBuffer中可能有不同的行为。关键实现与检查点public class ThermalRenderFeature : ScriptableRendererFeature { class ThermalRenderPass : ScriptableRenderPass { private RenderTexture _thermalRT; private Material _compositeMaterial; private string _profilerTag; public ThermalRenderPass(Material compositeMat, string tag) { _compositeMaterial compositeMat; _profilerTag tag; renderPassEvent RenderPassEvent.BeforeRenderingPostProcessing; // 选择合适的时机 } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { // 在Configure中创建RT确保与相机设置同步如MSAA、HDR if (_thermalRT null) { // 根据相机描述符创建RT确保格式兼容 RenderTextureDescriptor rtDesc cameraTextureDescriptor; rtDesc.colorFormat RenderTextureFormat.RHalf; // 可能使用半精度浮点数存储热强度 rtDesc.depthBufferBits 0; // 不需要深度 rtDesc.msaaSamples 1; // 后处理RT通常不需要MSAA _thermalRT RenderTexture.GetTemporary(rtDesc); // 使用GetTemporary _thermalRT.name URP_ThermalRT; } } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd CommandBufferPool.Get(_profilerTag); // 1. 这里假设有一个渲染热数据的Pass将结果绘制到_thermalRT // cmd.SetRenderTarget(_thermalRT); // ... 绘制热几何体 ... // 2. 将热成像图与相机颜色混合 if (_compositeMaterial ! null) { // 从相机获取当前颜色RT RenderTargetIdentifier cameraColor renderingData.cameraData.renderer.cameraColorTarget; // 创建一个临时RT用于混合结果 RenderTextureDescriptor desc renderingData.cameraData.cameraTargetDescriptor; int tempRTId Shader.PropertyToID(_TempThermalComposite); cmd.GetTemporaryRT(tempRTId, desc); // 使用材质进行Blit混合 cmd.Blit(cameraColor, tempRTId, _compositeMaterial, 0); // 假设材质使用_thermalRT cmd.Blit(tempRTId, cameraColor); cmd.ReleaseTemporaryRT(tempRTId); // 释放临时RT } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } // 至关重要的方法URP会在每帧渲染后或Pass不再需要时调用 public override void FrameCleanup(CommandBuffer cmd) { if (_thermalRT ! null) { RenderTexture.ReleaseTemporary(_thermalRT); // 释放GetTemporary创建的RT _thermalRT null; } } } }URP避坑指南在URP中Configure和FrameCleanup是你的生死线。GetTemporary创建的RT必须在同一帧的FrameCleanup中通过ReleaseTemporary释放。即使你的Pass不是每帧都执行也必须在FrameCleanup中做释放检查因为URP可能会跳过Execute但依然调用FrameCleanup。永远不要在Execute中创建而不释放。4.3 场景三在UIRawImage上显示热成像画中画这要求将RenderTexture赋值给一个RawImage.texture。泄漏风险点UI元素可能跨场景存在如DontDestroyOnLoad如果UI引用了某个场景中的RT而该场景被销毁了RT会因为被UI引用而无法释放。安全做法public class ThermalPictureInPictureUI : MonoBehaviour { public RawImage thermalDisplay; private RenderTexture _uiThermalRT; void Start() { // 由UI组件自己创建和管理RT _uiThermalRT new RenderTexture(256, 256, 0); thermalDisplay.texture _uiThermalRT; // 从热成像系统获取数据并更新_uiThermalRT例如通过一个共享管理器 ThermalSystemManager.Instance.RegisterPIPOutput(_uiThermalRT); } void OnDestroy() { // 在UI销毁时清理自己创建的RT if (_uiThermalRT ! null) { thermalDisplay.texture null; // 先断开UI引用 ThermalSystemManager.Instance?.UnregisterPIPOutput(_uiThermalRT); // 通知系统不再更新 _uiThermalRT.Release(); Destroy(_uiThermalRT); _uiThermalRT null; } } }核心原则谁创建谁销毁。如果RT是专门为某个UI创建的那么生命周期就应该由该UI组件管理。避免一个场景的对象持有另一个场景对象创建的RT引用。通过一个中心化的管理器如ThermalSystemManager来协调数据更新而不是直接传递RT引用可以降低耦合度和泄漏风险。5. 内存泄漏诊断工具与实战排查流程即使遵循了所有最佳实践复杂的项目中仍可能出现难以察觉的泄漏。这时我们需要借助工具进行诊断。5.1 Unity Profiler你的第一道防线Unity Profiler的Memory模块是排查RenderTexture泄漏最直观的工具。打开Deep Profile在Profiler窗口的Memory区域确保勾选“Detailed”或使用“Take Sample”功能。观察Texture内存趋势运行你的应用特别是重复执行可能引发泄漏的操作如开关热成像效果、切换场景。在Profiler的Memory区域关注“Texture Memory”或“Graphics Memory”的总量是否持续增长而不回落。定位具体资源在采集的Snapshot中找到“Textures”列表按内存大小排序。寻找那些名字可疑如包含“Thermal”、“Temp”、“RenderTexture”且数量异常多的纹理。点击其中一个在下方“Reference”面板中查看是哪些对象引用了它。如果引用者是一个你已经认为销毁了的脚本或GameObject那就是泄漏的铁证。使用Compare功能在疑似泄漏操作前后各抓取一个内存快照然后使用Compare功能。这能清晰地列出新分配的、未被释放的资源快速定位问题源头。5.2 自定义调试与日志在代码中添加简单的日志输出可以帮助你追踪RT的生命周期。public static class RenderTextureDebugger { public static int activeRenderTextureCount 0; public static RenderTexture CreateTrackedRT(int width, int height, int depth, RenderTextureFormat format, string creator) { var rt new RenderTexture(width, height, depth, format); rt.name ${creator}_{rt.GetInstanceID()}; activeRenderTextureCount; Debug.Log($[RT Created] {rt.name}. Total Active RTs: {activeRenderTextureCount}); return rt; } public static void ReleaseTrackedRT(RenderTexture rt) { if (rt null) return; Debug.Log($[RT Releasing] {rt.name}); rt.Release(); Destroy(rt); activeRenderTextureCount--; Debug.Log($[RT Released] {rt.name}. Total Active RTs: {activeRenderTextureCount}); } }将项目中所有new RenderTexture和释放操作替换为这个调试类的方法你就能在Console中清晰地看到RT的创建和销毁是否成对出现总数是否稳定。5.3 系统化排查流程当怀疑存在泄漏时遵循以下步骤复现问题找到一个能稳定导致内存增长的操作序列例如打开热成像 - 关闭 - 再打开循环10次。使用Profiler验证执行该序列观察“Texture Memory”是否阶梯式上涨。确认是RenderTexture问题。代码审查检查所有创建RenderTexture的地方搜索new RenderTexture和GetTemporary。对于new创建的检查是否有配对的Release()/Destroy()且调用路径在对应生命周期OnDestroy,OnDisable中一定能执行到。对于GetTemporary创建的检查是否有配对的ReleaseTemporary()且确保在同一个渲染帧或Pass的清理阶段被调用。检查引用链在Profiler中找到未被释放的RT查看其引用者。常见问题包括被某个Material的SetTexture引用而该材质未被销毁。被某个Camera的targetTexture引用而相机未重置。被某个静态类或单例管理器持有而该管理器没有清理逻辑。隔离测试如果项目复杂尝试创建一个新的、最简化的场景只包含热成像的核心功能重复测试。如果简化场景中无泄漏则问题出在项目其他系统的交互上。修复与验证修复找到的问题后重复步骤1和2确保内存曲线不再增长并最终稳定在一个合理水平。6. 进阶优化与最佳实践汇编解决了泄漏问题我们可以进一步优化RenderTexture在红外热成像中的使用提升性能和稳定性。6.1 根据性能动态调整RT分辨率红外热成像图通常不需要和主画面一样的分辨率。动态调整可以节省大量显存和带宽。public class DynamicThermalResolution : MonoBehaviour { public Camera thermalCamera; private RenderTexture _thermalRT; private int _currentWidth, _currentHeight; void Update() { // 根据目标帧率或设备性能动态计算所需分辨率 int targetWidth CalculateTargetWidthBasedOnPerformance(); int targetHeight CalculateTargetHeightBasedOnPerformance(); if (targetWidth ! _currentWidth || targetHeight ! _currentHeight) { ResizeThermalRT(targetWidth, targetHeight); } } void ResizeThermalRT(int newWidth, int newHeight) { if (_thermalRT ! null) { thermalCamera.targetTexture null; _thermalRT.Release(); Destroy(_thermalRT); } _thermalRT new RenderTexture(newWidth, newHeight, 16, RenderTextureFormat.RHalf); thermalCamera.targetTexture _thermalRT; _currentWidth newWidth; _currentHeight newHeight; // 通知所有使用此RT的Shader或材质更新引用如果需要 Shader.SetGlobalTexture(_GlobalThermalTex, _thermalRT); } }6.2 利用RenderTextureFormat节省带宽红外热成像数据通常是一个单通道的强度值温度。使用RenderTextureFormat.RHalf半精度浮点数或RenderTextureFormat.R88位整型来代替默认的ARGB32可以将纹理内存和带宽占用减少至1/4或1/16对性能提升巨大尤其是在移动端。6.3 避免在每帧的Update中创建临时RT这是一个低级但常见的错误。任何在Update、LateUpdate中无缓冲地创建RT的逻辑都是危险的。确保这类操作要么被移到按需调用的函数中要么使用对象池进行管理。6.4 在场景卸载前进行集中清理创建一个全局的资源管理器在游戏退出或场景切换时强制清理所有由热成像系统创建的RT和Material。public class ThermalResourceManager : MonoBehaviour { public static ThermalResourceManager Instance; private HashSetRenderTexture _managedRTs new HashSetRenderTexture(); private HashSetMaterial _managedMats new HashSetMaterial(); void Awake() { Instance this; } public void RegisterRT(RenderTexture rt) { _managedRTs.Add(rt); } public void RegisterMat(Material mat) { _managedMats.Add(mat); } public void CleanupAll() { foreach (var rt in _managedRTs) { if (rt ! null) { rt.Release(); Destroy(rt); } } _managedRTs.Clear(); foreach (var mat in _managedMats) { if (mat ! null) Destroy(mat); } _managedMats.Clear(); } void OnApplicationQuit() { CleanupAll(); } // 也可以监听场景切换事件 }在你的各个热成像组件中创建RT和Material后向这个管理器注册。这提供了一个最后的安全网。6.5 编写自动化测试对于核心的热成像模块可以编写简单的PlayMode测试在编辑器中自动执行“创建-使用-销毁”循环数十上百次并使用Assert来检查RenderTexture.active的数量或纹理内存是否在预期范围内将内存泄漏的风险扼杀在开发阶段。