RK3588硬编实战:MPP实现8路1080P视频编码优化
1. 从一次8路编码需求说起为什么单靠CPU扛不住去年下半年接手一个多路视频汇聚的项目需求很直接一块RK3588板子接8路1080P摄像头每路25帧实时编码成H.264/H.265再推流出去。第一反应是拿FFmpeg软编先跑通流程结果8路同时开起来CPU八个核全部飙到90%以上帧率掉到十几帧延迟肉眼可见地涨。这个结果其实不意外——1080P25fps的YUV420数据量是1920×1080×1.5×25≈77.8MB/s8路就是622MB/s的原始数据要过一遍编码器纯靠CPU做运动估计和变换量化算力根本不够分。RK3588这颗芯片的定位决定了它必须走硬编路线。它内置了独立的VPUVideo Processing Unit官方叫MPPMedia Process Platform是一套跨平台的媒体处理框架封装了底层硬件编解码器的调用。MPP对上提供统一的MPI接口对下管理RK3588的VEPU视频编码单元和VDPU视频解码单元。关键点在于编码这件事从CPU手里彻底剥离出去交给专用硬件流水线CPU只负责喂数据和收码流。但有硬编和能跑满8路之间隔着不少工程细节。我前后调了两周多中间踩了内存带宽、通道绑定、码率控制、帧缓冲复用好几个坑最后稳定在8路1080P25fps、H.265编码、总码率约16Mbps、CPU占用不到15%的状态。这篇就把整个优化路径拆开讲包括MPP的调用逻辑、参数怎么配、瓶颈怎么定位以及那些文档里不会写的经验。适合谁看正在RK3588上做多路视频编码的嵌入式工程师、做NVR/IPC/边缘盒子产品的开发者以及想搞清楚MPP到底怎么用的人。如果你只是跑个单路demo这篇的部分内容可能偏重但瓶颈分析那几节值得一看。2. MPP的编码流水线到底长什么样2.1 从应用层到硬件的四层结构很多人用MPP就是照着sample改改完能跑就不管了。但8路场景下不理解内部结构就没法定位瓶颈。MPP的编码路径大致分四层应用层你调用的mpp_create、mpp_init、mpp_encode_put_frame这些接口。MPP框架层管理上下文、通道、码率控制器做参数校验和任务分发。HAL层把MPP的抽象指令翻译成VEPU寄存器操作。硬件层VEPU实际执行运动估计、DCT变换、量化、熵编码。这里有个容易误解的点MPP不是一个编码器而是一组编码通道的管理器。RK3588的VEPU硬件上支持多个编码实例并行MPP通过MppCtx和MppApi把每个编码任务映射到一个硬件通道。8路编码本质上是8个MppCtx同时工作共享同一块VEPU硬件资源。2.2 编码通道的硬件约束RK3588的VEPU在硬件规格上有几个硬约束直接决定了8路能不能跑满约束项规格对8路的影响最大编码分辨率8192×8192单路不是瓶颈最大总像素率约8K30fps等效8路1080P25fps合计约4K50fps有余量同时编码通道数硬件支持多实例需确认固件版本输入格式NV12/YUV420SP为主摄像头输出需转换码率控制CBR/VBR/FIXQP多路建议CBR注意不同批次的RK3588和不同版本的MPP库通道数上限可能有差异。我手上这块板子跑8路没问题但建议你先用mpp_info查一下VPU的硬件能力别直接照搬。2.3 为什么输入格式必须是NV12摄像头出来的数据通常是YUYV或MJPEG而VEPU的编码输入要求NV12YUV420SP。这中间必须做一次格式转换。转换有两条路CPU转或者RGA转。CPU转的话8路622MB/s的数据要过一遍色彩空间转换又是几个核的算力没了。正确做法是用**RGARaster Graphic Acceleration**做硬件转换。RGA是RK3588的2D加速单元支持YUV格式转换和缩放走DMA通道几乎不占CPU。这里有个细节RGA和VEPU之间最好用DMA-BUF做零拷贝传递。如果中间过一遍内存拷贝622MB/s的带宽开销会直接吃掉性能。我在早期版本里就是忘了配DMA-BUF结果RGA转完写回内存MPP再从内存读白白多了一倍带宽占用8路跑起来内存带宽直接打满。3. 8路并行的资源账先算清楚再动手3.1 内存带宽的粗略估算动手之前先算账这是我做嵌入式优化养成的习惯。8路1080P25fps的编码数据流大致是摄像头采集写入622MB/sNV12RGA读取做格式转换622MB/sRGA写回622MB/sMPP读取编码输入622MB/sMPP写回参考帧和重建帧约622MB/s×2编码器内部有参考帧读写码流输出约16Mbps×816MB/s可忽略合计内存带宽需求约3.1GB/s。RK3588的LPDDR4X/LPDDR5理论带宽在10GB/s以上看起来够但实际可用带宽受总线仲裁和访问模式影响通常只能到理论值的50%-60%。也就是说实际可用约5-6GB/s3.1GB/s的需求占了六成左右留给CPU和其他外设的余量就不多了。这就是为什么零拷贝和DMA-BUF这么重要——每省一次拷贝就省622MB/s的带宽。3.2 编码器内部缓存的分配策略MPP在初始化时会为每个编码通道分配内部缓存包括参考帧、重建帧、码流buffer。8路同时开这些缓存加起来不小。默认配置下MPP会按最大分辨率预留如果你不手动限制内存占用会很夸张。我的做法是在mpp_init之前通过MppEncCfg设置MPP_ENC_CFG_PREPARE相关参数明确指定输入分辨率和帧缓冲数量。参考帧数量从默认的4降到2重建帧从默认的3降到2在画质损失可接受的前提下省下不少内存和带宽。// 设置编码器基础参数 MppEncCfg cfg; mpp_enc_cfg_init(cfg); mpp_enc_cfg_set_s32(cfg, prep:width, 1920); mpp_enc_cfg_set_s32(cfg, prep:height, 1080); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_YUV420SP); mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, rc:bps_target, 2000000); // 2Mbps每路 mpp_enc_cfg_set_s32(cfg, rc:fps_in_num, 25); mpp_enc_cfg_set_s32(cfg, rc:fps_out_num, 25); mpp_enc_cfg_set_s32(cfg, rc:gop, 50); // 2秒一个I帧 mpp_enc_cfg_set_s32(cfg, h264:profile, 100); // High Profile mpp_enc_cfg_set_s32(cfg, h264:level, 40); // Level 4.03.3 线程模型一通道一线程还是线程池MPP的编码调用是阻塞式的mpp_encode_put_frame之后要等mpp_encode_get_packet拿到码流。8路如果用一个线程轮询任何一路卡住都会拖累其他路。我的方案是每路一个独立线程线程内做取帧→送编码→收码流→推流的循环。但线程数不是越多越好。8个编码线程加上采集线程、推流线程总共十几个线程在跑调度开销不能忽视。实测下来把采集和编码放在同一个线程里采集完直接送编码比分开两个线程效率更高因为省了一次线程间同步和队列操作。void* encode_thread(void* arg) { EncChannel* ch (EncChannel*)arg; MppFrame frame NULL; MppPacket packet NULL; while (ch-running) { // 从采集buffer取一帧NV12数据 if (get_camera_frame(ch, frame) 0) continue; // 送编码 mpp_encode_put_frame(ch-ctx, frame); mpp_frame_deinit(frame); // 收码流 while (mpp_encode_get_packet(ch-ctx, packet) MPP_OK) { if (packet) { push_stream(ch, packet); mpp_packet_deinit(packet); } } } return NULL; }提示mpp_encode_get_packet可能一次返回多个packet比如SPS/PPS和I帧分开要用while循环收干净否则码流会积压。4. 参数调优码率、GOP和QP的取舍4.1 码率控制模式的选择MPP支持CBR、VBR、FIXQP、AVBR几种码率控制模式。8路场景下我强烈建议用CBR原因有两个一是总带宽可预测8路×2Mbps16Mbps网络和存储都好规划二是CBR下编码器的负载相对稳定不会因为某路画面突变导致瞬时算力飙升影响其他路。VBR虽然画质更优但码率波动大8路叠加起来峰值可能到30Mbps以上对推流和存储都不友好。FIXQP更简单但画质不可控暗场景噪点会被放大。CBR的关键参数是bps_target和bps_max。我一般把bps_max设成bps_target的1.2倍给编码器一点弹性空间避免码率被卡得太死导致画质崩。4.2 GOP长度与I帧策略GOPGroup of Pictures决定I帧的间隔。GOP越短随机访问越快但码率越高GOP越长压缩率越高但丢包恢复慢。8路场景下我用的GOP是5025fps下即2秒一个I帧。这个值的考量是推流如果走RTSP/RTP2秒的I帧间隔在丢包时恢复可接受同时I帧占比不高码率不会因为I帧频繁而膨胀。但有个坑8路的I帧如果同时到达会造成瞬时码率峰值。因为I帧的数据量通常是P帧的5-10倍8个I帧挤在一起瞬时码率可能冲到50Mbps以上。解决办法是错开各路的I帧位置比如第1路GOP50第2路GOP52第3路GOP54让I帧分散开。// 错开I帧每路GOP略有不同 int gop 50 (channel_id % 4) * 2; mpp_enc_cfg_set_s32(cfg, rc:gop, gop);4.3 QP范围与画质平衡CBR模式下编码器通过调整QP来维持目标码率。QP范围设得太宽画质波动大设得太窄码率控制不灵敏。我的经验值是初始QP设26最小QP设20最大QP设45。初始QP 261080P2Mbps下画质和码率的平衡点。最小QP 20防止简单画面下码率浪费。最大QP 45防止复杂画面下码率失控同时保证画质不至于糊成一片。如果某路画面特别复杂比如监控场景里全是树叶晃动可以把该路的bps_target单独调高到3Mbps其他路降到1.8Mbps总码率不变但画质分配更合理。5. 瓶颈定位从CPU占用到内存带宽的排查链路5.1 第一层确认编码是否真的走了硬件跑起来第一件事是确认编码走的是VEPU而不是软编。方法很简单看CPU占用。如果8路跑起来CPU占用超过50%大概率有部分在软编或者格式转换走了CPU。更准确的方法是查/sys/kernel/debug/mpp_service下的统计信息能看到每个通道的硬件使用情况。如果某个通道的hw_time为0说明它没走硬件。我遇到过一次MPP初始化时格式设成了MPP_FMT_YUV420P平面格式而VEPU只认MPP_FMT_YUV420SP半平面格式结果MPP内部做了软转换CPU占用直接翻倍。改成NV12后问题消失。5.2 第二层内存带宽是否打满CPU占用正常但帧率上不去大概率是内存带宽瓶颈。用devfreq或者/sys/class/devfreq看DDR的频率和负载如果长期在最高频且负载接近100%就是带宽不够。这时候的优化方向检查是否所有数据通路都用了DMA-BUF有没有多余的拷贝。降低参考帧数量减少编码器内部读写。如果摄像头支持直接输出NV12省掉RGA转换。5.3 第三层编码通道是否互相抢占8路共享VEPU如果某路的编码任务特别重比如高码率、高复杂度会挤占其他路的时间片。表现是某些路帧率正常某些路掉帧。排查方法是单独跑每一路记录单路编码耗时然后8路一起跑看耗时是否显著增加。如果增加超过30%说明通道间抢占严重。解决办法是限制单路的编码复杂度。MPP支持设置MPP_ENC_CFG_RC_MODE下的rc:quality参数把质量档位从best降到normal编码器会减少运动估计的搜索范围单路耗时下降8路并行更均衡。5.4 第四层采集和推流的干扰编码本身没问题但采集线程或推流线程拖后腿。采集如果走V4L2的read接口每次都要拷贝推流如果走TCP且网络拥塞会阻塞编码线程。采集建议用V4L2的mmap方式直接拿DMA buffer配合DMA-BUF传给RGA和MPP。推流建议用独立线程环形缓冲编码线程只管往缓冲里写推流线程慢慢发不阻塞编码。6. 实测数据与稳定性验证6.1 8路满跑的实测结果最终配置RK3588LPDDR4X 8GB8路1080P25fpsH.265编码CBR 2Mbps/路GOP错开NV12输入RGADMA-BUF零拷贝。指标实测值总码率约16MbpsCPU占用12%-15%内存带宽占用约3.5GB/s单路编码延迟约40ms8路总延迟约60ms连续运行72小时无掉帧、无崩溃单路编码延迟40ms主要是编码器内部流水线深度决定的8路总延迟60ms是因为线程调度和缓冲引入的额外延迟。这个延迟对于监控和直播场景完全够用。6.2 长时间运行的稳定性问题跑通不等于稳定。我遇到过两个长时间运行才暴露的问题第一个是内存泄漏。MPP的MppFrame和MppPacket如果忘记deinit每帧泄漏一点几小时后内存就满了。排查方法是用valgrind跑单路或者定期打印/proc/meminfo看内存增长趋势。解决就是在每个get之后确保有对应的deinit。第二个是码率漂移。CBR模式下如果画面长期静止编码器会不断降低QP码率低于目标值一旦画面突变码率会瞬间冲高。长时间运行后平均码率可能偏离目标10%以上。解决办法是定期比如每1000帧重置一次码率控制器的状态或者用AVBR模式让编码器自适应。6.3 温度与降频的影响RK3588满负荷跑编码芯片温度会上升。如果散热不好到85度以上会触发降频VEPU的频率下降编码帧率跟着掉。实测加一个普通散热片8路满跑温度稳定在65-70度不降频。如果做密闭盒子建议加风扇或者用金属外壳做散热。另外可以在软件上限制VEPU的最高频率避免温度冲高代价是编码耗时略增。7. 几个容易踩的坑和我的处理方式7.1 MPP版本与固件的匹配MPP库和内核里的VPU固件版本必须匹配。我遇到过MPP库是新的固件是旧的结果编码出来的码流花屏。排查了半天才发现是版本问题。建议从官方SDK里拿配套的MPP和固件别自己混搭。7.2 摄像头输出格式的坑不是所有摄像头都支持NV12输出。有些USB摄像头只出MJPEG或YUYV这时候RGA转换的压力会大很多。MJPEG还要先解码再转NV12等于多了一道解码。选摄像头时优先选支持NV12输出的MIPI摄像头能省不少事。7.3 推流协议的缓冲策略RTSP推流时如果客户端消费慢服务端的发送缓冲会积压最终阻塞编码线程。我的做法是给每路推流设一个固定大小的环形缓冲比如30帧满了就丢最旧的帧。监控场景丢几帧比整个编码卡住要好。7.4 多路音频的同步如果项目里还有音频8路音频和视频的同步是个麻烦事。我的做法是用统一的时间戳基准音视频各自打时间戳推流时用RTCP做同步。这块内容比较多这里不展开但提醒一句别等到视频调通了再加音频同步问题会推翻很多设计。8. 后续还能怎么压榨性能8路1080P跑通之后我试过继续往上压。几个方向降到H.264H.264的编码复杂度比H.265低同样硬件资源下能多跑1-2路但码率会高30%左右。动态分辨率非关键路降到720P关键路保持1080P总路数能到12路以上。ROI编码MPP支持感兴趣区域编码对画面中不重要的区域降低画质能省不少码率。智能帧率画面静止时降帧率有运动时升帧率平均算力需求下降。这些优化的前提是先把基础8路跑稳。基础不稳加再多技巧都是空中楼阁。最后分享一个调试习惯每次改参数只改一个改完跑30分钟看数据。我早期图快一次改好几个参数结果出问题不知道是哪个引起的反而浪费时间。嵌入式优化没有捷径就是算清楚账、一次动一个变量、用数据说话。

相关新闻

客服机器人RAG工程实践:从知识库搭建到检索生成,系统化消除大模型幻觉

客服机器人RAG工程实践:从知识库搭建到检索生成,系统化消除大模型幻觉

1. 为什么"检索增强"能管住大模型那张嘴先聊一个很多团队都撞过的墙:把通用大模型直接丢到客服场景里,前面三句话客客气气,第四句话开始一本正经地编政策。用户问"退货邮费谁出",模型说"根据《消费者权益…

2026/10/7 5:49:23 阅读更多 →
Cartographer 2D激光+IMU融合建图配置与物理对齐指南

Cartographer 2D激光+IMU融合建图配置与物理对齐指南

1. 这不是“调通就行”的配置,而是让Cartographer真正理解物理世界的起点Cartographer实战:2D激光雷达与IMU融合建图配置详解与避坑指南——这句话里,“实战”两个字是铁律,“融合”是核心,“避坑”是血泪经验。我带过…

2026/10/7 5:49:23 阅读更多 →
DeepSeek桌面版实战指南:告别WebUI,构建本地高效工作流

DeepSeek桌面版实战指南:告别WebUI,构建本地高效工作流

从 WebUI 切到 DeepSeek 桌面版已经两周多了,说实话,回不去了。以前总觉得“浏览器里开个页面就能聊”是最省事的方案,但用久了才发现,WebUI 那套东西更适合临时体验,真要天天高强度用,效率和手感都差着意思…

2026/10/7 5:49:23 阅读更多 →

最新新闻

FCS-MPC控制MMC整流器Simulink仿真复现:从参数设计到代价函数调参

FCS-MPC控制MMC整流器Simulink仿真复现:从参数设计到代价函数调参

拿到这篇IEEE的SCI二区论文时,我第一反应不是兴奋,而是头大。FCS-MPC控制MMC整流电路的Simulink仿真,这几个词单独拎出来都好懂,合在一起就成了硬骨头。论文里的关键参数表、代价函数权重、甚至主电路细节,基本都是“懂…

2026/10/7 6:18:39 阅读更多 →
AI-native重构制造业ERP/MES:从胶水集成到神经突触

AI-native重构制造业ERP/MES:从胶水集成到神经突触

1. 这不是“给ERP加个AI按钮”,而是把整套系统从地基里重铸一遍我去年在一家中型汽配厂做数字化顾问,亲眼看着他们花280万买了一套标榜“AI增强”的MES系统——上线半年后,车间主任指着大屏上跳动的“智能排产建议”跟我说:“这玩…

2026/10/7 6:18:39 阅读更多 →
Agent-Reach:轻量级AI Agent任务调度与协议桥接框架

Agent-Reach:轻量级AI Agent任务调度与协议桥接框架

1. 项目概述:Agent-Reach 是什么,它解决的不是“调用API”而是“可靠调度”Agent-Reach 不是一个简单的命令行工具别名,也不是某个大模型厂商推出的官方CLI客户端——它是一套面向生产级AI Agent工作流的轻量级任务调度与协议桥接框架。我第一…

2026/10/7 6:18:39 阅读更多 →
Higress云原生网关:基于Envoy+Wasm+Gateway API的生产实践

Higress云原生网关:基于Envoy+Wasm+Gateway API的生产实践

1. 什么是 Higress?它到底在解决什么问题?Higress 不是又一个“看起来很酷但用不起来”的网关玩具,而是阿里内部经过双十一大促真实流量淬炼、后来开源出来的一套生产级云原生网关系统。我第一次在客户现场看到它跑在 Kubernetes 集群里扛住每…

2026/10/7 6:18:39 阅读更多 →
ponytail:从tail -f到可编程实时日志过滤CLI的实操指南

ponytail:从tail -f到可编程实时日志过滤CLI的实操指南

第一次看到 “ponytail” 这个名字,我以为是哪个妹子扎马尾辫的摄影教程。直到有次排查线上接口超时,同事甩过来一条命令,一串带着颜色和过滤规则的日志唰唰往下滚,我才知道这是开发者圈子里那个让tail -f升级成“日志驾驶舱”的开…

2026/10/7 6:18:39 阅读更多 →
GEO如何改写招商加盟获客逻辑:在AI答案中抢占线索

GEO如何改写招商加盟获客逻辑:在AI答案中抢占线索

招商加盟这个行业,正在被GEO悄悄改写获客逻辑。过去我们给品牌做招商加盟推广,基本就是百度竞价拉满、SEO信息流铺满,再配合一批加盟网站做曝光;但这两年的变化非常明显,越来越多的意向加盟商,第一次了解一…

2026/10/7 6:17:38 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

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

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

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

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

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

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/6 6:26:51 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/6 8:21:32 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/6 4:21:51 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/6 1:18:13 阅读更多 →