简介这份演示文稿聚焦 Android SystemUI 架构设计面向系统开发、Framework 定制及系统 UI 组件学习者帮助理解 SystemUI 的职责范围、模块划分和通知处理链路。内容从 SystemUI 的定位与源码目录切入依次介绍状态栏、通知、锁屏、最近任务面板、音量 UI、电量 UI、画中画、生物识别解锁等功能模块并补充导航栏、快捷设置等常见系统级交互场景同时给出结构图、类图、启动流程以及通知注册、系统发送到 SystemUI、SystemUI 内处理等关键流程便于建立从整体架构到具体模块的认知。压缩包内共 1 个 pptx 文件大小约 11.64MB以图文页面组织模块关系与流程说明适合按章节浏览、对照复盘和整理学习笔记整体以主题分页推进便于快速定位。已有 403 人学习可作为 Android 系统 UI 架构入门、功能定位与知识查漏的参考材料便于在系统定制或问题排查前建立知识储备。1. 一个反直觉的结论SystemUI 不是一个 App而是几十个模块的“合租房”提到 SystemUI架构设计多数人的第一反应是“系统自带的那个界面 App”。从外观上看确实如此锁屏、状态栏、导航栏、最近任务、快捷设置全在它身上源码位置也规规矩矩躺在frameworks/base/packages/SystemUI下编译产物就是一个 apk。但只要你打开它的入口类第一反应会是困惑——里面没有一棵统一的视图树也没有一个“主页面”的概念而是一堆彼此不认识的组件各自管一块窗口各自决定什么时候启动。这个“合租房”式的组织方式才是 SystemUI 所有设计取舍的起点。它要解决的问题是几十个功能独立、生命周期不同、对系统权限要求极高的 UI怎么塞进一个进程还不互相拖死。搞懂这一点后面模块划分、启动流程、通知管线为什么长成那样全都是顺理成章的。2. 进程模型SystemUI 为什么必须以 system 身份常驻普通 App 想要画一个悬浮窗得先申请SYSTEM_ALERT_WINDOW用户点头了才有资格SystemUI 不这样它从一开始就以system身份跑在一个叫com.android.systemui的进程里加窗口时用的是只有系统身份才能碰的窗口类型。这个身份不是特权而是硬性需求——状态栏、导航栏、锁屏这些窗口如果允许第三方随意覆盖整套交互秩序就没了。2.1 SystemUIService 的启动时序与 SystemServer 的交接SystemUI 不是被用户点开的也不是被 Launcher 拉起的它由 SystemServer 在启动流程的后期主动拉起。大致的时序是SystemServer启动ActivityManagerService、WindowManagerService、PackageManagerService等核心服务后在startOtherServices阶段调用startSystemUi(context, windowManagerService)最终走到context.startServiceAsUser(new Intent(...SystemUIService.class), UserHandle.SYSTEM)。这里有个容易被忽略的细节startServiceAsUser指定了UserHandle.SYSTEM也就是用户 0。这意味着 SystemUI 的主实例永远属于系统用户多用户场景下的其他用户是通过额外的组件实例去覆盖的而不是再起一个同名进程。交接完成后AMS 负责创建进程、加载 apk、实例化Application真正的“启动”才交给 SystemUI 自己。// SystemUIApplication 的启动骨架结构大致如此 public class SystemUIApplication extends Application { private SystemUI[] mServices; private boolean mBootCompleted false; Override public void onCreate() { super.onCreate(); // 1. 初始化依赖容器把 Context 和主线程 Handler 塞进去 Dependency.init(this); // 2. 组件清单来自 config不是硬编码写死的 mServices new SystemUI[] { new StatusBar(), // 状态栏 锁屏宿主窗口 new KeyguardViewMediator(), // 锁屏状态的中介者 new Recents(), // 最近任务 new VolumeUI(), // 音量面板 new PowerUI(), // 低电量、关机流程 new RingtonePlayer(), // 铃声播放 new PipUI(), // 画中画 new BiometricDialogImpl(), // 生物识别弹窗 // ... }; startServicesIfNeeded(); } private void startServicesIfNeeded() { for (SystemUI svc : mServices) { // 未开机完成时只启动标记为“必须常驻”的组件 if (mBootCompleted || svc.isEnabled()) { svc.start(); } } } }这段代码里最关键的一行不是start()而是isEnabled()。它决定了某个组件在开机广播到来之前要不要提前起来。状态栏必须提前起因为它要在锁屏界面显示信号和电量画中画可以晚一点因为它只在有视频应用请求时才需要。参数上mBootCompleted由BOOT_COMPLETED广播置位isEnabled()则读config.xml里的布尔开关厂商定制时改的就是这个值。2.2 一个进程多个组件Dependency 与组件的生命周期Android 的Application只有一个onCreate但 SystemUI 需要几十个组件共享同一批对象NotificationEntryManager、KeyguardStateController、StatusBarStateController、CommandQueue跟 SystemServer 通信的桥。如果每个组件自己 new 一份状态立刻就对不上了——锁屏以为自己是展开的状态栏以为自己是收起的用户看到的就是半个屏幕卡住。SystemUI 的解法是一个很轻的静态容器Dependency启动时把接口和实现按类型注册进一张MapClass?, Object组件在构造函数里用Inject标注参数容器反射完成注入。这不是 Dagger没有编译期代码生成代价是运行期反射的开销和“注册顺序错了就 NPE”的脆弱性收益是构建简单、启动快——对一个要在开机后几百毫秒内把状态栏画出来的进程来说这笔账算得过来。组件本身实现SystemUI抽象类只要求三个方法start()开始干活、dump()供dumpsys输出内部状态、以及可选的isEnabled()。这套约定看起来简陋但它带来一个很实在的好处——排查问题时adb shell dumpsys activity service com.android.systemui/.SystemUIService能直接把每个组件的自述打出来不用猜。2.3 为什么模块要“按需启动”而不是全量加载按需启动不是为了省内存那么简单。SystemUI 有几个组件的初始化成本极高Recents要跟 AMS 的RecentTasks建立回调、PipUI要跟PipManager绑定、BiometricDialogImpl要注册BiometricPrompt的客户端。如果开机就全量初始化状态栏首帧的绘制会被拖到几秒之后用户看到的是黑屏加一个迟到的状态栏。更麻烦的是权限和状态依赖某些组件必须在特定用户解锁之后才能工作提前初始化会拿到一个null的UserHandle然后在整个生命周期里带着这个错误状态跑下去。所以 SystemUI 的做法是分两批——第一批只起“开机可见”的组件剩下的等BOOT_COMPLETED再统一start()。这个设计后来被很多模块借鉴血泪经验就是能把初始化推迟的一律推迟。3. 模块划分每个子类都是一个功能域而不是一个页面看懂模块划分的关键是放弃“页面”这个心智模型。SystemUI 的一个模块通常对应一个功能域它可能包含多个窗口、多个跟系统服务的 binder 连接、以及一套自己的状态机。模块之间不直接调用只通过Dependency里的共享状态对象间接通信这是它能在单进程里活下来的前提。3.1 StatusBar 与窗口类型状态栏为什么不能被普通 View 覆盖StatusBar这个名字有误导性它其实管了两样东西状态栏本身以及锁屏的宿主窗口。在窗口层级上它通过WindowManager添加一个类型为TYPE_STATUS_BAR的窗口携带着StatusBarWindowView这棵视图树。这个窗口类型属于系统窗口区间普通应用的窗口无论怎么设Z-order都盖不上去。这解释了日常开发里的一个经典困惑为什么我在应用里全屏、隐藏状态栏下拉一下状态栏还是能出来因为“隐藏”只是把窗口的可见性置为GONE窗口本身还在WindowManagerService的层级表里StatusBar收到手势后可以自己再把它设回来。系统从来不信任应用能真正“禁用”状态栏。StatusBarWindowView这棵树里同时挂着三块内容状态栏图标区、下拉后的快捷设置面板NotificationPanelView、以及锁屏容器。它们同属一个窗口所以锁屏和状态栏之间的动画不用跨窗口做直接在一个ViewGroup里改 translationY 就行。这是设计上的一个精明取舍——用“窗口合并”换“动画一致性”代价是这个窗口的布局复杂到几乎没人能一次讲清楚。3.2 KeyguardViewMediator锁屏为什么是一个“中介者”而不是一个 View锁屏的实现名字里带Mediator这个词是刻意的。锁屏不是一棵视图树而是一组状态的协调器KeyguardStateController记录当前是锁屏、解除还是遮挡KeyguardUpdateMonitor订阅指纹、人脸、SIM 卡、电池等系统事件KeyguardSecurityContainer负责密码/PIN/图案的输入校验。KeyguardViewMediator站在中间谁的状态变了就通知其他人。之所以不把锁屏写成一个大 View是因为锁屏的“决策者”根本不在 UI 进程里。什么时候锁屏、什么时候允许解锁、密码错了几次要延迟这些判断由WindowManagerService和ActivityManagerService掌握。KeyguardViewMediator通过IKeyguardService跟系统侧通信UI 只是把系统决策渲染出来。你在这边改了一个动画系统那边的KeyguardStateController状态没变界面立刻就会自己弹回去——这类“改了没用”的坑本质上都是没搞清楚谁才是状态的权威来源。3.3 Recents / VolumeUI / PowerUI / PipUI / BiometricDialogImpl 的边界剩下的模块放在一起看能看出一个共同的边界规律每个模块都紧贴一个系统服务并且只做那个服务的“可视化前端”。模块依赖的系统侧职责边界RecentsActivityManagerService 的 RecentTasks展示任务快照、处理滑动删除任务数据本身不归它管VolumeUIAudioService音量变化事件、音量条与设置面板的同步PowerUIBatteryService、PowerManager低电量弹窗、关机/重启确认流程RingtonePlayer通过 AIDL 暴露给系统铃声播放本身不持有播放列表PipUIPipManager画中画窗口的进入/退出动画与菜单BiometricDialogImplBiometricPrompt 框架指纹/人脸/虹膜的弹窗与错误提示这张表的用处在于定位问题一旦某个现象只在特定服务相关时出现直接去对应模块里找不用在几十个类里翻。反过来如果一个改动需要同时动两个模块的状态那多半是设计上出了问题——SystemUI 的模块划分本来就允许你只改一边。4. 通知管线一条通知从 NMS 到状态栏的完整链路通知是 SystemUI 里最复杂、也最容易被误改的一条链路。它涉及的模块最多NotificationListenerService的客户端、NotificationEntryManager的条目管理、PreparationCoordinator的视图准备、NotificationPresenterImpl的呈现、以及StackScrollLayout的布局动画。任何一个环节理解错表现都是“通知不显示”或者“通知重复显示”而这两种现象的原因往往完全相反。4.1 通知在系统中的位置与注册流程先纠正一个常见误解通知不是 SystemUI 创建的SystemUI 只是订阅者。权威数据在NotificationManagerServiceNMS应用调NotificationManager.notify()之后通知被写入 NMS 的NotificationRecord列表ranking优先级、是否横幅、是否静默也在 NMS 侧算完。SystemUI 的NotificationListener继承自NotificationListenerService向 NMS 注册自己注册成功后才开始收数据。注册流程分两步NotificationListenerService在onBind时把INotificationListener的 binder 交给 NMSNMS 记录这个监听器并授予相应的ManagedServices权限随后在onListenerConnected回调里SystemUI 才算真正“上线”。这个回调是整个链路里最关键的一次握手——因为全量通知只在这时候下发一次。4.2 从 SystemServer 到 SystemUIListener 绑定与 rank 计算握手之后的通信是增量的有新通知NMS 回调onNotificationPosted(sbn, rankingMap)有移除回调onNotificationRemoved。RankingMap里带着每个通知的NotificationRecord排序信息SystemUI 用它来决定这条通知该不该弹横幅、该不该进摘要组、该不该被静音。class NotificationListener extends NotificationListenerService { Override public void onListenerConnected() { // 连接瞬间的全量快照之后不会再有第二次 StatusBarNotification[] current getActiveNotifications(); mMainHandler.post(() - mEntryManager.onNotificationsInitialized(current)); } Override public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { // 注意这里是 Binder 线程直接碰 View 会崩 mMainHandler.post(() - mEntryManager.onNotificationPosted(sbn, rankingMap)); } Override public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, int reason) { mMainHandler.post(() - mEntryManager.onNotificationRemoved(sbn, rankingMap, reason)); } }reason这个参数值得单独说它区分了“用户划掉”、“应用取消”、“被其他通知替换”等场景直接影响移除动画走哪一条。如果你的改动把滑走动画改成了消失动画先看看这里是不是把这个参数丢了。rankingMap同理它不是装饰品横幅判定完全依赖它里面的getContactAffinity和重要度字段。4.3 SystemUI 内部的处理从 Entry 到视图拿到StatusBarNotification之后SystemUI 内部还要走三段。第一段是NotificationEntryManager它维护一个currentNotifications的快照负责去重、更新、以及跟NotificationListener的连接状态做对齐。第二段是PreparationCoordinator它把通知对象转换成视图——inflate 布局、绑定图标、异步加载大图这一步跑在单独的 HandlerThread 上因为加载大图是通知卡顿的主要来源。第三段是NotificationPresenterImpl它决定这条通知是进列表、进横幅、还是被折叠进摘要组。这三段之间是异步的所以会出现一个时序窗口通知数据已经进了EntryManager但视图还没准备好。这时候如果有另一个通知进来触发了重新排序列表就会短暂地缺一条。原生实现的做法是给每个 entry 一个InflationState在 inflate 完成前先占位避免布局跳动。自己改这块代码时如果跳过占位逻辑表现就是通知列表“抖一下”很少有人第一次就能想到是 inflate 异步导致的。另一处容易踩的是分组同一应用的多条通知会被 NMS 打上FLAG_GROUP_SUMMARYSystemUI 侧靠NotificationGroupManager判断当前该展开哪一组。这个判断依赖RankingMap里的 group key一旦你在中间层把sbn重建了一次而忘了带 group key分组立刻失效用户看到的就是一堆零散通知。5. 避坑清单SystemUI 改动后为什么最先崩的是通知和锁屏5.1 现象重启后通知全部消失新通知正常原因几乎总是同一个onListenerConnected里的全量快照没接住。NMS 只在连接建立那一刻下发getActiveNotifications()之后全是增量。如果你的改动把这段逻辑挪到了异步线程、或者在EntryManager初始化之前就调用了快照会被丢弃界面上就只剩新通知。解决方式是把快照请求挪到EntryManager就绪之后并且加一条兜底连接状态从断开恢复时主动再拉一次全量。这条兜底在原生的实现里是有的自己重写时最容易漏掉。5.2 现象状态栏偶尔卡住几秒然后 ANRSystemUI 的主线程同时承载状态栏、锁屏、通知列表的布局任何超过 5 秒的阻塞都会直接触发 ANR。常见的阻塞源是图标加载和PackageManager查询——每个通知都要拿应用图标如果每次都同步查一次PackageManager几十条通知就能把主线程堵死。解决思路是把这类查询移到BackgroundThread用缓存兜住重复查询。SystemUI 里本来就有PackageManager的缓存层改动时如果绕过了它直接查性能立刻掉一个量级。5.3 现象改了 Dependency 的注册顺序编译通过但一运行就 NPEDependency是一张静态 Map谁先注册谁先可用。某个组件在构造函数里注入StatusBarStateController但那个控制器还没注册注入结果是null等到第一次使用才崩堆栈还指在无关的地方。解决是两条注册顺序按依赖方向排好被依赖的先注册以及在Dependency.get()里加断言让失败发生在启动阶段而不是运行阶段。后者看起来多余实际能省掉大量排查时间。5.4 现象锁屏界面上滑之后状态栏消失了或者反过来这是窗口层级问题。锁屏和状态栏在同一个StatusBarWindowView里靠StatusBarState切换可见性。如果某次改动让两个状态同时为真视图树的alpha和translationY会打架表现就是一块空白。排查方法是dumpsys window看窗口可见性再看StatusBarStateController的当前状态值。多数情况下不是布局写错而是状态机进了非法组合。5.5 现象点击通知后没有跳转或者直接跳到了锁屏后面通知的点击靠PendingIntent而PendingIntent的身份是创建它的应用不是 SystemUI。SystemUI 只负责把contentIntent透传给ActivityStarter中间任何一次重建PendingIntent都会改变身份导致目标 Activity 起不来或者被安全策略拦截。锁屏场景更特殊ActivityStarter会检查目标 Activity 是否允许在锁屏上显示不允许的话会先要求解锁。如果你的改动绕过了这层检查表现就是应用内容直接暴露在锁屏上这是安全问题不是体验问题。6. 设计取舍为什么 SystemUI 选择了“重进程 弱耦合”这条路站在今天回看SystemUI 的架构其实做了一个相当激进的决定把所有系统级 UI 塞进一个进程用一套极轻的静态注入把它们粘起来。这个选择的反面是显而易见的——任何一个模块泄漏内存整个 SystemUI 都遭殃任何一次主线程阻塞状态栏和锁屏一起卡Dependency的隐式依赖让代码阅读成本很高新人接手经常找不到某个状态是哪来的。但它换来的东西同样明确。第一是状态一致性锁屏、状态栏、通知面板共享同一份StatusBarStateController动画可以做到帧级同步如果拆成多进程跨进程状态同步的延迟会直接体现在动画上。第二是启动速度单进程少一轮 binder 握手和进程创建开机后状态栏能早几百毫秒出现。第三是权限模型简单所有模块共享system身份不用为每个模块单独设计权限边界。真正值得学的是它对“耦合”的处理方式模块之间不直接持有对方引用只通过Dependency里的接口通信且每个模块的状态都尽量向系统服务对齐——UI 只是镜像权威在 NMS、在 WMS、在 AMS。这个原则让“改一个模块”的爆炸半径可控也是它能在单进程里活这么多年的根本原因。验证一个改动是否守住了这条线我一般会做三件事dumpsys看组件自述状态是否自洽用Systrace看主线程有没有超过 16ms 的块以及断网重启一次看通知快照和锁屏状态能不能自己恢复。这三步做完大部分“玄学”问题都会现形。我从踩过最狠的一次坑之后养成一个习惯动 SystemUI 的任何一块之前先问自己“这个状态的权威在哪个进程”。答案不在 SystemUI 里的一律只做渲染不做判断。希望帮到你。这份架构梳理的原始材料把模块划分、结构图、启动流程和通知链路放在了一起配合上面的机制拆解对着看会比单读代码快得多需要的话可以取一份留着当索引。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Visual Studio资讯订阅指南与技巧 1. 为什么需要订阅Visual Studio的独家资讯? 作为一名长期使用Visual Studio进行开发的程序员,我深刻体会到及时获取官方更新信息的重要性。Visual Studio作为微软旗下的旗舰级集成开发环境(IDE),其功能迭代速度之快、… · 2026/11/3 3:16:32
VSCode开发SpringBoot项目:轻量高效的全栈解决方案 1. 为什么选择VSCode开发SpringBoot项目?作为一名长期使用IntelliJ IDEA的Java开发者,我第一次尝试用VSCode开发SpringBoot项目时内心是充满怀疑的。但经过三个月的深度使用后,我发现这个轻量级编辑器在SpringBoot开发中展现出惊人的潜力。VS… · 2026/11/3 3:16:32
HDFS分布式存储原理与大数据应用实践 1. HDFS 的分布式设计哲学在2003年的一个普通工作日,Doug Cutting正在为他的开源网络爬虫项目Nutch的存储问题发愁。当时的单机存储系统已经无法满足日益增长的网页数据存储需求,这促使他开始思考如何构建一个能够跨多台机器工作的文件系统。这个想法最终… · 2026/11/3 3:16:32
2024年TensorFlow实战指南:从Keras到部署,对比PyTorch 打开PyPI的下载统计页,TensorFlow依然是整个深度学习生态里下载量最大的框架之一。但在很多新手群里,一提TensorFlow总有人觉得这是“老框架”,好像已经被PyTorch全面超越了。我自己的感觉是,这种印象有对的部分,也有很… · 2026/11/3 3:51:38
MNN部署YOLO人体关键点检测:C++工程CPU/GPU全流程实现 做部署这些年,我一直觉得“跑通模型”只是第一步,真正让人头疼的是把模型塞进C工程里,还要在CPU和GPU两种条件下都稳定工作。这个项目就是把YOLOv8-Pose、YOLO11-Pose、YOLO26-Pose人体关键点检测模型,用MNN部署到C环境࿰… · 2026/11/3 3:51:38
华为手机离线AI自主开发闭环实战 1. 项目本质与真实场景还原:一台“裸机”华为手机上的AI自主开发闭环你看到标题第一反应可能是:“这不就是用AI写个App再装上去?”——错了。这个项目真正令人头皮发麻的点,根本不在“写代码”,而在于整个开发-编译-安… · 2026/11/3 3:51:38
AI Agent技能管理器:基于SKILL.md与Redis的可视化操作系统 1. 这不是又一个“AI Agent 配置面板”,而是一套可落地的技能生命周期操作系统你有没有试过给 AI Agent 写了十几个工具函数——天气查询、数据库读写、邮件发送、PDF 解析、Excel 表格生成……结果两周后打开代码,连哪个函数叫什么、参数怎么传、返回值… · 2026/11/3 3:51:38
Kafka核心术语与架构设计深度解析 1. Kafka核心术语全景解析作为分布式消息系统的标杆,Kafka的设计哲学体现在其特有的术语体系中。初次接触Kafka时,这些术语就像密码本里的暗号,理解它们才能与Kafka进行有效对话。让我们从生产者-消费者的基础模型开始,逐步拆解每… · 2026/11/3 3:51:38
深度学习在食品图像分类中的应用与实践 1. 深度学习食品分类项目概述食品分类是计算机视觉领域一个极具实用价值的课题。我在实际项目中发现,利用深度学习技术来自动识别和分类食品图像,不仅能大幅提升效率,还能避免人工分类的主观性和疲劳误差。这个技术可以应用于餐饮管理、健康监… · 2026/11/3 3:50:37
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18