news 2026/10/15 1:33:41

嵌入式GUI方案:FlyThings OS到底是库还是操作系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式GUI方案:FlyThings OS到底是库还是操作系统

凡是给带屏设备做过界面开发的人,应该都被同一个问题卡过:我现在要用的这套GUI方案,到底算一个库,还是一个操作系统?这个问题听起来有点钻牛角尖,但它一上来就把后续决策全牵扯进去——业务代码往哪放、任务怎么分、内存怎么管、出了问题先查驱动还是先查框架。FlyThings OS恰恰是这个问题最典型的样本,名字里堂而皇之挂着OS,实际开发起来却像在调一个类库。很多人第一次用它时都会犯嘀咕:我到底是在写应用,还是在操作一个系统?这篇文章就从三层结构把这事彻底拆开:先讲嵌入式GUI方案的形态分类,再拆它的RTOS内核层、GUI框架层和开发工具体系层,最后落回到选型和实战避坑。不管你最后选不选它,这套拆法都适用于市面上绝大多数带屏GUI方案。

1. 疑问从哪来:嵌入式GUI方案到底分几种形态

1.1 三类常见方案:自绘、GUI库、平台型

行业里做带屏界面,从来不是只有一种姿势。最原始的一类叫裸机自绘,MCU不带操作系统,也不引入任何GUI中间件,从画点、画线、画矩形开始写,字体要自己集成,控件要自己维护列表,点击命中要自己算坐标,整个界面代码和业务代码糊在一起。这种方案灵活性最高,哪一块逻辑都能改,但每一次需求变更都意味着从底层开始动刀。

第二类是纯GUI库,也是目前最主流的一类。这类方案以代码库形式提供给开发者,里面封装好了控件、布局、事件回调、绘制函数,开发者只需要创建一个窗口、创建几个控件、绑定回调函数,界面就能跑起来。它不会强制要求底层必须有操作系统,裸机上可以跑,已有的RTOS之上也可以跑,甚至你愿意在main函数里死循环调用它的刷新函数也能工作。

第三类就是FlyThings OS这种平台型方案。它和纯GUI库最大的区别在于:它不是一个代码包,而是一整套交付物——自带一个精简的RTOS内核,自带一套完整的GUI框架,还附带一整套集成开发环境、模拟器、代码生成器和资源打包工具。开发者不需要自己搭RTOS,不需要挑选GUI组件,甚至工程创建的方式都和传统嵌入式项目不一样。这种“全家桶”式的形态,恰好就是“库还是操作系统”这个疑问的来源。

1.2 “库”与“操作系统”的边界在哪里

要回答标题里的问题,先把概念定义清楚。

库的本质是一堆可被链接的代码集合。它的运行模型很简单:你的程序在某个时刻主动调用库里的函数,函数执行完毕返回,一切都在同一个进程、同一个上下文里完成。它不拥有CPU,不管理内存,不负责调度任何东西,离开应用程序它什么都不是。

操作系统的本质完全不同。它负责CPU调度、内存管理、设备驱动、任务间通信,给应用程序提供运行环境和资源分配能力。应用程序通过系统调用请求服务,操作系统决定什么时候执行哪个任务、如何隔离资源、如何响应中断。它有“主人”的身份,应用程序是它的“房客”。

嵌入式GUI方案的尴尬之处在于,它恰好站在这两条线的交叉地带。一个像样的GUI,首先要处理多个并发任务:界面刷新是一个任务,后台业务逻辑是另一个任务,触摸检测和事件派发又牵扯到中断和消息队列。没有底层的任务调度与通信机制,GUI就只能在单线程里打转。但另一方面,GUI的核心价值又全在控件、布局、绘制这些“库”该干的事情上。把这两层混在一起,命名自然就模糊了。

1.3 叫“OS”是营销还是历史惯性

其实嵌入式GUI领域管自己叫“OS”不是一件新鲜事。早年有些知名的嵌入式窗口系统,本质上是窗口管理器加上控件库,命名里仍然带着“Window OS”的痕迹。因为从应用角度看,一个带窗口、带消息循环、带事件驱动的GUI运行环境,本身就营造出了“一个操作系统”的使用体验——应用被事件驱动,系统分发消息,控件响应消息,开发者就像在操作系统上写程序一样思考。

另一个原因更实际。平台型方案补上了库所不具备的配套工具:负责工程管理的IDE、负责界面预览的模拟器、负责生成代码的向导。这些工具合在一起,给人的感觉不再是“我在C工程里include了一个库”,而是“我在一套开发环境里做应用开发”。开发环境这个体验,确实更接近OS级的完整感。所以“OS”这两个字母,与其说是技术事实,不如说是对“一套完整开发体验”的定义。

2. 拆开前两层:RTOS内核和GUI框架各自管什么

2.1 三层总览:从触摸到上屏的数据旅程

以FlyThings OS为代表的平台型方案,从设计上可以切成三层:底层是RTOS内核,中间是GUI框架层,上层是开发工具体系层。理解这个三层结构的最快方式,是追踪一条触摸数据的完整旅程。

用户手指按下屏幕,触摸芯片产生中断。底层RTOS的中断处理程序拿到原始触摸坐标,把它包装成一条输入消息,投递到系统消息队列。这一步,是操作系统层的活。

消息队列另一头,GUI框架层专门有一个消息循环在等待。它取出消息,解析出坐标,把这个坐标传给控件管理模块。控件管理模块根据控件层级、可见区域、命中检测算法,找到用户真正按到的是哪个控件。找到之后,框架调用开发者提前注册好的事件回调函数。这一步,是GUI框架层的活。

事件回调里执行的是开发者的业务逻辑,比如更新一个温度值、切换一个页面。逻辑跑完之后,业务数据要变成屏幕上的像素,于是开发者调用框架提供的控件刷新接口,框架把需要重绘的区域打包,提交给底层的帧缓冲。屏幕控制器扫描帧缓冲,点亮对应像素。整个流程里,三层各管一摊,一层出问题,症状都会表现得完全不一样。

层级核心职责典型模块
底层RTOS层任务调度、内存分配、消息队列、定时器、中断管理内核调度器、消息队列、堆管理
中间GUI框架层窗口管理、控件体系、事件分发、渲染绘制、资源管理控件库、事件系统、帧缓冲管理、字体引擎
上层开发工具体系层工程创建、UI设计、代码生成、模拟调试、资源打包IDE、模拟器、代码模板、打包工具

2.2 底层RTOS层:为什么GUI不能裸奔

很多人有个误解,觉得GUI无非就是“在屏幕上画东西”,一个while循环加一个显示驱动就够了。这种想法在小屏幕、无交互的设备上确实成立,但一旦界面上存在多个需要同时运转的东西,裸奔就会原形毕露。

举一个最常见的场景:界面上有一个正在翻页的数字动画,同时后台在通过通信接口接收数据并解析。如果只有裸奔的main循环,每次通信接收都会阻塞界面刷新,动画立刻出现掉帧、停顿。更麻烦的是,触摸检测往往依赖外部中断,中断里稍一处理复杂逻辑,整个界面的实时性就崩塌。解决这些问题的唯一正确方式,是把“界面刷新”和“业务逻辑”拆成不同任务,让它们拥有各自的时间片。这恰恰是RTOS内核提供的核心能力。

在FlyThings这类平台里,底层内核并不追求功能大而全。调度器、消息队列、互斥锁、系统定时器、堆内存管理,这几个组件是GUI运行的必要条件。至于文件系统、网络协议栈这类对GUI没有直接帮助的模块,通常会被裁剪掉,以节省RAM和Flash占用。这也解释了为什么它叫OS但又不是你印象里那种厚重OS——它是一个“为GUI量身裁剪”的轻量级运行底座。

2.3 中间GUI框架层:控件、事件与渲染的边界

中间这一层是最像“GUI库”的部分,也是最容易被误认成“整个方案都在干这件事”的部分。控件库提供按钮、文本、进度条、滑动条、列表、弹窗等基础元素。每个控件都有自己的属性,比如坐标、尺寸、背景色、字体大小、是否可见。控件之间存在父子关系和层级关系,页面切换时,整套控件树会被创建或销毁。

事件系统是框架层的心跳。控件上可以注册各种类型的回调——点击事件、触摸按下事件、触摸释放事件、定时器事件。框架层用一个循环不断从系统消息队列里取消息,解析坐标,命中控件,调用回调。这套机制和桌面GUI应用程序的消息循环本质上是同构的,区别只在于具体实现和资源开销。

绘制部分则负责把控件“画”到屏幕。常见的做法是维护一块帧缓冲,RGB565或者RGB888格式。当某个控件需要刷新时,框架计算脏矩形区域,把区域内所有控件的绘制代码执行一遍,最终把结果交到帧缓冲。这里有个很关键的设计取舍:如果只采用全屏刷新,逻辑简单但闪烁和性能问题严重;如果精确处理脏矩形,效率高但实现复杂度陡增。开发者平时感觉不到这个细节,只有当刷新出现严重闪烁时,才会意识到底层选择了哪种策略。

资源管理也是框架层的一部分。中文字体、图标图片、多语言字符串,这些资源在开发阶段是独立文件,通过IDE打包成特定格式的二进制资源文件,烧录到设备里。框架层负责在运行时解析这些资源、按需加载字模和位图。这个设计让开发者不用关心底层存储格式,也方便模拟器和真机共用同一套资源。

3. 第三层开发体系:藏在IDE、模拟器和代码生成器里的竞争力

3.1 模拟器“模拟”了什么,又漏了什么

很多人在评估FlyThings这类平台时,注意力全放在GUI框架和RTOS上,却忽略了真正的杀手锏其实是开发工具体系层。首当其冲的是模拟器。

模拟器做的事情看上去很“简单”:在PC上把同一套UI代码编译运行起来,显示在Windows窗口里。但这件事的价值被严重低估。传统纯GUI库的开发流程里,每改一次界面布局或事件逻辑,都要经过编译、烧录、上电、观察屏幕的循环。如果目标板数量有限,还得排队。有了模拟器,界面布局的调整和事件逻辑的验证,可以以秒级速度完成。我见过不少团队从用模拟器之后,UI联调时间被压缩到原来的三分之一左右。

但模拟器也有它“漏掉”的部分。它跑在PC的CPU和内存上,天然不具备目标MCU的性能约束。模拟器上动画流畅,不代表真机上流畅;在模拟器上内存充足,不代表目标板上内存够用。凡是涉及性能、时序、资源紧张的问题,模拟器给不了任何保证,必须回板子上验证。这一点如果认识不清,很容易出现“模拟器完美、上板翻车”的项目事故。

3.2 代码生成与工程模板如何改变开发节奏

平台型方案还有一个容易被当“空气”的功能:代码生成器。使用IDE创建工程时,系统会自动生成一套C++工程骨架,包括主程序入口、任务创建、GUI运行环境的初始化、消息循环的装配。开发者不需要手写RTOS的启动代码,也不需要手动初始化GUI框架层,这些重复劳动全部被模板接管了。

UI编辑器在此基础上更进一步。传统的GUI库开发,控件是通过代码逐个创建的,每个控件的坐标和属性都要在代码里手工调整。改一个按钮的位置,就要改代码、编译、烧录、看效果。而这类平台的IDE里,控件可以直接模板拖放。拖放完成后,IDE自动生成对应的控件对象的C++代码骨架,开发者在生成的函数里填写业务逻辑即可。

这直接改变了开发节奏。传统模式是“编码—编译—烧录—观察”,平台模式变成了“设计—生成—填逻辑—模拟器预览”。整个流程从“面向寄存器/绘图API编程”变成“面向业务逻辑编程”。对产品周期短、迭代频繁的项目来说,这种效率差距往往是决定性的。

3.3 有没有第三层,是“平台”和“库”的分水岭

只提供代码包,即使它再优秀,也只是库。库的交付边界在编译期:开发者拿到源码或静态库,自行决定编译选项、底层操作系统、硬件驱动,所有集成成本都由使用方承担。

平台的交付边界则宽得多。平台不仅给了你一套代码,还给了你一套规则和工具:器件的驱动适配、工程结构约定、事件模型约定、资源打包规范、调试手段。规则意味着约束,但也意味着稳定。在一个项目里,约束驱动的开发,通常比全自由的定制更不容易出低级错误。

所以,有没有这第三层开发体系,不是“体验好不好”的问题,而是“到底是零件还是整车”的问题。零件需要你有自己的装配能力,整车只需要你握住方向盘。FlyThings这类方案选择了整车路线,这是它对“库还是操作系统”之争最实际的回应。

4. 回到标题:它到底是库还是操作系统

4.1 用定义和代码形态逐个对照

现在可以正面回答标题问题了。先说库的维度。FlyThings OS中,开发者最终写的代码确实是以C++代码形式组织起来的,控件API以类的形式暴露,提供创建方法、属性设置、事件绑定。编译之后,这些代码和库一样被链接进最终固件,运行时没有独立的地址空间隔离,没有进程边界,GUI代码和设备驱动代码共享同一个内存空间。从这个意义上讲,它确实具备库的特征——它是应用程序的一部分,而不是凌驾于应用程序之下的独立系统。

再看操作系统的维度。它内部含有任务调度器,多个任务可以并发运行;有消息队列实现任务间通信;有互斥锁保护共享资源;有一个统一的内存堆管理入口。开发者以“线程/任务”的视角来思考业务划分,而不是以“主循环+中断”的裸机方式思考。交互模型是事件驱动,UI代码驻留在特定任务上下文中,消息循环贯穿整个生命周期。这些都是操作系统的典型特征。

判断维度更像库的事实更像OS的事实
代码形态C++类API,编译期链接进固件自带RTOS调度器与IPC组件
运行模型无地址空间隔离,共享内存多任务并发,消息队列驱动
资源管理对外暴露堆分配接口内核统一管理内存与调度
开发体验业务代码像调用普通函数库IDE+模拟器构成操作系统式开发
系统调用不存在标准系统调用层存在裁剪过的服务接口

两边都沾边,两边都不彻底。它不是教科书定义上的完整操作系统,因为它没有进程保护,没有标准驱动框架,也没有用户态和内核态的严格划分。它也不是传统意义上的纯GUI库,因为库不会自带任务调度和消息循环。它是一个介于两者之间的“融合态”。

4.2 更准确的定位:面向带屏设备的GUI应用开发平台

如果非要给它一个最贴切的称呼,我认为“GUI应用开发平台”比“库”或“操作系统”都准确。这个定位有三个层次的含义。

它在技术上提供了一个完整运行环境,从任务启动到GUI初始化再到消息循环,都有标准实现,不需要开发者自己拼装。它在开发流程上提供了一套生产范式,UI设计、代码生成、模拟调试、真机运行形成闭环。它在生态上还附带一套成长路径,资源和工程的规范化让多项目复用成为可能。

这个定位对开发者有一个非常实际的指导意义:你不需要纠结它是不是OS,只需要认清楚工作是“在平台上做应用开发”。界面需求用框架层的能力满足,并发和通信问题用底层内核的能力满足,工程问题用工具链满足。哪个层面出问题,就回到哪个层面去找答案,远比纠结名词更有价值。

4.3 和纯GUI库对比,选型时真正损失的是什么

看清了这个定位,“用平台还是用库”的取舍就清晰了。这不是技术优劣的比较,而是两类开发模式的比较。

用纯GUI库,你拥有极高的自由度。RTOS可以选自己喜欢的,驱动层可以自己写,GUI框架的源码全部可见,想怎么改就怎么改。代价是土建工程量全在自己身上:要移植GUI库,要适配显示驱动,要搭建交叉编译环境,要自己写内存分配策略,要做代码里的UI布局调试。这些工作不是不能用,而是会消耗大量排期。

用平台型方案,开发速度和稳定性是实打实的优势。工程结构替你搭好了,编译链配好了,模拟器也让调试利索很多。代价是自由度受限:你没法随意替换底层RTOS,不能轻易跨到另一家硬件平台,GUI框架的内部实现往往是半封闭的,遇到框架层 bug 时能做的定制非常有限。本质上,你选择的是把技术工作前置,换取后续的开发效率。

对比维度纯GUI库平台型方案
上手成本需要自己搭整个环境开箱即用,几步建工程
界面调试速度编译烧录,周期长模拟器即时预览
底层层选择可自由选择RTOS或裸机使用内置RTOS,不可替换
源码可控性具备完整源码与修改能力框架部分可控,部分封闭
跨硬件迁移相对灵活迁移成本高,平台绑定
交付稳定性依赖团队自身架构能力平台已经过大量收敛

5. 实战经验:用这类平台做产品时要避开的坑

5.1 一个典型界面任务的完整流程

拿一个最常见的“设置页”举例:界面上一排开关,一个数值显示框,一个保存按钮。使用平台型方案,第一步是创建工程,选定屏幕分辨率。分辨率这个参数看着简单,实际影响巨大——它决定了整个UI设计是在一块多大的画布上进行,也会影响后续所有控件的坐标规划。

第二步是在IDE里拖控件。放置开关、数值框、按钮,调整位置和层级。注意控件层级不是越深越好,每个层级都会增加事件命中检测的开销,页面控件超过十几个以后,还是要梳理一下层级关系,把同层级的控件尽量平铺。

第三步是绑定事件。开关控件的状态切换、按钮的点击,都需要选择对应的事件类型。这里建议优先使用“点击”或“释放”这类业务语义明确的事件,而不是去依赖底层触摸事件。否则后续做防抖、做手势扩展时,回调会变得很难维护。

第四步就是在生成的模板函数里写业务逻辑。保存按钮的回调里拼接参数、关掉页面、提示结果,逻辑本身并不复杂。关键点在于这些操作都很轻量,如果后续涉及通信或文件操作,就不能在这个回调里同步执行,需要交由业务线程处理。这块细节在下一小节展开。

第五步是用模拟器验证布局和逻辑。你会发现大量问题在这一步就能解决:按钮位置不对、文本超出边界、弹窗遮挡内容。改完这些,再编译下载到板子,进真机阶段重点验证的就不是UI本身了,而是性能和交互手感。

5.2 事件模型与线程边界:最容易踩雷的地方

平台型方案中,事件回调是运行在GUI线程上下文里的。这个上下文有一个鲜明的性格:它负责界面刷新和事件派发,对时间特别敏感。如果你在回调里写了一行耗时操作,比如一个阻塞式的数据查询、一个稍微大点的字符串拼接、一个耗时的图片解码,整个消息循环都会卡住。症状就是界面“假死”:点击没有反应,动画停止跳动,看起来和死机一样。

正确做法是区分两类逻辑。轻量级的界面状态更新,直接在回调里做。所有响应时间长于十几毫秒的操作,拆到独立业务线程里执行。业务线程处理完结果后,通过消息队列把结果回传给GUI线程,再在回调里触发界面刷新。这样GUI线程始终保持流畅,业务逻辑也不会因为界面卡顿而变慢。

另一个高频踩雷点是数据竞争。业务线程正在修改一个全局变量,同时GUI线程在事件回调里读取同一个变量。如果没有任何同步措施,就会出现偶发性的显示错乱或崩溃。解决方式也很简单:共享数据尽量用互斥锁保护,或者直接使用消息拷贝的方式。消息拷贝虽然多点内存开销,但从架构上消除了大多数并发问题,也更适合资源受限的MCU环境。

5.3 内存、刷新、字体等高频问题排查速查表

真机调试阶段,问题通常会集中在几个固定方向。下面按我踩坑的频率从高到低整理出来:

异常现象可能原因排查与处理方向
界面严重闪烁刷新策略是全屏刷新或缺帧缓冲检查框架是否支持局部刷新,确认帧缓冲配置是否正确
点击某个控件无反应事件循环被耗时逻辑阻塞检查是否有回调里做了耗时操作,把耗时逻辑移出GUI线程
触摸坐标偏了触摸屏校准参数不对或坐标系转换有误确认坐标转换逻辑,用触摸测试页打印原始坐标比对
中文显示乱码或空白字体资源不完整,编码与字库不匹配在IDE资源面板检查字体是否包含所需字符,默认编码是否匹配
模拟器流畅但真机卡顿目标MCU主频偏低、RAM偏小、像素格式转换开销大缩减刷新区域,把色彩格式尽量统一,禁用透明叠加效果
运行几天后内存溢出控件频繁动态创建未释放,堆碎片累积减少动态创建控件,尽量复用控件对象,检查对象回收路径
偶发性系统重启某个任务栈溢出,访问越界查看栈溢出检测标记,加大任务栈,检查越界写操作

字体问题尤其值得多说两句。中文设备上,字体文件体积通常比预期大很多。平台型方案通常支持把字体子集化——只打包用到的字符,减小资源体积。首次制作字体资源时,经常出现“这个字显示正常,那个字是空白”的情况,多半是字体子集打包时遗漏了字符。每次界面文案变更,都记得同步更新字体资源,这个习惯能省掉不少现场问题。

内存方面,如果项目使用了大量页面切换,控件反复创建销毁几乎是必然的。平台类方案的内存堆通常从RAM中划出固定大小,频繁分配释放会产生碎片。经验之谈是尽量复用控件,或者采用页面预创建策略——把常用页面的控件在初始化阶段一次性建好,切换时只控制可见性,消耗的内存换来的是稳定性和流畅度。

最后是性能排查的定位思路。真机卡顿出现时,先确认是不是渲染瓶颈:把动画区域缩小,看是否好转。再确认是不是逻辑瓶颈:把业务线程的通信频率降低,看是否好转。如果两者都不明显,则问题多半出在帧缓冲写屏环节,比如色彩深度转换太低效。一次性能问题排查,基本就是这三步的排列组合。

我个人在这些年里先后经历过裸机自绘、纯GUI库和平台型方案三种路线,最大的感受是:纠结“库还是OS”这件事,本意不是要站队,而是校准预期。纯库给你的是零件和自由,平台给你的是流水线和规则。如果你享受自己搭一切的过程,选库无可厚非;如果你更在意产品迭代速度和交付稳定性,这种三层平台型方案会顺手得多。最后留一个小建议:拿到这类平台,先别急着拖控件,把文档里“消息模型”和“内存管理”两章看明白,后面省下的时间一定会超过你此刻投入的精力。

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

基于MP2642GR-Z与STM32L4S5ZI的两串磷酸铁锂主动均衡系统设计

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

作者头像 李华
网站建设 2026/10/15 1:33:18

ESP8266 AT指令调试入门:从接线到TCP通信实战

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

作者头像 李华
网站建设 2026/10/15 1:33:06

几何直观与向量抽象:解题中的认知协议切换

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

作者头像 李华
网站建设 2026/10/15 1:33:02

C#多线程编程实战:从Task选型到线程安全与死锁预防

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

作者头像 李华
网站建设 2026/10/15 1:32:42

Apollo-11机载计算机启示:资源受限下的嵌入式实时系统设计

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

作者头像 李华
网站建设 2026/10/15 1:32:41

Siamese RPN全解析:从模板匹配到端到端目标跟踪回归

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

作者头像 李华