news 2026/9/4 19:10:27

Unity游戏发热元凶:从功耗原理到性能优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity游戏发热元凶:从功耗原理到性能优化实践

作为常年泡在 Unity 性能优化一线的开发者,我几乎每周都能在测试群里看到类似的话:“帧率看着挺稳,怎么玩 20 分钟手机就烫得能煎鸡蛋了?” 或者更经典的:“帧率 60,温度 60,这算不算某种意义上的平衡?” 说实话,“玩游戏发热”这个问题,绝大多数情况下不是手机散热不行,而是我们做出来的包在持续压榨硬件。这一篇作为系列第 1 篇,我想先把最核心的“为什么烫”讲透,从硬件功耗和 Unity 渲染、逻辑脚本之间的关系说起,把这个病根拆明白了,后面的系列文章才会落地。

这个内容适合谁看?两类人。一类是自己做独立游戏、在真机上测出过热但完全不知道从哪里下手排查的开发者;另一类是团队里负责性能优化,但每次开会只能给出“降低画质”这种建议的所谓优化负责人。我会尽量用大白话,把 CPU、GPU、电压、频率这些听起来很高大上的词,跟你在 Profiler 里看到的一条条尖峰对应上。

1. 为什么烫:从功耗物理课上说起

1.1 发热的本质是“耗电做的功没变成画面”

先问一个问题:手机为什么会烫?初中学过能量守恒,电流通过芯片内部的晶体管,一部分能量变成计算输出,另一部分就变成热能散掉了。芯片的功耗近似公式是 P = C × V² × f,C 是电容负载,V 是核心电压,f 是运行频率。这里最要命的是 V²,电压稍微一拉,功耗直接翻倍往上冲。手机端 SoC 的调频策略一般是在温控阈值之内尽量跑高频,一旦温度到了 45℃ 左右,开始降频。降频带来的直接后果就是你的 60 帧变成了 50 帧、40 帧,然后画面开始卡,玩家手指就开始骂娘。

换句话说,游戏发烫的本质就是:你的代码和渲染指令让硬件不得不在高电压、高频率的状态下持续工作。如果你能想办法“偷个懒”——在肉眼不容易察觉的地方削减计算量——温度自然就下来了。我们做优化的终极思路不是“让手机多干活还得不烫”,而是“让手机少干点没意义的活,把功率花在刀刃上”。

1.2 为什么帧率稳并不代表功耗低

这里有个天坑,我见过大量开发者犯迷糊。用 Unity 默认设置打个包,什么都不动,场景里就摆了一个 Cube,帧率能跑 120,你以为挺优秀?实际上 Unity 的空场景在跑Camera.Render的时候,每一帧依旧会做Skybox渲染、光照计算、阴影判定、OnRenderImage之类的处理。哪怕场景再空,这些固定开销也一分不少在消耗功率。

我做过一次实测比较粗线条的统计:同样一个模型展示场景,不做任何优化时,CPU 主线程开销大概是 8ms/帧,渲染线程 6ms/帧,GPU 12ms/帧;当我们把垂直同步改成半速(30 FPS),把不需要的组件禁用,把后处理特效全部干掉后,CPU 能降到 3ms/帧,GPU 降到 5ms/帧。这里有意思的是:改动之前和改动之后的画面“感觉”没有差多少,但功耗差了将近 40%。所以“帧率稳”只能说明你没有严重的卡顿,完全不能代表你在省电,也不能代表手机不热。以后你再看到谁说“我帧率很稳所以性能没问题”,你心里就该知道他是外行。

2. Unity 里发热的几大污染源

2.1 GPU 侧:FillRate、分辨率与 Shader 复杂度是头号杀手

GPU 负责把三角形变成像素。手机上屏幕就那么大,但如果你一个物体被画了很多次,或者每一次像素填充所需的运算非常重,GPU 功耗就会直线飙升。Unity 里面最常见的问题有三个:

第一是Overdraw(过度绘制)。同一个像素被反复填色,每一次填充都在消耗 GPU 的 ALU 吞吐。粒子系统尤其严重,一个半透明粒子叠十个,同一个像素可能被片元着色器执行了十几次。查看方法也简单,在 Game 视图的右上角把Overdraw模式打开,如果整个屏幕是红的发紫的,那你的粒子层级肯定出了问题。

第二是Render Scale和屏幕分辨率后处理。有时候你不小心把Dynamic Resolution的 scale 设得过高,或者用了一个全屏模糊、全屏辉光效果,这相当于让 GPU 每帧把整块屏幕的像素都做一遍重型“数学按摩”。后处理这类操作对芯片的功耗冲击,比你想象中严重得多。以移动端为例,默认的 Vignette 一开,可能直接多出 4~6ms 的 GPU 耗时,发热体的主体之一就是这些全屏特效。

第三是 Shader 里写了太多分支、用了太多纹理采样。移动端 GPU 的架构和 PC 不太一样,PC 上随便写写问题不大,移动端一个带 6 张纹理采样的片元着色器,在低端机上直接能把 GPU 单元干满。这里我强烈建议,所有自定义 Shader 都要用Shader Inspector里的Compile and show code看一眼生成后的 GLSL,如果发现一堆循环嵌套还有分支,趁早重写。

2.2 CPU 侧:脚本和物理是消耗 CPU 电量的大户

CPU 发烫通常是被四类事情折磨疯了:垃圾回收(GC)、Mono 调用开销、物理引擎的碰撞检测、动画系统骨骼计算。

GC 是大家最熟悉的陌生人。你每帧new一个 List、每秒钟拼一个字符串、频繁触发GameObject.Find,这些都会在托管堆上留下垃圾。Unity 的 Mono/IL2CPP 在堆内存达到一定阈值后就会触发 GC,而 GC 一旦触发,主线程直接卡一下。卡一下看起来只是帧率波动,但它对 CPU 功耗的影响是这样的:GC 需要遍历整个托管堆,把所有对象引用关系重新整理一遍,这期间 CPU 大部分处理单元会处于高负载状态,频率可能会临时拔高,功耗瞬间上去。发热就是这么一趟一趟积累的。

物理引擎是另一个容易被忽视的点。默认 3D 物理是 PhysX,2D 是 Box2D。如果你场景里有几百个带ColliderRigidbody的物体彼此靠近,碰撞检测的复杂度是 O(n²) 级别的(当然有 broadphase 优化,但空间哈希也不可能完全救回来)。每一次物理迭代都在消耗 CPU 的核心运算单元。很多开发者觉得物理是“引擎自动算的”,不需要花心思,但忽略了大规模物理体本身就是一场耗电马拉松。

动画系统同样不省心。AnimatorStateMachine计算、OnAnimatorMove事件、IK Pass,这些都是每帧执行的。如果你把几百个角色的动画全部交给 GPU Skin 去算,除非用了GPU Instancing+ 骨骼纹理方案,否则 CPU 的负担会非常夸张。我们团队之前优化过一个密集人群场景,把动画采样从每帧一次改成了每 0.1 秒一次,CPU 耗时直接砍半,功耗温度双双降下来。

2.3 内存与显存带宽的隐藏消耗

还有一个非常隐蔽的发热元凶,就是带宽。手机芯片上除了 CPU 和 GPU 之外,还有一块 DRAM 控制器。当你频繁读取大纹理、反复在内存和显存之间搬数据,整个系统的功耗也会大幅上涨。这就像你把书从书柜搬到书桌、再从书桌搬回书柜,书本身没变,但来回搬书这件事本身累死活人。

在 Unity 里面,AssetBundle加载和卸载、纹理格式选错(比如用 RGBA32 高精度但我们根本不需要 Alpha)、Mipmap没开导致远处物体纹理采样消耗激增,这些全都会体现在带宽消耗上。移动端的显存带宽非常有限,GPU 和 CPU 抢带宽的结果就是大家都在等数据,功耗自然下不来。

3. 真机实测:我拿三个典型场景做了下 Thermal 对比

3.1 测试设备与环境说明

为了把这个“烫”字量化,我简单跑了一组对比测试。设备是一台骁龙 8+ 的手机,室温 25℃左右,屏幕亮度固定 50%,用同一部手机跑同一个包。测试场景三个:A 场景是典型的小白默认场景(空场景+天空盒+Bloom);B 场景是正常开发场景(有一些动态物体碰撞,粒子特效若干,UI 用了 TextMeshPro);C 场景是做过一轮优化后的版本(限制帧率、Overdraw 削减、无后处理、物理替代为触发器+射线检测)。

我主要用PerfDog记录功耗、帧率和温度,每场跑 10 分钟,取仿真平均值。测试结果见下表:

场景平均帧率平均功耗(W)10 分钟温度(℃)表现评价
A(默认空场景+后处理)594.842.3功耗偏高,纯浪费
B(正常开发场景)446.546.8能玩但明显发热
C(一轮基础优化后)563.237.5温度大幅下降,流畅度不错

这个结果有点反直觉的地方在于:A 场景画面极其简单,功耗却有 4.8W,跟 C 场景比功耗高了一半。原因就是 A 场景每帧都在做全屏 Bloom,GPU 的大量执行单元被这种“重量级”后处理拖住了,哪怕场景空,后处理的计算量却是实实在在的。所以如果你只是把后处理关掉、把帧率限制在 60,你的基础功耗就已经能降下来一大截了。

3.2 帧率限制是投入产出比最高的“降温按钮”

有人觉得限制帧率等于画质缩水,其实在手机上绝大多数游戏跑 60 帧所产生的功耗,换来的视觉提升很有限。我做了帧率对照实验:同一个带后处理场景,分别在 60 FPS、45 FPS、30 FPS 下跑,功耗数据分别是 5.6W、4.3W、2.9W。温度差值就更夸张了,60 帧 10 分钟后 46℃,30 帧只有 36℃。

原因很好理解:运行帧率高,意味着 CPU 和 GPU 每秒钟需要“运算并提交画面”的次数更多。每一帧即便没有玩家输入,所有更新逻辑、物理、动画、渲染管线都依然要走一遍。多数场景里,把帧率锁到 45 或 30 对肉眼的流畅度影响没有想象中那么大,但能耗和发热却能立竿见影地降下来。当然,如果是射击类电竞游戏,这个结论需要打折扣,需要综合权衡。

在实际落地的时候,我通常用Application.targetFrameRate = 45;或者 30,加一个设置界面让玩家自己选画质档位。手机上玩家能碰到“烫手”的机会远比“不够流畅”的机会少。

3.3 屏幕分辨率与 Render Scale 的降温实测配套

除了帧率,还有一个会影响发热的因素,就是Render Scale或官方建议的Dynamic Resolution。我同一场景下分别跑了 1.0 和 0.8 的 Render Scale,功耗从 5.4W 降到 4.0W,下降了四分之一还多。

这个逻辑也很直白:Render Scale 降到 0.8,意味着内部渲染分辨率约为原来的 64%,GPU 的片元着色器计算量直接打了六折多。屏幕实际输出时,通过双线性拉升到目标分辨率,画质损失远不如你想的那么严重,尤其在移动端那种小屏幕上,观感差距竟然比数据看起来温和得多。

所以我的结论是:以后谁跟你提“游戏越玩越烫”,第一个反问就应该是——锁帧了吗?Render Scale 调到多少?后处理开着几个?这三个问题比什么都管用。

4. 定位发热元凶的实操方法论

4.1 用 Profiler 找到 CPU 和 GPU 的尖峰

很多人在发热问题上卡住,是因为“热”是一个整体现象,没法告诉你哪一行代码不行。所以第一步永远是查工具:Window > Analysis > Profiler。这里我特别强调一个习惯,真机联调时,把Development BuildAutoconnect Profiler都打开,同时勾上Deep Profile(一个会显著拖慢帧率但是能定位到具体函数开销的选项)。在 Profiler 里,有一条至关重要的分隔线——16.67ms(对应 60 FPS),如果你的 CPU 主线程或者 GPU 时间频繁超过这条线,掉帧发热迟早发生。

但你首先要用到的其实是CPU Usage模块里的Player Loop,展开后能看到ScriptRunBehaviourUpdatePhysics.FixedUpdateAnimation.UpdateUI.Update等等。哪一栏树的叶子最长,你的瓶颈就在哪。我在排查发热问题时,绝大多数情况都会在ScriptRunBehaviourUpdate里找到若干个隐藏的Update死循环。

4.2 Frame Debugger:看 GPU 到底画了什么

CPU 查完,GPU 侧的事要用Window > Analysis > Frame Debugger。它能一帧一帧地回放渲染过程,你可以直观看到每一个Draw Call,每一个RenderPass。我排查过的一个典型项目,用一个角色模型,结果因为材质球数量太多,被拆成了 30 多个 Draw Call,而且每个材质球都有独立的MainTexNormalMap采样,GPU 会疯狂地在不同的纹理之间切来切去。Frame Debugger 里能看到明显的连续SetTexture调用消耗,这在移动端是发热大户。解决办法很简单:把材质球合并,纹理依赖的贴图做进一张图集,或者直接用GPU Instancing处理相同物体的绘制。

还有一个很好用的技巧,Frame Debugger 的右侧会显示每个 Draw Call 用到的 Shader 名称。如果你看到同一个模型的 Shader 是Standard (Specular setup)而不是Standard (Roughness setup),在移动端这就有可能是几毫秒的差异。移动端尽量用为移动 GPU 定制的 Shader,比如Universal Render Pipeline/Lit,或者自己手写 Mobile Shader。

4.3 温度与功耗的第三方工具

Profiler 不够直观的时候,我会用 Android 平台上的PerfDog或者Snapdragon Profiler。PerfDog 能同时采样 FPS、CPU 占用率、功耗、SoC 温度,把这些曲线叠加之后,你能很清晰地看到游戏的操作高峰和功耗峰值是否同步。我曾经排查过一个案例:打开某个商店界面时,温度曲线瞬间陡增,点回去看了代码,发现只是打开了 UI 界面,但界面背景迭代加载了一个 4K 大图,同时该界面里有个动画在每帧新建 UI 顶点数据,直接导致 GPU 和 CPU 同时爆负载。这种问题如果只看帧率曲线你根本看不出名堂,只有把温度和功耗变量纳入记录表里,才能锁定元凶。

真机调温度优化,有三件套我用下来最顺手:

工具用途说明
Target FPS / 垂直同步设置限制输出帧率Application.targetFrameRate 最直接
UPR / PerfDog采集功耗与温度需真机,用于前后对比
Unity Profiler定位 CPU 函数级开销Deep Profile 模式更精细
Frame Debugger分析渲染提交明细配合 RenderDoc 可深入分析

4.4 一个真实案例的排查复盘

这里分享一个我印象很深的项目。那是一款卡牌+轻度 3D 展示的手游,玩家反馈“只要进入抽卡动画就发烫严重”。一开始我们以为是特效场景的原因,因为抽卡动画做了非常复杂的粒子汇聚效果和全屏光效。

我们先用 Profile 确认了 CPU 耗时正常,但 GPU 时间报表里ForwardOpaqueParticleSystem占了 80% 以上。再用 PerfDog 看功耗曲线,进入抽卡动画那一刹那,功耗直接冲上 7W。然后我们在 Frame Debugger 一帧一帧翻,发现粒子数量远超预期。原来我们给每个粒子都加了Trail Renderer,十几个粒子发射器再叠加出来就是几百个拖尾。拖尾的本质是不断生成新的 Mesh 几何体,填充率和顶点数一下子全爆了。

解决方案是重构了粒子方案:把粒子发射数量从 500 降到 80,关闭了大多数粒子的拖尾,换成一个预烘焙的序列帧动画模拟;全屏辉光后处理直接去掉,改用一个比较廉价的 Bloom 变体。抽卡动画重构完,同场景功耗从 7W 降到 4.3W,温度从 48℃ 降到 39℃。玩家的体感直接翻转,从“烫手”变成了正常的温热。

这类问题我总结成一条经验:抽卡、大招动画这类“华丽镜头”,是发热的重灾区,因为它们通常同时叠加了大量粒子、后处理、动态动画,靠后期瞎调参数是没有用的,必须正面拆解粒子数量和后处理等级。

5. 常见发热问题与排查技巧实录

5.1 明明没做复杂内容,为什么手机上还是热?

这是新手最多的情况:场景里只有一些放好的模型和 UI,什么粒子、后处理都没加,理论上不应该热。但我之前提过,空场景 + 默认设置本身就不省电。这里有几个基础检查点:

第一,看看有没有开垂直同步。如果垂直同步关闭,游戏帧率“无上限”,在高刷屏手机上能跑到 120 甚至 144 FPS。你以为空场景是在“独善其身”,实际上手机 CPU/GPU 正以每 6.9ms 一帧的速度疯狂输出,耗电速度可想而知。在开发初始阶段,游戏里就应该预留一个DebugSettings界面,把目标帧率固定下来。

第二,检查QualitySettings里的Shadow QualityPixel Light CountTexture Quality。默认的 High 档位在移动端非常激进,尤其是Shadow Distance,如果设成了 150 米,那意味着很远的物体也要渲阴影。阴影的大面积计算在移动端特别烧 GPU。适合移动端的通常是:Shadow Distance 40~60Pixel Light Count 1(只保留最重要主光)、纹理用Half Res就足够。

第三,TextMeshPro 或者 UGUI 动态字体。如果你频繁更新 UI 文本且使用了复杂的富文本,UGUI 会重新构建整个Canvas的网格,这个过程会消耗 CPU。解决方案是按需更新,不要每帧都赋值text.text

5.2 Unity 对内存管理的潜在坑

内存导致的发热比较隐蔽,但值得单列一节。最常见的问题是AssetBundle加载后不释放,或者Addressables资源LoadAssetAsync了但没调用Release。逻辑上只是内存占用变大,感觉和发热没关系?其实当内存接近极限时,Unity 会频繁触发 GC,每次 GC 都是处理器的“大扫除”,功耗自然上去。更严重的是手机系统会定期把后台内存换到 zRAM 压缩交换区,这个压缩解压操作在 CPU 上非常耗电。把Profiler Memory面板打开,看看Total Allocated Memory是不是在缓慢爬坡,如果爬坡,恭喜你,你找到了一个能让你手机热一整天的隐性罪犯。

5.3 真机与编辑器表现差异过大

有一种非常坑的情况:在电脑上 Profiler 一切正常,CPU 占用 8ms 以内,GPU 也没有异常,但一到真机上就热。这种情况的排查重心往往要转向 Shader 和纹理压缩。PC 上 Shader 是 DX11/DX12,移动端是 OpenGL ES/Vulkan,两者执行模型差异巨大。一些在 PC 上几乎不费力的半透明混合、全屏法线计算,移动端可能会变成沉重负担。纹理格式上,PC 的 BC7、BC5 在移动端往往不支持,会自动转成 RGBA32,内存翻好几倍,带宽压力增大,高温随之而来。针对移动端的正确做法是使用 ASTC(Android)和 ASTC/PVRTC(iOS),并且开启 Mipmap。

5.4 排查时的风云问题:UI 重叠、Canvas 重建与布局漂移

UGUI 的 Canvas 是另一个容易“顺手坑你”的地方。一个常见问题是:一个界面里有几百个 UI 元素,而这些元素分别放在不同的 Canvas 下,每个 Canvas 都开启了自己的Screen Space - Overlay,于是每次 UI 更新都会触发多个 Canvas 的重建。更欠揍的是,某些 UI 组件在隐藏时会频繁SetActive(true/false),这会强制 Canvas 重建,极端情况下帧率直接从 60 掉到 35。

排查方式也很直接:Profiler 的UI Module里看Canvas.SendWillRenderCanvases,如果占比较高,说明 Canvas 更新很频繁。优化方向是把静态 UI 做成独立的Canvas,设置UI BatchCanvasadditionalShaderChannels最小化,尽量避免动态变化影响整块 Canvas。

5.5 关于 IL2CPP 与代码裁剪的小提示

如果你用的Mono脚本后端打包,在某些型号上会有额外的即时编译开销,会加重发热。默认改成IL2CPP后运行效率会更好,代价是包体更大且首次启动耗时增加。部分公司为了省事选择一直用 Mono,结果在低端机上跑起来,每次执行复杂 C# 逻辑时 CPU 都会发烫。IL2CPP 会把 C# 转成 C++ 再编译成机器码,执行效率能提升不少,是真机优化里性价比非常高的一步。另外 IL2CPP 提供了代码裁剪,能去掉的冗余代码就去掉,包体瘦身后加载也更快,功耗会有间接收益。

5.6 常见问题的速查对照表

为了大家方便排查,我整理了一个发热问题速查表,基本覆盖了我这些年遇到的高频情况:

表现场景第一嫌疑第二嫌疑快速验证手段
游戏一开始就明显发热后处理/高帧率大纹理无压缩关后处理、锁帧率、查看内存占用
玩到后面才开始发热内存泄漏/资源不释放长时间运行的技能特效观察内存曲线、使用 PerfDog 记录温度趋势
UI 界面操作时发热Canvas 频繁重建动态字体UI Profiler 查看 Canvas 重建次数
3D 场景移动时发热阴影距离过大Shader 复杂度高降低阴影距离、替换 Mobile Shader
粒子/特效场景发热Overdraw 严重Trail Renderer 过多Overdraw 模式检查、减少粒子数

6. 写在系列前面的几条经验总结

这篇是系列的第 1 篇,做的是一件最基础但最重要的事——把发热的“元凶画像”给大家提前画出来。如果你连发热到底来自 CPU 还是 GPU、来自帧率还是后处理都分不清,后面任何优化技巧都无处安放。

我个人的体会是:Unity 的发热优化,跟“游戏好不好玩”没有关系,它是一门纯粹依靠数据分析的工程题。每一次优化,本质上都是回答三个问题——这一帧有没有在做无用功?能不能偷个懒?偷完懒之后肉眼看得出来吗?

实际动手时,建议你从“锁帧 + 减少后处理 + 缩短阴影距离”这三板斧开始,这是投入产出比最高的起步动作。测完第一轮温度数据,再进 Profiler 去看大开销项,逐步缩小范围。整个过程其实像在排雷,一颗一颗扫过去,心态别急就好。

下一篇我打算针对移动端最普遍的“后处理发热”做一次拆解,把 Bloom、Depth of Field、SSAO 这些看似惊艳的功能,在手机功耗上是否值得开、怎么开更划算,用实测数据一条条说清楚。按照我的经验,光这一块,就能帮你把一个项目的发热问题减少三成以上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 19:10:24

基于深度学习的人脸表情识别系统:从模型训练到工程部署全流程实战

简介:本资源是一套面向本科毕业设计的完整人脸表情识别系统实现方案,适用于计算机、人工智能及相关专业学生开展深度学习实践与项目开发。系统基于Python构建,采用CNN等主流模型实现面部图像采集、预处理、特征提取与七类基础表情&#xff08…

作者头像 李华
网站建设 2026/9/4 19:10:21

构建可靠财务Agent:从最小任务集到人工审批的必要工程化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 19:07:26

还没用上Codex?从安装配置到权限安全的上手障碍全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 19:07:18

GPU利用率低?从驱动到代码,系统排查与优化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 19:05:43

AI动画创作:从游戏同人到个人叙事引擎的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 19:05:36

PWM频率与占空比实战指南:从LED调光到电机控制的精准配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华