ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity UI Overdraw优化:解决手机发烫与GPU过载

2026/9/15 20:06:45 拓冰建站 浏览量
Unity UI Overdraw优化:解决手机发烫与GPU过载 1. 项目概述为什么“同一面墙刷了 N 遍漆”是 Unity 里最隐蔽的发烫元凶你有没有遇到过这样的情况游戏在手机上跑着跑着机身明显发烫电量掉得飞快但 CPU 占用率才 30%GPU 占用率却飙到 95% 以上帧率还卡在 30 帧上不去打开 Profiler 一看GPU Time 柱状图像一座座小山峰可 Render、Shadow、Lighting 这些常规模块的耗时加起来居然只占 GPU 总耗时的一半不到——剩下那一大块“幽灵时间”既没报错也没显眼的函数名就像空气一样悬在那里。这时候八成是 Overdraw 在作祟。它不是 bug不是代码写错了而是你亲手把一堵墙用不同图层、不同 Canvas、不同 UI 元素一层一层地、反复地、毫无察觉地“刷了 N 遍漆”。Unity 的渲染管线本质上是个“画家”。它不会聪明地跳过已经被画满的区域而是老老实实按顺序从后往前或按 Canvas 的 Sorting Order把所有该画的东西一遍又一遍地往同一个像素点上“盖章”。一个像素点被盖了 5 次GPU 就要为它执行 5 次像素着色器计算、5 次深度测试、5 次 Alpha 混合——哪怕第 2 到第 5 次的结果最终都被第 1 次盖住完全不可见。这就像你给一面白墙刷漆第一遍底漆打完墙已经不透光了第二遍面漆上去墙更亮了第三遍、第四遍……你只是在徒劳地增加漆料消耗和干燥时间而视觉效果毫无变化。GPU 干的正是这种活儿而且它干得越卖力手机就越热电池就越虚。这个现象在 UI 场景中尤其致命。一个简单的“设置面板”可能包含背景图、毛玻璃遮罩、标题文字、多个按钮、滑动条 Thumb、滑动条 Track、滑动条 Fill、图标、状态提示文本、甚至一个半透明的蒙版 Canvas——它们层层叠叠绝大多数像素点都经历了 6~10 次重绘。而“unity做一个滑动条”、“unity如何扩大按钮的点击范围”这类需求往往通过叠加空 GameObject、增大 RectTransform 的 size 或添加透明 Image 来实现这恰恰是在主动制造 Overdraw。它不报错不崩溃但会悄无声息地把你的设备变成暖手宝。本篇就聚焦于这个“看不见的性能杀手”不讲大道理只拆解真实项目里最常踩的坑、最有效的检测手段、最立竿见影的优化方案以及那些连官方文档都懒得提的底层细节。2. Overdraw 的本质与量化不是“画多了”而是“算多了”2.1 真正的敌人不是“画”而是“计算”很多人误以为 Overdraw 是“画了太多东西”所以第一反应是删 UI、减特效。这是个根本性误解。Overdraw 的核心问题从来不是“画得多”而是“为同一个像素重复计算了太多次”。GPU 的像素着色器Pixel Shader是并行计算单元它的吞吐量是有限的。当一个像素被 8 次覆盖时GPU 就必须为它运行 8 次完整的着色器程序包括纹理采样、数学运算、分支判断哪怕只是if (alpha 0.1) discard;、Alpha 混合等所有步骤。每一次计算都消耗着 GPU 的 ALU算术逻辑单元、纹理单元、内存带宽和功耗。举个具体例子一个 1080p 屏幕有 2,073,600 个像素。如果平均 Overdraw 是 4x那么 GPU 实际需要处理的像素总量就是 2,073,600 × 4 8,294,400 个。这相当于 GPU 要在 16ms 内60fps完成将近 830 万次独立的像素级计算。而现代移动 GPU 的填充率Fill Rate通常在 10~20 GPixel/s十亿像素每秒之间。表面看似乎绰绰有余但这里有个关键陷阱填充率是理论峰值实际能达到的往往只有峰值的 30%~50%。因为真实场景中纹理缓存命中率、内存带宽瓶颈、着色器复杂度、分支预测失败等因素会严重拖慢实际速度。当 Overdraw 把有效填充率压到临界点以下GPU 就会“忙不过来”开始排队等待最终表现为 GPU Time 暴涨、帧率骤降、设备发热。2.2 Unity 中的 Overdraw 可视化不止一种“刷漆”视角Unity 提供了多种内置工具来可视化 Overdraw但它们的原理和适用场景截然不同用错一个就会得出错误结论。Frame Debugger 的 Overdraw 模式这是最精确的调试方式。它会暂停在某一帧逐个绘制 Draw Call并用颜色叠加显示每个像素被绘制的次数。红色代表高 Overdraw如 8x绿色代表低如 1x。它的优势在于“所见即所得”能精准定位到是哪个 UI 元素、哪个 Mesh、哪张贴图导致了问题。劣势是无法实时观察且在复杂 UI 中颜色叠加会让画面变得一片混沌难以分辨具体是哪一层。Scene View 的 Overdraw 模式Render Mode → Overdraw这是日常开发中最常用的。它会在 Scene 视图中实时渲染一个伪彩色图颜色深浅代表该位置的 Overdraw 值。但它有一个致命缺陷它只计算 Camera 渲染的物体完全无视 Canvas 和 UI 元素这意味着你在 Scene View 里看到一片绿色低 OverdrawUI 却可能正在后台疯狂地刷着 10x 的漆。很多开发者因此误判以为 UI 没问题转而去优化 3D 模型结果发现发烫问题纹丝不动。Profiler 的 GPU Time Rendering Stats这是最实用的“诊断金标准”。在 Profiler 的 GPU 时间线里找到DrawCall或Gfx.WaitForPresent附近的一个长条右键选择 “View Frame Debugging”。然后在 Frame Debugger 中切换到 “Overdraw” 模式。同时在 Profiler 的 Rendering 面板下重点关注Draw Calls、Tris、Verts和Saved by batching这几项。一个健康的 UI 场景Draw Calls应该远低于Tris三角形数因为 UI 的顶点数极少但 Draw Call 很多而Saved by batching应该是一个很大的正数表示合批成功。如果Draw Calls高得离谱Saved by batching却是负数或很小那基本可以断定是 UI Overdraw 和合批失败共同作祟。提示不要迷信单一工具。正确的流程是先用 Profiler 看到 GPU Time 高企 → 切换到 Frame Debugger 的 Overdraw 模式锁定高亮区域 → 回到 Scene View选中该区域的 UI 元素检查其 Canvas、Sorting Order、Material、Image TypeSliced/Full等属性。这是一个闭环的排查链。2.3 UI 层的 Overdraw比 3D 更危险的“隐形战场”UI 是 Overdraw 的重灾区原因有三层级结构天然嵌套一个Canvas下挂PanelPanel下挂ButtonButton下又有Image背景、Text标题、Image图标——这本身就是三层嵌套。如果每个Image都用了Color.a 0.9的半透明那么最底层的像素就要被计算三次。Canvas 的“独立世界”特性每个Canvas都是一个独立的渲染上下文。即使两个 Canvas 完全重叠Unity 也会为它们分别生成 Draw Call并分别进行像素计算。一个World Space的 Canvas 和一个Screen Space - Overlay的 Canvas 叠在一起Overdraw 就是 2x 起步。“无害”的 UI 组件最致命比如Mask组件。它看起来只是“裁剪一下”但背后是创建了一个额外的 Render Texture并执行一次完整的渲染过程来生成 Mask 图然后再用这张图去裁剪目标内容。这意味着被 Mask 的区域至少要被绘制两次一次生成 Mask一次应用 Mask。RectMask2D相对轻量但Mask基于Graphic的开销巨大。同理“unity 如何扩大按钮的点击范围”这个问题如果用一个超大尺寸的透明Image作为Button的子物体来实现这个透明Image就成了一个完美的 Overdraw 制造机——它没有视觉输出却消耗着 GPU 的每一滴算力。3. 核心优化策略与实操要点从“刷漆”到“一次成型”3.1 Canvas 分层与合并砍掉不必要的“刷漆工”Canvas 是 UI 渲染的根节点也是 Overdraw 的源头。优化的第一步就是对 Canvas 进行外科手术式的精简。原则一一个屏幕一个主 Canvas。这是铁律。不要为了“方便管理”就给每个功能模块设置、背包、商店都建一个 Canvas。它们应该共享同一个 Canvas通过GameObject.SetActive(true/false)来控制显示/隐藏。每个额外的 Canvas都会带来至少一次额外的完整渲染流程。原则二分离“静态”与“动态”。将 UI 中几乎不变化的部分如背景图、固定标题栏、底部导航栏提取到一个Static Canvas中并将其Render Mode设为Screen Space - OverlaySorting Order设为一个较低的值如 0。将频繁变化的部分如滚动列表、弹窗、动画元素放在另一个Dynamic Canvas中Sorting Order设为更高如 10。这样静态部分只需渲染一次动态部分的变化不会触发静态部分的重绘。原则三消灭“幽灵 Canvas”。检查项目中是否存在仅用于组织层级、本身没有任何 UI 组件Image、Text的空Canvas。这些空 Canvas 不仅不贡献任何视觉还会强制 Unity 为其创建渲染上下文。解决方案直接删除或者将子物体的父级改为一个普通的Empty GameObject。实操案例一个电商 App 的商品详情页。原始结构是RootCanvasOverlay→BackgroundImageRootCanvas→HeaderCanvasOverlay→TitleText,BackButtonRootCanvas→ContentCanvasOverlay→ScrollView→ProductList。优化后RootCanvasOverlay→BackgroundImage,TitleText,BackButton,ScrollView。HeaderCanvas和ContentCanvas被彻底移除所有子物体直接挂在RootCanvas下。Sorting Order统一设为 0因为它们都在同一层。此举直接将该页面的 Draw Calls 从 42 降到了 18。注意Canvas的Render Mode选择至关重要。Screen Space - Overlay最省资源因为它直接画在屏幕最上层无需深度测试Screen Space - Camera需要与 3D 场景混合开销稍大World Space开销最大因为它要参与完整的 3D 渲染管线。除非你真的需要 UI 跟随 3D 物体旋转如 Pico4 开发中的 HUD否则一律用Overlay。3.2 UI 元素的“瘦身”与“合体”让每一笔都物有所值单个 UI 元素的优化是降低 Overdraw 的微观基础。Image 组件的终极奥义Filled模式与Alpha Cutoff。Image的Type属性有Simple、Sliced、Tiled、Filled四种。Sliced和Tiled为了保证缩放时边缘和中心的正确性会生成更多的顶点和更复杂的着色器从而增加 GPU 计算负担。对于纯色背景、渐变色块务必使用Simple类型并勾选Preserve Aspect如果需要。更重要的是Image的Material。默认的UI/Default材质是半透明的Blend Mode: SrcAlpha OneMinusSrcAlpha这意味着它会进行 Alpha 混合哪怕Color.a 1。而 Alpha 混合是 Overdraw 的放大器。解决方案为所有不透明的 UI 元素纯色背景、不透明图标创建一个自定义材质Shader选用UI/Unlit/Transparent并在 Inspector 中将Rendering Mode改为Opaque。这样GPU 就会启用深度测试Z-Test一旦某个像素被前面的不透明物体画满后面的物体就完全被剔除不再进行任何计算。这是“一次成型”的核心。Text 组件的“字体纹理”陷阱。Text组件依赖Font资源生成的Texture Atlas字体图集。如果图集过大包含了上千个字符每次渲染一个字GPU 都要从巨大的纹理中采样造成纹理缓存Texture Cache压力间接加剧 Overdraw。优化方法在Font的 Inspector 中将Character Spacing和Line Spacing设为 0Font Size设为合适的值避免 runtime 缩放最重要的是使用Dynamic Font时务必勾选Use Dynamic Font并为Font Texture设置一个合理的Max Size如 1024×1024。对于静态文本推荐使用TextMeshPro它支持Sprite Asset可以将常用字预烘焙成 Sprite彻底绕过字体纹理采样。滑动条Slider的“三合一”重构。unity做一个滑动条的标准做法是Slider组件自带Background、Fill Area、Handle Slide Area、Handle四个子物体。其中Background和Fill Area通常是两个ImageHandle是一个Image。这至少是 3 层 Overdraw。优化方案将Background和Fill Area合并为一个ImageType设为FilledFill Method设为HorizontalFill Origin设为Left。然后将Handle的RectTransform的Anchor设为Middle CenterPivot也设为0.5, 0.5并通过脚本动态修改Image.fillAmount来控制填充进度。这样整个 Slider 只需要 2 个 Draw Call一个Filled Image一个Handle Image。实测下来一个页面 10 个 Slider优化后 GPU Time 降低了 18%。3.3 Mask 与遮罩的替代方案用“裁剪”代替“覆盖”Mask是 UI 开发者最爱用也是 Overdraw 最大的帮凶之一。我们必须找到更轻量的替代方案。RectMask2D免费的午餐。RectMask2D是Mask的轻量级替代品。它不创建 Render Texture而是利用 GPU 的 Scissor Test裁剪测试来实现遮罩效果开销几乎为零。它的限制是只能做矩形裁剪。对于大多数 UI如头像框、列表项、卡片这完全够用。使用方法将RectMask2D组件添加到父容器如Panel上然后确保子物体的Raycast Target为false如果不需要点击并且Image的Material使用UI/Default即可。它比Mask快 3~5 倍。Shader Level 的裁剪终极方案。对于需要圆角、椭圆、甚至自定义形状裁剪的场景RectMask2D就无能为力了。这时就需要自定义 Shader。一个经典的方案是UI/Unlit/Clipped它在顶点着色器中计算 UV 坐标在像素着色器中根据 UV 与裁剪区域的几何关系如distance(uv, center) radius来discard像素。这意味着被裁剪掉的像素根本不会进入像素着色器的计算流程实现了真正的“零 Overdraw”。我写过一个通用的RoundedRectClippingShader只需要传入Center、Radius、Softness三个参数就能完美实现圆角裁剪且性能与RectMask2D相当。“扩大点击范围”的优雅解法。回到那个经典问题“unity如何扩大按钮的点击范围”。标准答案是加一个透明Image但这会制造 Overdraw。更好的方案是直接修改Button组件的Navigation→Select On Up/Down/Left/Right或者更推荐使用EventTrigger组件监听PointerEnter和PointerExit事件然后在脚本中手动扩展RectTransform的sizeDelta并在PointerExit时恢复。这样点击范围是逻辑上的不产生任何额外的渲染。4. 实操过程与核心环节实现从 Profiler 到真机热感4.1 一套完整的“Overdraw 诊断-优化-验证”工作流这不是一次性的任务而是一个需要融入日常开发的习惯。我给自己定了一套 SOP标准操作流程每次提交 UI 相关代码前必走一遍。Step 1建立 Baseline基线在目标设备如 iPhone 13、小米 12上进入待优化的 UI 页面。打开 Unity ProfilerWindow → Analysis → Profiler确保Deep Profile关闭它会严重干扰 GPU 数据CPU Usage和GPU Usage都勾选。点击Record静置 10 秒让 UI 完全稳定。停止录制截图保存当前的GPU Time例如12.4ms、Draw Calls例如65、Tris例如1200。Step 2定位 Hot Spot热点在 Profiler 的GPU时间线下找到一个明显的峰值如 15ms。右键该峰值 →View Frame Debugging。在 Frame Debugger 窗口中点击左上角的Render Mode→Overdraw。此时整个画面会变成红绿相间的热力图。用鼠标悬停在最红的区域Frame Debugger 的右侧Hierarchy面板会高亮显示对应的 GameObject。记下它的名字如SettingsPanel/Background。Step 3针对性优化选中该 GameObject在 Inspector 中检查它是否在一个独立的、不必要的Canvas下→ 移动到主 Canvas。它的ImageType是Sliced吗→ 改为Simple。它的Color.a是 1 吗→ 如果是将Material改为UI/Unlit/Transparent并设为Opaque。它下面有Mask吗→ 替换为RectMask2D。修改后保存场景重新 Build Run 到设备。Step 4量化验证重复 Step 1 的录制过程。对比新旧数据GPU Time是否下降Draw Calls是否减少Saved by batching是否显著增加终极验证用手摸。在真机上连续操作该页面 2 分钟感受设备背部温度。一个成功的优化应该能让“暖手宝”变成“常温”。4.2 一个真实项目的优化记录从“发烫警告”到“冷酷到底”我们曾接手一个教育类 App用户反馈“课程列表页一打开就发烫”。Profiling 数据如下指标优化前优化后变化GPU Time (ms)24.88.2↓ 67%Draw Calls13741↓ 70%Tris21002100—Saved by batching-1289↑ 101优化过程分解Canvas 大扫除页面有 7 个独立的Canvas分别用于 Header、TabBar、SearchBar、FilterPanel、CourseList、LoadingIndicator、ErrorPanel。全部合并到一个MainCanvas下通过SetActive控制。Draw Calls直接从 137 降到 92。Image 大清洗CourseList中的每个CourseItem都有一个BackgroundSliced、一个CoverImageSimple、一个TitleText、一个ProgressSlider。我们将Background的Type改为Simple并为其分配UI/Unlit/TransparentOpaque材质。CoverImage保持Simple但Color.a从 0.95 改为 1.0并同样使用 Opaque 材质。ProgressSlider按前述“三合一”方案重构。此步将Draw Calls从 92 降到 58。Mask 大替换FilterPanel使用了Mask组件来实现圆角弹窗效果。我们将其替换为RectMask2D并用RoundedRectClippingShader 为FilterPanel的Image添加圆角。此步将Draw Calls从 58 降到 41。最后的“神来之笔”LoadingIndicator是一个ImageType为FilledFill Method为Radial 360用于旋转加载动画。它本身就是一个高 Overdraw 元素因为Filled模式在旋转时会产生大量顶点。我们将其替换为一个SpriteRenderer使用一张预渲染好的 32 帧旋转序列图.png序列并用Animator控制播放。这不仅消除了Filled的开销还让Draw Calls稳定在 41不再波动。实操心得优化不是“一步到位”而是“步步为营”。每次只改一个点然后立刻验证。我见过太多人一次性改了十几个地方结果发现GPU Time不降反升最后排查了两天才发现是某个Canvas的Sorting Order写错了导致合批完全失效。小步快跑稳扎稳打才是王道。4.3 工具链与自动化让优化成为肌肉记忆手动 Profiling 效率太低。我们构建了一个轻量级的自动化检查工具。Editor ScriptOverdraw Checker。这是一个在 Unity Editor 中运行的脚本它会扫描当前 Scene 中所有Canvas并报告Canvas的数量。每个Canvas下Image组件的数量及其Type分布Sliced/Tiled的占比。所有Image中Color.a 1的比例。所有Mask组件的数量。所有Text组件中Font的Max Size是否超过 2048。 运行一次5 秒内就能得到一份“Overdraw 健康报告”明确告诉你哪里最该优先优化。Build PostprocessorRelease Check。在BuildPipeline.BuildPlayer的回调中加入一个检查。如果PlayerSettings中的Graphics API是OpenGLES3或Vulkan移动端则强制检查Canvas的Render Mode是否为Screen Space - Overlay。如果不是则中断 Build 并抛出错误“Canvas Render Mode must be Overlay for mobile!”。这杜绝了因疏忽导致的低级错误。真机监控Logcat Hook。在 Android 平台我们编写了一个简单的 Java 插件它会 hookLogcat持续监听Adreno-GPU或Mali-GPU的日志。当 GPU 温度超过 65°C或GPU Frequency长期维持在最高频它会自动在 Unity 的Debug.Log中打印一条警告“[GPU WARNING] High temperature detected! Please check Overdraw.”。这让我们能在 QA 测试阶段就捕捉到潜在的发烫风险。5. 常见问题与排查技巧实录那些让你抓耳挠腮的“幽灵 Overdraw”5.1 “明明没动 UIGPU Time 却飙升”幕后黑手是Canvas.ForceUpdate这是最让人迷惑的问题。你什么都没改只是点了下按钮或者滑动了一下列表GPU Time就从 5ms 突然跳到 20ms。罪魁祸首往往是Canvas.ForceUpdate()。Canvas.ForceUpdate()是一个“暴力刷新”函数。它会强制 Canvas 重新计算所有 UI 元素的布局Layout、重建网格Rebuild、并触发一次完整的渲染。在Scroll View的OnValueChanged回调、Dropdown的OnValueChanged回调中如果你写了canvas.ForceUpdate()那就等于告诉 GPU“来把这一页 UI从头到尾再刷一遍漆”。排查技巧在 Profiler 的CPU Usage面板中展开Scripts查找Canvas.ForceUpdate的调用栈。检查所有Scroll View、Dropdown、Toggle Group的事件监听器确认没有手动调用ForceUpdate。替代方案对于Scroll View使用content.anchoredPosition来平滑滚动对于Dropdown使用dropdown.value来设置选项它们都是增量更新不会触发全量刷新。5.2 “UI 卡顿但 Profiler 显示 GPU Time 很低”你可能掉进了UI.Rebuild的陷阱有时候UI 卡顿但 Profiler 的GPU Time却很平静CPU Usage却很高。点开CPU Usage你会发现UI.Rebuild占据了大量时间。这说明问题不在 GPU 的“刷漆”而在 CPU 的“画图纸”。UI.Rebuild是 Canvas 在每一帧开始前为所有 UI 元素计算最终位置、大小、颜色、Mesh 的过程。如果一个Canvas下有上百个Text组件每个Text都在 runtime 动态修改text属性那么每一帧CPU 都要为这上百个Text重新生成字体图集、计算 Layout、重建 Mesh。这会严重拖慢主线程导致帧率下降。解决方案批量更新将需要动态更新的Text组件统一收集到一个ListText中然后在LateUpdate中统一更新而不是在Update中逐个更新。缓存与复用对于状态文本如“金币1234”不要每次都text.text 金币 gold;而是预先创建好格式字符串string.Format(金币{0}, gold)并缓存gold的旧值只在值改变时才更新。TextMeshPro的Auto Size是毒药TextMeshPro的Auto Size功能非常方便但它会在每一帧都重新测量文本宽度以确定最佳字号。在列表中这会导致灾难性的性能损失。务必关闭Auto Size为TextMeshPro设置一个固定的FontSize。5.3 “Unity world ui 无遮挡”World Space Canvas 的 Overdraw 陷阱World SpaceCanvas 是为了在 3D 场景中放置 UI如 NPC 头顶的血条、物品名称但它带来的 Overdraw 是指数级的。问题根源World SpaceCanvas 的渲染要参与完整的 3D 渲染管线。它需要进行 MVP 矩阵变换、深度测试、光照计算即使你没开 Light。一个World SpaceCanvas其开销远大于 10 个OverlayCanvas。优化方案非必要不用 World Space。能用Screen Space - OverlayCamera.WorldToScreenPoint计算位置的就绝不用World Space。如果必须用务必精简。World SpaceCanvas 下只放真正需要跟随 3D 物体的 UI如血条其他所有静态 UI如 HUD、菜单都放在OverlayCanvas 下。使用Culling Mask。为World SpaceCanvas 的Camera设置一个独立的Culling Mask只让它渲染必要的 Layer如UI_3D避免它去渲染整个 3D 场景。5.4 “Comfy UI 无法支持 GPU 加速”外部工具的警示灯虽然comfy ui、comfyui是 Python 生态的工具与 Unity 无关但它的名字频繁出现在热搜词中恰恰反映了行业的一个普遍焦虑GPU 资源是稀缺的任何一点浪费都会影响最终体验。当你在 Unity 里为一个滑动条、一个按钮、一个背景图不经意间制造了 5x Overdraw你就是在用宝贵的 GPU 算力去干一件本可以用 1x 完成的事。这和comfyui无法开启 GPU 加速导致推理慢如蜗牛是同一个道理资源错配。常见问题速查表现象最可能原因快速验证方法解决方案GPU Time 高Draw Calls 高Canvas 过多、Image Type 为 Sliced/Tiled、存在 MaskFrame Debugger → Overdraw 模式看红色区域合并 Canvas、改 Image Type、换 RectMask2DGPU Time 高Draw Calls 低使用了UI/Default材质的不透明 UI、存在大量半透明 UIProfiler → Rendering →Saved by batching为负数为不透明 UI 创建 Opaque 材质UI 卡顿CPU Usage 高UI.Rebuild 高Text 组件过多、Auto Size 开启、频繁修改 text 属性CPU Usage → Scripts → UI.Rebuild批量更新、关闭 Auto Size、缓存字符串真机发烫Profiler 数据正常Canvas.ForceUpdate()被滥用、World SpaceCanvas 渲染开销大CPU Usage → 查找 ForceUpdate 调用栈删除 ForceUpdate、用 Overlay WorldToScreenPoint 替代 World Space滑动列表卡顿Scroll View的Content下有大量未优化的 UI、Mask组件Frame Debugger → Overdraw看列表项是否一片红用ObjectPool复用列表项、用 RectMask2D 替代 Mask我在实际使用中发现90% 的 UI 发烫问题都能通过“合并 Canvas”、“改 Image Type”、“换 Opaque 材质”这三个动作解决。剩下的 10%往往是ForceUpdate或World SpaceCanvas 这种“高危操作”惹的祸。优化不是玄学它是一门手艺而手艺的核心就是知道在哪个环节用哪一把“刀”去切掉哪一块“多余的漆”。