新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android显示链路全解:SurfaceFlinger、HWC与驱动协作机制

发布时间:2026/9/28 16:33:28来源:尧图网络
Android显示链路全解:SurfaceFlinger、HWC与驱动协作机制
干Android显示系统这行最绕不开的就是SurfaceFlinger、HWCHardware Composer和显示驱动这三个角色。很多人对SurfaceFlinger的Layer管理和BufferQueue机制比较熟但一说到HWC到驱动这一截就有点发虚总觉得无非是“硬件能合成就让硬件合成不行就GPU兜底”。这句话只讲对了一半。真正需要弄清楚的是HWC在SurfaceFlinger和显示驱动之间到底扮演什么角色合成决策是怎么一步步做出来的驱动侧的Plane又是怎么跟HWC的Layer对应上的这篇文章我打算按实际项目里从头到尾调试链路的方式把一条buffer从App侧画完到SurfaceFlinger做合成再到HWC做合成决策最后通过显示驱动落到屏幕的完整轨迹理清楚。内容偏实战适合正在啃Android源码、移植显示驱动、或者被显示性能问题折磨的工程师参考。1. 先建立全局视图谁负责合成、谁负责显示1.1 上层调度、策略决策与底层执行的边界要理解HWC第一步得先搞清楚Android显示系统里三个角色的职责边界。SurfaceFlinger是Android framework层的核心服务它管的是“合成调度”。系统里每个可见窗口都会对应一个SurfaceSurfaceFlinger把它们统一封装成Layer对象维护一份全屏可见图层列表然后根据VSYNC节奏决定什么时候去取这些图层、怎么合成、什么时候送显示。但SurfaceFlinger本身不直接操作屏幕硬件它只能通过HWC暴露出来的接口去“下发”合成任务。HWC的全称是Hardware Composer它是一个HAL层模块。它处于SurfaceFlinger和内核驱动之间职责很明确向SurfaceFlinger暴露合成能力比如支持多少个硬件overlay层、支不支持旋转、支不支持某些像素格式然后按照SurfaceFlinger的请求把可硬件合成的图层配置到显示控制器上把不可硬件合成的图层回退给GPU。HWC是一个厂商实现的so库最终通过libdrm或者业务私有接口去操作内核驱动。显示驱动承担的是最底层落地的部分。在Linux内核里这几年主流方案已经是DRM/KMS框架。显示驱动负责初始化CRTC、Encoder、Connector、Plane这些显示资源按HWC配置好的参数把多个Plane的数据混合成一路像素流经DSI、DP或者HDMI接口输出到屏幕。用一句话类比SurfaceFlinger是总包甲方负责排计划HWC是包工头负责调度硬件资源驱动是施工队负责真正把像素送到屏幕。很多人调显示问题一上来就翻内核log其实大多数问题在SurfaceFlinger和HWC这一层就已经能定位了。1.2 为什么显示链路离不开硬件合成先算一笔账。一块1080P分辨率、RGB888格式的buffer一帧大小大概是1920x1080x3约6.22MB。60Hz刷新率下一秒就有120帧级别的读写动作GPU读多个图层、混合、写回一块新buffer带宽轻轻松松就到GB/s量级。如果全屏有5个可见图层GPU先读5块buffer、再写1块buffer一秒钟光这块数据流量就奔着几个GB走了。功耗、发热、延迟全都会上去。硬件合成器解决的就是这个问题。显示控制器内部往往自带多个overlay plane每个plane可以直接绑定一块buffer。CRTC把所有plane的数据实时混合成最终画面不需要先经过GPU写回显存。这一下就把“N块buffer读出来混合再写回”变成了“N块buffer直接喂给显示控制器硬件混合”省掉的不是一点半点功耗和帧延迟都会明显改善。所以HWC的核心价值就在这里能用硬件平面直接合成的图层尽量不碰GPU。只有HWC能力不够、或者图层属性太复杂比如某些非标准旋转、特殊混合模式的时候才把一部分图层丢回SurfaceFlinger让GPU合成到一块client target再交给HWC去显示。2. 全程链路拆解一个图层从App到屏幕发生了什么2.1 App生产buffer与BufferQueue的三方交接链路的最上游其实是App。每个App窗口在SurfaceFlinger侧对应一个BufferQueueApp是生产者SurfaceFlinger是消费者HWC和驱动则是在更后面接着消费最终结果。App需要画一帧时通过dequeueBuffer从BufferQueue里取一块空闲buffer交给CPU或者GPU渲染完成后再queueBuffer把它交还给BufferQueue同时带出一个fence表示“我这帧画完了消费者可以读了”。SurfaceFlinger在VSYNC回调里检查各Layer的BufferQueue有没有新buffer可用有的话通过acquireBuffer取走之后就会进入合成流程。这里有一个很容易被忽略的点这块GraphicBuffer的内存底层一般来自ION或dma-buf分配器是物理连续的或者至少是能映射到显示控制器地址空间的内存。HWC和驱动要合成它必须能访问到同一块物理内存。所以HWC层看到的buffer_handle_t说到底是一个可以翻译成dma-buf fd的句柄。理解了这一点后面看HWC和DRM之间怎么传递fb_id就会很顺。fence在链路里也极其重要。Android图形栈的buffer到处都带着acquire fence和release fence。acquire fence表示“消费者要等这个fence signal之后才能读buffer”release fence表示“生产者要等这个fence signal之后才能继续写buffer”。GPU画完一帧后release fence会signalSurfaceFlinger/HWC拿到buffer后就知道内容可用了。如果fence处理不对最常见的现象就是画面卡死第一帧或者出现撕裂。2.2 SurfaceFlinger如何拍板“GPU合”还是“HWC合”每个VSYNC周期SurfaceFlinger都会对当前所有可见Layer做一次合成决策。它不是自己死记硬背而是把候选Layer集合交给HWC去“表态”。具体决策依据主要是这几个维度Layer的Z序、位置、尺寸、透明度和混合模式、像素格式以及HWC是否支持这块图层的旋转与缩放。HWC的validate结果才是最终判断标准能支持的Layer标记为Device合成由HWC硬件平面做不支持的Layer标记为Client合成意味着SurfaceFlinger要先用GPU把它们全部合成到一块buffer上。不少看过SurfaceFlinger日志的人应该见过这样的输出一个Layer显示的是“Device composition”还是“Client composition”。如果屏幕上状态栏、导航栏、主界面都标注为Device说明HWC基本接住了大多数图层整体效率高。如果整个列表几乎全是Client那就要警惕了大概率HWC能力不足或者驱动哪里有缺陷。这里有个容易误解的地方Client合成不是把每个Layer单独合成一次而是把多个Client Layer按Z序连续混合到一块临时的client target buffer里。这块buffer在HWC眼里就当成普通一整个Layer来处理。所以HWC最终看到的Layer数里可能包含一个巨大的client target再加上若干个Device Layer。2.3 HWC的validate/present一次合成请求的完整生命周期HWC 1.x时代接口非常直白prepare阶段和set阶段。prepare时SurfaceFlinger把全部Layer交给HWCHWC逐个“表态”能合就置为HWC_OVERLAY不能合就置为HWC_FRAMEBUFFER。set阶段SurfaceFlinger先把标记为FRAMEBUFFER的图层用GPU合成好再调一次set把整个Layer列表包括GPU合成后的framebuffer target交给HWC送显。到了HWC 2.x以后接口模型变成了对象化的IDevice、IDisplay、Layer流程也更精细。整个生命周期大致是这样的SurfaceFlinger先把每个Layer的信息设置到HWC对应的Display对象上包括layer的buffer、合成类型、显示区域、裁剪区域、变换矩阵、混合模式等。全部设置完以后调用validateDisplay让HWC做一次校验。HWC会用自身能力逐项检查这组Layer能不能在硬件上合成能就把合成类型定为DEVICE不能就打回CLIENT并通过getChangedCompositionTypes把需要回退的Layer列表返回给SurfaceFlinger。SurfaceFlinger处理完这些回退Layer也就是GPU合成到client target再重新把设置刷新一遍再一次validate如此反复直到所有Layer都达到可提交状态。最后调用presentDisplay把最终Layer列表和fence一起交出去HWC在合适的VSYNC时机把它们配置给显示控制器屏幕上就能看到画面了。这个过程中最考验实现质量的是fence的传递和同步。present时HWC要等acquire fence signal确保buffer内容已经画好然后再触发显示控制器去读。等显示控制器真正读完以后release fence再signalbuffer才能还给App继续画。这一套同步链条任何一个环节卡住都会直接表现成卡顿、花屏或者屏幕静止。3. HWC HAL开发实战从接口实现到驱动调用3.1 HWC 1.x到2.x/3.x接口模型为什么越分越细如果只做应用开发其实不太需要关心HWC版本演进。但只要是碰framework或者移植显示系统就躲不开这个问题。HWC 1.x把所有能力塞在hwc_composer_device_1上结构体里一堆函数指针Layer列表用数组往下传。好处是简单坏处是扩展性差虚拟显示比如投屏支持不好多显示器管理也困难任何新增能力都得改一堆函数指针。HWC 2.x引入的IDevice、IDisplay、Layer对象模型是一次比较大的重构。每个Display独立管理虚拟显示和物理显示都能用同一套逻辑描述Layer的合成类型、fence、dataspace这些属性都有了明确归属。到Android 10之后HWC又改为通过AIDL的Composer HAL也就是常说的HWC 3.xcomposer3来暴露底层还是类似的IDevice/IDisplay概念只是把接口定义切到了AIDL语言配合Treble架构让系统升级更干净。从实现者的视角看HWC 2.x和3.x的核心要处理的事情其实没变对外正确上报能力对内正确把Layer映射成DRM的Plane配置。接口变了思路没变。3.2 最小可用的composer HAL骨架讲骨架之前先说清楚完整实现一个商用HWC的工作量很大这里只是把最关键的方法剥出来让你知道SF调HWC时到底会落在哪些函数上。以AIDL化的composer HAL为例核心类大概是这样的结构// MyComposerHal.h using android::hardware::graphics::composer3::IComposerHal; class MyComposerHal : public IComposerHal { public: Error getDisplayCapabilities(DisplayId display, DisplayCapabilities* outCaps) override; Error setLayerCompositionType(DisplayId display, LayerId layer, Composition composition) override; Error validateDisplay(DisplayId display, const std::vectorLayerId layers, std::vectorComposition* outChangedTypes, std::vectorLayerId* outRequestedLayers) override; Error presentDisplay(DisplayId display, const std::vectorLayerId layers, int32_t* outPresentFence) override; };getDisplayCapabilities里要如实上报这块显示控制器支持的能力比如最大图层数、支持哪些变换、支持哪些像素格式、有没有硬件双缓存。这一项不要为了跑分虚报后面validate不匹配的时候还是会露馅。setLayerCompositionType就是一个单纯的“记录”动作把SF期望的composition存下来顺便做一轮能力检查。如果SF想设成DEVICE但当前硬件资源不够可以在这里先记一个标记等validate阶段统一返回。validateDisplay是核心。这里要做的事包括统计Layer数量是否超过可用Plane数量检查每个Layer的格式、尺寸、旋转、缩放是否被硬件支持如果发现不支持就把对应Layer的合成类型改成CLIENT塞进outChangedTypes。这里有一个实现细节validate返回后SF通常会重新设置这些Layer的属性再调用validate所以validate必须能处理“上次说不行、这次改好了”的状态变化。presentDisplay是真正要“干活”的地方。大部分现代平台都走DRM atomic commit我简单写一下基本套路// MyComposerDisplay.cpp int32_t MyDisplay::present(const std::vectorLayer layers) { drmModeAtomicReqPtr req drmModeAtomicAlloc(); int planeIdx 0; for (auto layer : layers) { if (layer.compositionType Composition::CLIENT) continue; // client target在另一个单独的commit里处理 auto plane mPlanes[planeIdx]; // 把layer的buffer映射为drm framebuffer uint32_t fbId framebufferFor(layer.buffer); drmModeAtomicAddProperty(req, plane.id, mPlaneFbIdProp, fbId); // src和crtc矩形 drmModeAtomicAddProperty(req, plane.id, mPlaneSrcProp, srcRectValue(layer)); drmModeAtomicAddProperty(req, plane.id, mPlaneCrtcProp, crtcRectValue(layer)); // 关联crtc drmModeAtomicAddProperty(req, plane.id, mPlaneCrtcIdProp, mCrtcId); } int ret drmModeAtomicCommit(mDrmFd, req, DRM_MODE_ATOMIC_NONBLOCK, nullptr); drmModeAtomicFree(req); return ret 0 ? Error::NONE : Error::BAD_PARAMETER; }这里还没展开fence、layer的acquire/release处理以及client target的上屏路径但你已经能看到整条链路的“接缝”HWC把SF的Layer对象翻译成DRM的Plane属性commit下去之后剩下的就是内核的事情。3.3 这里的坑最深fence、composition状态和buffer所有权第一类坑是fence不signal。最常见的情况是HWC实现里没有把一个fd正确dup导致多路引用时fence被提前关闭内核里那个sync object再也等不到signal画面就卡死在第一帧。排查的时候用systrace抓SF的acquireFence和presentFence能看到fence迟迟不signal。这个问题几乎每个没有成熟经验的HWC实现团队都会踩一次。第二类坑是composition状态不一致。HWC在validate阶段把某些Layer标记为CLIENT但SF重新提交时HWC内部还留着旧的DEVICE缓冲状态。等present的时候新旧状态混淆屏幕输出不是花屏就是图层错位。解决办法是在validate通过之后clear掉旧的Layer状态强制以最新的set结果为准。第三类坑是buffer所有权交接。HWC把buffer交给DRM去显示后内核和HWC都必须明确谁在什么时候能把buffer的所有权释放。尤其要注意release fence的生成时机必须等到显示控制器真正读完buffer之后再signal。如果提前signalApp下一帧就会写到一块还在被显示控制的buffer上画面撕裂就在所难免。第四类坑是能力上报不实。比如硬件明明只有两个overlay planeHWC却上报支持四层设备合成。结果就是SF高高兴兴把四个Layer都标成DEVICE提交过来HWC在validate时又只能打回一部分CLIENT来回多跑几轮帧率直接被拉低。能力上报的原则永远是“保守”上报前最好实际测一圈旋转、缩放、格式的组合。4. 驱动侧落地drm/kms如何承接HWC的合成请求4.1 CRTC、Plane、Connectordrm为HWC准备的硬件抽象内核侧如果走DRM/KMSHWC要操作的对象就非常明确了。DRM把显示硬件抽象成几个对象CRTC对应显示控制器负责把buffer里的像素数据变成输出时序Plane对应硬件叠加层可以把多块buffer直接送进CRTC做混合Encoder负责把像素流编码成接口协议比如DSI的包格式Connector代表物理接口比如eDP、HDMI、DSI它连接着panel。HWC与DRM对象的对应关系在实战中是非常明确的SF里的一个Layer对应HWC里的一个Layer再对应DRM里的一个Plane。SF里合成出来的client target在DRM里通常挂在Primary Plane上。CRTC最终把Primary Plane和所有Overlay Plane混合成一路输出经Encoder和Connector发到面板。所以当你在dumpsys SurfaceFlinger里看到某个Layer是“Device composition”时心里应该立刻想到这个Layer后面一定有一个DRM Plane并且那个Plane的fb_id一定指向这块Layer的buffer。反过来如果你在drm状态里看到某个Plane被disable但SF却把它标成了Device合成那说明HWC和驱动之间的状态已经对不上了。实际调试时/sys/kernel/debug/dri/0/state这个debugfs节点非常有用能看到每个Plane的fb_id、crtc_id、src/crtc矩形、rotation属性是不是处于预期状态。曾经有客户报告“某个画面里视频区域屏幕闪烁”最后就是通过对比dumpsys和drm state发现视频那层实际被当成Client合成写到了Primary Plane而驱动里Primary Plane没开带fence的异步更新画面刷新自然就有撕裂。4.2 DSI链路与显示时序参数的计算移动平台绝大多数屏幕走的是MIPI DSI接口。DSI分两种工作模式Command Mode和Video Mode。Command Mode下面板自带GRAM主机把图像数据写入面板的GRAM面板自己负责刷新适合低功耗、低刷新率的屏幕比如智能手表Video Mode下SoC源源不断把像素流推给屏幕面板只是个实时显示终端手机、平板的主力屏基本都是这种模式。调试DSI屏幕时最基础的工作是确认时序参数。一个常见场景是换屏后显示花屏或黑屏第一反应就该检查时序里的h_active、h_back_porch、h_sync_pulse、h_front_porch、v_active这些值是不是跟屏规格书一致。DRM侧对应的是drm_display_modeHWC本身一般不直接改这些参数但屏幕刷新率的最终效果跟它强相关。如果只是粗略估算DSI时钟需求可以这么算byte_clock (h_total) x (v_total) x fps x bpp / lanes / 8。我拿一个1080P、24bpp、60Hz、4 lane的屏幕举例假设h_total约2200、v_total约1125那么每秒总像素约2200x1125x60≈148.5Mpixel乘以24bit就是约3.56Gbps除以4条lane每条lane大约需要890Mbps。这个数值折成byte clock就是约110MB/s的DSI时钟频率。实际还要加上DSI包头的ECC和CRC开销所以配置时往往要留一点余量。这种计算在HWC调试中通常不会直接去做因为驱动层的panel驱动已经配好了但排查“刷新率只有标称一半”“屏幕闪烁”这类问题时懂这个计算能帮你更快怀疑到带宽或时钟配置上。我曾经见过一个案子h_total和v_total里两个blanking值没配对屏幕能亮但刷新率从60掉到30还伴随水平方向的细纹后来就是把blanking恢复成规格书数值才解决。4.3 驱动侧常见显示问题与排查方向黑屏是驱动侧最常见也最让人头疼的问题。拿到一个黑屏报告先分清是完全没有背光还是背光亮但没有图像还是图像一闪而过。完全没有背光优先查面板供电和背光使能GPIO背光亮但没有图像优先查DSI video mode的clock lane和data lane配置如果是开机logo正常、进入Android后黑屏那就要把注意力放到HWC和SurfaceFlinger的合成路径上很可能是Layer提交后HWC没有正确commit CRTC或者urfaceFlinger的client target没有正确上屏。花屏和撕裂通常是时序或者buffer同步问题。花屏重点检查像素格式是否匹配尤其注意RGB888和RGB565混用的场景撕裂则十有八九是fence没等住或者display controller在buffer还没写完时就去读了。刷新率异常的问题经常出现在VRR或者高刷新率屏上。如果屏幕标称120Hz但实际跑在60Hz第一件事是用dumpsys display看当前activeMode是不是真的切到了120Hz模式再看DSI时钟是否足够支撑120Hz的带宽。带宽不够时面板端会表现为闪屏、细纹甚至直接黑掉。5. 实战调试三板斧抓合成决策、盯fence、验证显示帧率5.1 dumpsys与Perfetto把合成决策和性能一次看清做HWC相关调试我一般会固定用几个工具它们在定位问题时的作用各不相同。第一个是dumpsys SurfaceFlinger。这个命令会把当前所有Layer、合成方式、buffer状态、fence状态都打出来。看的时候重点看每个Layer后面跟着的composition类型以及底部有没有报HWC错误。如果所有Layer都变成Client composition优先怀疑HWC的能力上报或者validate实现有问题。第二个是dumpsys SurfaceFlinger --latency。它会输出最近帧的时间戳样本能直观算出当前帧率是不是稳定有没有周期性掉帧。这个命令在验证刷新率配置和VSYNC调度是否正常时特别有效。第三个是Perfetto。它可以把BufferQueue的dequeue/queue、SurfaceFlinger的vsync和合成、HWC的validate/present整个执行链拉成一条可视化时间线。fence卡住、等待时间过长、HWC提交太慢这些问题在Perfetto里都是一眼能看出来的。内核侧配合一个简单的ftrace就能抓到drm_atomic_commit的耗时。第四个是debugfs下的drm state文件可以直接看到每个Plane的state包括fb_id、crtc矩形、src矩形、rotation、active状态。这个文件和dumpsys SurfaceFlinger对照着看能很快确定HWC下发的Layer有没有真正变成DRM Plane的配置。5.2 典型问题速查现象、原因、处理手段我整理了一张比较实用的速查表基本都是实际项目里反复出现的几类情况现象可能原因排查手段画面卡在第一帧不动fence未signal或buffer所有权没有正确交接Perfetto抓acquire/present fence检查是否出现pending fence超时全屏图层回退到GPU合成HWC overlay不足、能力上报不实、validate逻辑过早放弃dumpsys SurfaceFlinger看composition类型对照drm state看plane数量花屏像素格式不匹配、Plane的stride配置错误、SRC/CRTC矩形错位检查drm state中fb的format和plane矩形再对比buffer的stride闪烁VSYNC丢失、fence等待超过刷新周期、DSI clock余量不足Perfetto看SF/HWC帧间隔再用DSI clock估算余量刷新率减半mode没切成功、blanking参数错误、带宽不够dumpsys display确认activeMode结合clock估算公式验算低亮度时屏幕微闪背光PWM频率过低、产生可见频闪调整背光驱动PWM频率避开与刷新率的倍频关系投屏画面异常Virtual Display相关能力未实现、dataspace转换遗漏单独跑virtual display用例检查Display的type和capabilities这类问题里很大一部分多花五分钟在dumpsys和drm state上排查能省下后面好几个小时的猜谜时间。尤其是合成方式回退这类问题如果能在dumpsys输出里看到到底哪一层被标记为Client就已经解决了八成。最后再说一个我自己的习惯做HWC开发时永远把“当前合成决策”作为第一个观测点。不管来的是什么显示异常先把dumpsys SurfaceFlinger拍下来再抓一份drm state两边一对照是SF给错了、HWC传错了、还是驱动没配上基本立见分晓。这个习惯帮我避开过大量没有头绪的排障过程也推荐给所有正在折腾Android显示链路的朋友。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

为你的“小龙虾”OpenClaw开发自定义 Skills:从 SKILL.md 到 TypeScript/Shell 实战 2026/9/28 18:53:24

为你的“小龙虾”OpenClaw开发自定义 Skills:从 SKILL.md 到 TypeScript/Shell 实战

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

阅读更多 →
2025年上半年前端技术圈生态总结:TaoToken 统一 Key 接入 AI 工具配置实战 2026/9/28 18:53:24

2025年上半年前端技术圈生态总结:TaoToken 统一 Key 接入 AI 工具配置实战

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

阅读更多 →
AI智能体训练与企业级应用落地:从可验证奖励到工程化实践 2026/9/28 18:53:23

AI智能体训练与企业级应用落地:从可验证奖励到工程化实践

今天是2026年9月19日,照例把最近AI圈子里真正值得关注的事情翻了一遍。现在每天AI行业的动态多到刷不过来,绝大多数是模型发布、融资消息、产品更新,热闹归热闹,但跟一线干活的人关系不大。所以这份日报我不想做成新闻流水账&…

阅读更多 →
Linux之Http<3>--表单 2026/9/28 18:53:17

Linux之Http<3>--表单

之前的代码编写,都是对网站资源的请求,等等 ,那些都是对于静态资源的获取但是还存在动态内容的获取,比如动态站点,支持用户进行登录,注册,支付,查找等功能功能静态资源请求 VS 动态请求1. 静态资源请求目标:.html、.png、.css、.js这类文件特点:文件内容…

阅读更多 →
用好 Pi:一份来自官方文档与社区实战的深度技巧指南 2026/9/28 18:53:17

用好 Pi:一份来自官方文档与社区实战的深度技巧指南

用好 Pi:一份来自官方文档与社区实战的深度技巧指南 如果你已经被各种 AI 编程 Agent 的"全家桶"压得喘不过气——臃肿的系统提示词、点不完的权限弹窗、永远猜不到它在想什么的"黑盒"——那么 Pi(pi.dev)可能是你一直在…

阅读更多 →
立创EDA + AI 生成 STM32 原理图教程:用 TaoToken 统一 Key 打通配置链路 2026/9/28 18:53:11

立创EDA + AI 生成 STM32 原理图教程:用 TaoToken 统一 Key 打通配置链路

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉