news 2026/10/2 3:29:09

ADB 自动化测试入门:环境搭建、高频命令、Python 封装与日志排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ADB 自动化测试入门:环境搭建、高频命令、Python 封装与日志排查

adb 这东西,说它简单是真简单,敲三条命令就能装应用、点屏幕、拉日志;说它麻烦也是真麻烦,环境没配对、设备没授权、好几台设备抢着连同一个端口,随便中一个都能让你在工位上耗掉一下午。我最早把 adb 用进日常测试,不是为了写什么高大上的框架,纯粹是被一轮又一轮的重复手工回归逼的——同一个流程,每次发版前手动跑几十遍,点到手酸还容易漏。后来用 adb 把"装包、启动、点击、截图、抓日志"这几件事串成脚本,才发现自动化测试的入口其实比想象中低得多,不需要先啃完一整套框架文档。

如果你现在的情况是:听过 adb、也复制过别人的命令,但真到自己要写一个能跑的自动化流程时不知道从哪下手,那这篇就是给你写的。我会从 adb 在自动化测试里的真实定位讲起,把环境搭建、高频命令、Python 封装、日志定位、以及我踩过的那些坑全部摊开说。读完你应该能独立写出一个"启动 App → 执行操作 → 断言结果 → 失败留证"的完整脚本,并且知道它什么时候够用、什么时候该换工具。

1. ADB 在自动化测试链路里到底站在哪一层

1.1 客户端、服务端、设备守护进程的三段式结构

adb 的全称是 Android Debug Bridge,直译"调试桥"。很多人把它当成一个单纯的命令行工具,实际上它是一个典型的三段式结构,拆开看是这样的:

  • adb client:你在终端敲adb xxx时临时启动的那个进程,负责把你的指令送出去。它自己不直接跟手机打交道,而是先去找本机的服务端,默认通过 5037 端口。
  • adb server:常驻后台的管理进程,维护设备列表、端口转发、连接复用。你敲的adb kill-server干掉的就是它。
  • adbd:跑在手机或模拟器系统内部的守护进程,真正执行 shell、读写文件、转发数据流的是它。

把这三段记住,最大的价值在于排错时能立刻缩小范围。比如adb devices死活不显示设备,你就该顺着链路问自己:是 client 找不到 server(5037 端口被别的程序占了),还是 server 连不上 adbd(USB 驱动没装、数据线只供电不传数据、设备端授权弹窗没点允许)。分清楚是哪一段断了,比漫无目的地重装工具包高效得多。

1.2 它和 Appium、UiAutomator 不是替代关系

新手最容易绕进去的一个问题:既然有 Appium 和 UiAutomator2 这种成熟的自动化框架,为什么还要学 adb?

我的理解是,它们解决的是不同层的问题。adb 更像是一把瑞士军刀,处在"设备控制层",负责把应用装上去、拉起来、退回桌面、切飞行模式、灌一条文本、按下返回键、把日志捞出来。而 Appium 这类框架处在"业务操作层",它负责用控件树而不是坐标去定位元素,让你的脚本在不同分辨率的机器上依然能跑。

关键点在于,Appium 在底层其实也在调 adb。它启动一个 UiAutomator2 的 instrumentation 服务、安装辅助 APK、把设备状态归零,这些动作全靠 adb 完成。所以你如果 adb 都不熟,用 Appium 时遇到设备掉线、应用装不上、端口被占这类问题,只能干瞪眼。反过来,纯 adb 脚本虽然"土",但在做设备巡检、批量装机、稳定性 Monkey、崩溃日志复现这些场景里,它反而是最轻最稳的选择。

我一般的判断标准是:流程固定、对控件属性依赖低、要跑在很多台设备上,优先用 adb 脚本;流程复杂、要频繁断言界面元素文本,再上框架。两者不冲突,混合用是常态。

2. 环境搭建:从装 adb 到设备真正连上

2.1 三种安装路径的选择逻辑

装 adb 这步看着简单,但选错方式后面会一直别扭。常见三条路:

方式适合谁优点代价
官方 platform-tools 压缩包想长期做自动化的人版本可控、纯绿色、可多版本共存需要手动配环境变量
系统包管理器安装Mac / Linux 用户图省事一句命令搞定版本可能偏旧
第三方一键安装器只想临时用一下图形化、不用配环境变量版本落后、路径不透明

我把话说直白点:只要打算写脚本,就下官方的 platform-tools 压缩包。解压到某个固定目录,比如D:\tools\platform-tools,然后把这个目录加进 PATH。这样做的好处是版本透明——出问题时你能明确知道自己用的是哪个版本,而不是被一个隐藏在某处的旧 adb 搞得怀疑人生。

Windows 上还容易撞一个坑:系统里同时存在多个 adb.exe,PATH 里排前面的那个是你几个月前装模拟器时带进来的旧版本,跟当前 adb server 版本不匹配,会报adb server version doesn't match this client。解决办法很简单,where adb把所有路径列出来,删掉或者调整顺序,然后adb kill-server再adb start-server。

2.2 真机和模拟器连上来的差异

真机连接的第一道门槛是驱动和授权。用数据线连上电脑后,手机通常会弹出"允许 USB 调试吗"的对话框。注意这里有个默认勾选项叫"一律允许使用这台计算机进行调试",如果你勾了,以后再插这台电脑就不会再弹框——这在共享测试机上是个隐患,别人拿走后你连不上,还得去撤销授权。

撤销入口在开发者选项里,一般是"撤销 USB 调试授权",点一下所有已授权电脑全部失效,重新插拔即可。

模拟器这边区别在于它通常有两种连接模式。一种是自带 adb 的集成模式,你在模拟器里操作等于直接操作宿主机 adb;另一种是独立端口模式,需要手动adb connect。后者的好处是端口固定、方便脚本管理,常见的默认端口大致是:夜神 62001、MuMu 7555、逍遥 21503 这个区间。端口号建议去模拟器设置里确认,别照着网上的老教程抄。

无线连接也是同理。先在 USB 连接状态下执行adb tcpip 5555把设备切到网络监听模式,再adb connect 设备IP:5555。这里要提醒一句:切换之前确保手机和电脑在同一个局域网,并且路由器没有开启客户端隔离,否则你会看到 connect 一直成功但 devices 列表里很快变成 offline。

2.3 连上之后别急着写脚本,先做三项验证

设备连上后,很多人直接开始写代码,结果脚本跑一半崩了才发现连接本身就有问题。我习惯先用三条命令把地基坐实:

adb devices -l adb shell getprop ro.product.model adb shell echo ok

第一条看设备是否真的在列表里且状态是device而不是unauthorized或offline。加-l是为了拿到设备型号和传输方式,多设备场景下这个信息很有用。第二条确认 shell 通道真的通了,能读出机型说明 adbd 正常工作。第三条是最朴素的连通性测试,返回ok就说明命令下发链路完整。

这三步花不了十秒,但能帮你提前挡掉后面一堆莫名其妙的报错。

3. 自动化脚本真正高频使用的 adb 命令

3.1 设备与包管理类命令

这类命令是脚本的"基础设施",每次跑用例前后的环境归零全靠它们。

# 列出设备,多设备时指定序列号操作 adb -s <serial> shell pm list packages | grep 关键词 # 查看应用安装路径,确认装的是哪个版本 adb shell pm path com.example.app # 覆盖安装,-r 保留数据,-t 允许测试包 adb install -r -t app-debug.apk # 卸载(保留数据用 -k) adb uninstall com.example.app # 清空应用数据,等价于"恢复出厂状态" adb shell pm clear com.example.app # 查看当前前台 Activity,判断 App 是否真的起来了 adb shell dumpsys window | grep mCurrentFocus

我最常用的是最后一条。启动应用后如果只靠 sleep 判断,脚本会因为机型性能差异时快时慢;而用dumpsys window抓当前聚焦窗口,就能做一个"等到目标 Activity 出现才继续"的可靠判断。这是把脚本从"能跑"提升到"稳定"的第一个分水岭。

3.2 输入与界面操作类命令

这是 adb 自动化的核心动作集,点击、滑动、输入、按键全在这。

# 点击坐标 adb shell input tap 540 1200 # 滑动:起点X 起点Y 终点X 终点Y 时长(ms) adb shell input swipe 540 1600 540 600 300 # 输入文本(注意:中文通常不支持,后面细说) adb shell input text "hello" # 按键:3=Home 4=返回 26=电源 187=最近任务 adb shell input keyevent 4 # 启动指定 Activity adb shell am start -n com.example.app/.MainActivity # 强制停止应用 adb shell am force-stop com.example.app

这里有个必须提前知道的限制:input text对中文和大部分非 ASCII 字符支持很差,直接传中文大概率变成一串乱码或者干脆无效。业界常见的绕法有几种——用 IME 辅助应用(比如 ADBKeyBoard)作为输入法,通过广播把文本塞进去;或者用 uiautomator 的 setText 接口;再或者干脆把中文输入这一步挪到 UI 框架里做。我一般推荐第一种,因为它不依赖额外框架,纯粹的 adb 广播就能搞定。

3.3 截图、录屏与文件传输

失败现场全靠这三样还原。

# 截图(二进制安全的方式,推荐) adb exec-out screencap -p > screen.png # 传统两步法,部分场景下才需要 adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png # 录屏,最长 180 秒 adb shell screenrecord --time-limit 20 /sdcard/demo.mp4 # 传文件到设备 adb push local.txt /sdcard/ # 从设备取文件 adb pull /sdcard/log.txt ./log.txt

exec-out和shell的区别值得说一下。早期的adb shell screencap -p > x.png在 Windows 上会把换行符做一次转换,导致图片损坏打不开,这是老教程里最常见的一个坑。exec-out不做终端转换,直接传原始字节流,所以现在我都用exec-out。

3.4 应用状态控制与权限授予

自动化测试经常需要跳过一些手动步骤,比如手工点授权弹窗、手工登录。这几条命令能省不少事。

# 授予权限,无需弹窗确认 adb shell pm grant com.example.app android.permission.ACCESS_FINE_LOCATION # 撤销权限,用来测"无权限"分支 adb shell pm revoke com.example.app android.permission.CAMERA # 关闭动画,加快脚本执行速度 adb shell settings put global window_animation_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global animator_duration_scale 0 # 查询屏幕分辨率和密度,做坐标换算的基础 adb shell wm size adb shell wm density

关闭动画这三条是我每次跑脚本前的固定动作。动画会让界面元素位置在短时间内持续变化,导致点击落空或者断言时机不准。关掉之后不仅脚本更稳,整体执行时间也能压缩一截,尤其是用例数量大的时候,收益非常明显。用完记得再用settings put global xxx 1恢复,别把测试机搞成"永远没有过渡动画"的奇怪状态。

4. 用 Python 把零散命令拼成可维护的脚本

4.1 为什么第一步先用 subprocess 而不是直接上框架

我见过太多人一上来就装 Appium,配一堆 capabilities,结果连"脚本为什么连不上设备"都答不上来。我的建议是:先写一个纯 adb 的 Python 脚本,把最基础的闭环跑通。

原因有三点。第一,纯 adb 脚本没有中间层,所有报错都是原生 adb 的报错,你被迫去理解每一个错误码意味着什么。第二,它足够轻,几十行代码就能跑起来,不需要理解 Driver 生命周期、不需要装辅助 APK,试错成本极低。第三,等你切到框架时,你对底层发生了什么是有概念的,遇到问题不会陷入"框架黑盒"的被动状态。

4.2 一个可以直接抄的 adb 封装

下面这个类是我在实际项目里反复用的简化版,核心就是把"执行命令、拿输出、超时控制"这三件事封装好。

import subprocess import time class Adb: def __init__(self, serial: str = None, timeout: int = 15): self.serial = serial self.timeout = timeout def _prefix(self): return ["adb", "-s", self.serial] if self.serial else ["adb"] def shell(self, cmd: str, timeout: int = None) -> str: full = self._prefix() + ["shell", cmd] try: out = subprocess.run( full, capture_output=True, text=True, timeout=timeout or self.timeout, encoding="utf-8", errors="replace", ) except subprocess.TimeoutExpired: raise RuntimeError(f"命令超时: {cmd}") if out.returncode != 0: raise RuntimeError(f"命令失败: {cmd}\n{out.stderr}") return out.stdout.strip() def tap(self, x: int, y: int): self.shell(f"input tap {x} {y}") def swipe(self, x1, y1, x2, y2, duration=300): self.shell(f"input swipe {x1} {y1} {x2} {y2} {duration}") def wait_activity(self, keyword: str, timeout: int = 20, interval: float = 0.5): deadline = time.time() + timeout while time.time() < deadline: focus = self.shell("dumpsys window | grep mCurrentFocus") if keyword in focus: return True time.sleep(interval) raise TimeoutError(f"等待 Activity 超时: {keyword}")

这段代码里有几个决定成败的细节。encoding="utf-8"和errors="replace"必须加,否则一旦设备输出里有非 UTF-8 字节,脚本直接抛 UnicodeDecodeError。timeout必须设,否则某个命令卡住会把你整个执行流程拖死。wait_activity里的轮询间隔不能太短,0.5 秒是个比较平衡的值,太快会白白占用 adb 通道,太慢则影响整体耗时。

4.3 用 uiautomator dump 拿到控件树

纯坐标点击最大的问题是不稳,界面稍微一变就点空。想拿到元素信息,可以用系统自带的 uiautomator dump。

adb shell uiautomator dump /sdcard/ui.xml adb pull /sdcard/ui.xml ./ui.xml

拿到 XML 后用 lxml 或 xml.etree 解析,就能按text、resource-id、class这些属性找节点,再取它的bounds属性算出中心坐标。

import re import xml.etree.ElementTree as ET def find_center_by_text(xml_path: str, text: str): tree = ET.parse(xml_path) for node in tree.iter("node"): if node.get("text") == text: bounds = node.get("bounds") # 形如 [x1,y1][x2,y2] nums = list(map(int, re.findall(r"\d+", bounds))) x1, y1, x2, y2 = nums return (x1 + x2) // 2, (y1 + y2) // 2 return None

提示:uiautomator dump对某些自定义绘制的界面(比如游戏、Flutter 渲染的页面)拿不到有效节点,这时候只能退回坐标方案,或者换用基于图像识别的思路。

这一步做完,你的脚本就从"写死坐标"升级成了"按文本找元素",跨分辨率移植的能力立刻上一个台阶。虽然还比不上框架的定位能力,但已经足够应付大部分业务场景了。

4.4 等待与重试是脚本稳定的命门

说个真实感受:新手脚本 90% 的失败不是因为逻辑写错,而是因为时机不对。点得太早、断言得太早、拉日志拉得太早,全是时间问题。

我的做法是三层等待。第一层是启动应用后等主界面 Activity;第二层是每次点击后等目标元素出现,用轮询代替 sleep;第三层是关键操作加重试,比如"点击提交后等结果列表,最多重试 3 次"。重试的时候记得在每次重试前截个图,事后排查会方便很多。

有个反直觉的点:重试次数不是越多越好。如果一个动作重试 5 次还失败,基本可以判断是环境或数据问题,继续重试只是在浪费时间。通常 2 到 3 次就是一个合理的上限。

5. 日志抓取与失败现场还原

5.1 logcat 的正确打开方式

自动化跑失败不可怕,可怕的是失败了不知道哪错了。logcat 就是你唯一能还原现场的工具。

# 先清空旧日志,保证抓到的是本次运行的 adb logcat -c # 按时间格式输出,只保留 Error 以上级别 adb logcat -v time *:E # 只抓某个进程的日志 adb logcat --pid=$(adb shell pidof -s com.example.app) # 输出到文件 adb logcat -v time > run.log

-c清空这一步经常被忽略。如果不清,脚本跑完你拿到的是几百兆的历史日志,翻起来非常痛苦。我一般会在脚本启动时就先清一次,这样跑完直接adb logcat -d把本次全部日志导出,干净利落。

5.2 从崩溃日志里筛出关键行

拿到日志后,直接搜FATAL EXCEPTION能定位到崩溃入口,往下的Caused by链就是根因。如果是 ANR,搜ANR in会看到大致的原因描述。

现象关键字大概率原因
应用闪退FATAL EXCEPTION空指针、资源找不到
界面无响应ANR in主线程阻塞、锁竞争
进程被杀lowmemorykiller / am_kill内存占用过高
权限报错SecurityException权限未授予
页面跳转异常ActivityNotFoundException组件名写错或未导出

这里有个经验:崩溃日志配合截图一起看,定位速度能快一倍。因为日志告诉你"崩在哪个类",截图告诉你"崩在哪个界面",两者一交叉,复现路径基本就清楚了。

5.3 让脚本自己保存现场

我在封装类里加了一个统一的失败处理逻辑:捕获异常时自动执行三条动作——截图、导出当前日志、导出当前界面 XML。三样东西存到以时间戳命名的目录里,跑完一轮用例直接翻目录就行,不用再去追着重跑。

import os import time import traceback def save_evidence(adb: Adb, tag: str): folder = os.path.join("evidence", f"{tag}_{int(time.time())}") os.makedirs(folder, exist_ok=True) # 截图 with open(os.path.join(folder, "screen.png"), "wb") as f: subprocess.run(["adb", "exec-out", "screencap", "-p"], stdout=f) # 界面树 adb.shell("uiautomator dump /sdcard/ui.xml") subprocess.run(["adb", "pull", "/sdcard/ui.xml", os.path.join(folder, "ui.xml")]) # 日志 with open(os.path.join(folder, "logcat.txt"), "w", encoding="utf-8") as f: subprocess.run(["adb", "logcat", "-d", "-v", "time"], stdout=f, text=True)

这三样东西加起来占用不了多少空间,但能帮你省掉大量"重跑一次看看"的时间。特别是那种偶发崩溃,重跑可能就不复现了,现场证据是唯一能救命的东西。

6. 那些让人卡半天的报错,我是这么排查的

6.1 adb unauthorized 的完整排查链路

unauthorized大概是出现频率最高的一个状态。它意味着电脑已经能看到设备,但设备端没有认可这台电脑的调试请求。按下面的顺序走,基本都能解决:

  1. 看手机屏幕上有没有授权弹窗,有就点"允许"。如果屏幕锁着,弹窗可能被拦截,解锁再看。
  2. 弹窗不见了但状态还是 unauthorized,进开发者选项点"撤销 USB 调试授权",然后重新插拔数据线。
  3. 换了数据线还是不行,考虑换个 USB 接口,优先用主板直出的口,别用前面板或者扩展坞。
  4. 还不行就重启 adb 服务:adb kill-server然后adb start-server,再adb devices看一次。
  5. 最后一步是检查 adbd 是否被设备策略限制,部分定制系统对调试通道管得比较严,需要在开发者选项里额外打开"USB 调试(安全设置)"之类开关,具体名称各家不同。

注意:有些设备的授权状态是跟数据线绑定的,换线之后要重新授权一次,这不是故障,是设计如此。

6.2 多设备场景下的命令错发

连着手机又开着模拟器时,如果不指定序列号,adb 会报more than one device,或者更糟——直接把命令发到了错误的目标上,脚本表现出一堆莫名其妙的错误。

解决办法是强制指定序列号,把序列号做成配置项而不是硬编码。在封装类里我用了-s serial的形式,初始化时传进去,所有命令自动带上。设备序列号用adb devices就能看到,模拟器的序列号一般是127.0.0.1:端口这种形式。

还有个隐蔽的坑:设备可能因为休眠或者网络抖动从列表里消失,脚本跑到一半命令全部报错。所以我在关键步骤前加了一个ensure_online检查,发现设备掉线就尝试重连一次,重连不上就明确报错退出,而不是让脚本带着一堆红色报错继续往下跑。

6.3 坐标在不同分辨率上的漂移

用模拟器写好坐标点击,换到真机上全部点空,这是坐标方案的天花板问题。常见的处理方式有两种:

一种是按比例换算。先用wm size拿到当前分辨率,跟设计基准分辨率做比例运算,把基准坐标映射到当前设备。

def scale_point(x, y, base=(1080, 1920), current=(1080, 2340)): sx = current[0] / base[0] sy = current[1] / base[1] return int(x * sx), int(y * sy)

另一种是彻底放弃坐标,改用控件定位(也就是前面说的 dump XML 方案)。我的实际选择是:能拿到控件就用控件,拿不到才退回坐标,并且坐标方案只用于那些界面结构极其固定的页面。

顺便说下横竖屏的问题。滑动方向在横屏下会完全反过来,脚本里最好根据dumpsys input或者屏幕尺寸判断一下当前方向,否则滑动操作会南辕北辙。

6.4 应用被系统冻结或后台被杀

这个坑比较隐蔽:脚本跑得好好的,切到某个页面后应用突然回到登录页,或者干脆进程没了。原因通常是后台清理机制。

排查方向有几个。先看dumpsys meminfo 包名里应用的内存情况,如果接近系统阈值,被回收的概率很高。再看电池优化设置,很多系统会主动限制后台应用。测试机上可以手动把应用加进"不受限制"的名单,脚本里也可以尝试通过am set-inactive之类的接口调整应用状态。

另外提一句应用冻结的场景:有些测试需要临时"冻结"某个应用来验证降级逻辑,本质上就是把应用置为禁用状态,pm disable和pm enable一对命令就能做到。用完记得恢复,否则下次跑用例时那个应用直接打不开,你会发现得莫名其妙。

7. adb 脚本和自动化框架的边界在哪

7.1 adb 脚本擅长的四类活

用了一段时间之后,我总结出 adb 脚本最舒服的四个场景:

  • 设备巡检:批量连上多台设备,跑一遍开机自检、检查关键应用是否存在、拉一遍基础信息。这种任务用框架纯属杀鸡用牛刀。
  • 批量安装与卸载:发版前在多台设备上装同一个包,验证安装成功率。
  • 稳定性压测:用adb shell monkey跑几千次随机事件,观察崩溃率和内存增长。
  • 崩溃复现与日志采集:用户报了一个崩溃,用固定操作路径快速复现并抓日志。

这四类活的共同点是:操作路径相对固定、对界面文本断言要求不高、要跑在设备层面而不是业务层面。用 adb 脚本做,代码量少、依赖轻、排查直观。

7.2 什么时候必须换框架

反过来,下面这些情况继续用 adb 硬扛就很不划算了:

  • 需要断言的元素很多,而且分布在动态列表里。纯 adb 每次都要 dump XML 再解析,代码会变得很臃肿。
  • 需要跨设备跑同一套用例,且设备分辨率、系统版本差异大。框架的定位机制能省掉大量适配工作。
  • 需要生成结构化测试报告、需要跟持续集成流水线打通。框架生态里这些是现成的。
  • 涉及 WebView 或者混合页面。纯 adb 对这类页面的控制力非常有限。

我的建议是给 adb 脚本划一个明确的定位:它是自动化测试的底座和补充手段,不是完整的测试方案。底座意思是,即使你用的是框架,设备管理、日志采集、失败留证这些周边能力还是靠 adb 更直接。补充手段意思是,遇到框架搞不定的边角场景(比如系统弹窗、状态栏操作、跨应用跳转),adb 往往能一把梭解决。

7.3 面试里被问到的几个点

如果你在准备自动化测试相关的问题,adb 这块高频被问的其实是这几种:

第一种是"设备连不上怎么排查",标准答案不应该是背命令,而是顺着 client-server-adbd 的链路一层层往下定位,体现的是排查思路而不是记忆量。第二种是"自动化中 adb 承担什么角色",答设备控制层和框架底层依赖,比答"用来跑命令"要有深度得多。第三种是"坐标点击怎么适配不同分辨率",答比例换算和控件定位两条路线,并说明各自适用场景。

这几道题的核心都不是命令本身,而是你能不能说清楚"为什么这么设计"。这也回到我一直强调的那点:工具是拿来用的,但用之前得知道它为什么长这样。

我自己跑这套东西最有价值的一次经历,是把一个需要手工点二十多分钟的回归流程压成了四十多秒的脚本,并且每天早上定时跑一遍,出问题直接在群里发截图和日志。真正省下来的不是那二十分钟,而是"每次发版前必须有人坐在那儿点"这件事本身的消失。如果你也正被类似的重复劳动困住,从最简单的三条命令开始,先让脚本跑起来,再一点点往上加能力,路子会越走越顺。

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

海信IP501H机顶盒U盘刷机全攻略:从固件选择到变砖自救

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

作者头像 李华
网站建设 2026/10/2 3:28:34

Windows关机原理与实战优化:从优雅退出到硬关机全解析

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

作者头像 李华
网站建设 2026/10/2 3:27:18

更弱智的算法学习:暴力枚举、剪枝与排序算法复盘

“更弱智的算法学习”这个名字听起来像是在自嘲&#xff0c;但今天是我坚持算法学习的第32天&#xff0c;我反而觉得这个“弱智”标签挺真实、也挺有用。朋友圈打卡时候随手起的标题&#xff0c;没想到成了我三十多天里最好的心理建设工具&#xff1a;不强求一次看懂所有高深理…

作者头像 李华
网站建设 2026/10/2 3:27:14

Arch Linux 移动硬盘安装指南:UEFI 启动与 GRUB 配置实战

1. 为什么要把 Arch Linux 装进移动硬盘——不是“能不能”&#xff0c;而是“值不值得”Arch Linux 装进移动硬盘&#xff0c;这事听起来像极了老司机在茶水间随口一提的骚操作&#xff1a;用一块几百块的 USB 3.2 Gen2 移动固态硬盘&#xff08;比如三星 T7 Shield、闪迪 E61…

作者头像 李华
网站建设 2026/10/2 3:27:09

自然连接与等值连接的区别:从原理到SQL实操

说实话&#xff0c;只要写SQL&#xff0c;就躲不开连接。自然连接和等值连接这两个词我第一次听的时候&#xff0c;还以为是一个东西有两个名字。后来在业务里写JOIN踩了坑&#xff0c;回去翻《数据库系统概论》&#xff0c;才发现教科书里的一句话&#xff0c;放到真实数据上能…

作者头像 李华