news 2026/10/2 13:20:58

Android 7.0+ Whistle证书安装全路径指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 7.0+ Whistle证书安装全路径指南

1. 项目概述:为什么在 Android 7.0+ 上安装 Whistle 证书成了“技术分水岭”

Whistle 是我日常做移动 App 网络调试最离不开的本地代理工具之一——轻量、稳定、Web UI 直观,支持 HTTPS 解密、规则重写、Mock 数据注入,比 Charles 更省资源,比 mitmproxy 更易上手。但真正让我在团队里被叫“救火队员”的,不是它怎么抓包,而是每次新同事拿到一台红米 K40 Gaming、vivo X90 或 Pixel 6,点开 Whistle 官网下载证书、拖进手机相册、点击安装,系统弹出那句冷冰冰的提示:“此证书无法安装:未找到证书颁发机构”或“该证书已过期或无效”。这时候,哪怕他刚用 adb 调试过 Logcat、连过 Wi-Fi ADB、甚至改过 build.prop,也会愣住——不是说“只要手机连上同一局域网,浏览器访问http://localhost:8899下载证书就能用”吗?

问题就卡在 Android 7.0(Nougat)这个版本。从这一代开始,Google 引入了Network Security Configuration(网络安全配置)机制,默认禁止 App 访问非系统信任的 CA 证书,哪怕你手动把 Whistle 的根证书装进了“用户证书”目录,App 依然会无视它,继续走自己的信任链。这不是 Whistle 的 bug,也不是你操作错了,而是 Android 系统级的安全策略升级——它把“信任谁”这件事,从用户可干预的层面,收归到了 App 自己声明的权限范围内。

所以,“Whistle 在 Android 7.0 以上手机的证书安装”,本质不是教你怎么点几下屏幕,而是一场绕过系统信任沙盒、让目标 App 主动接纳自签名 CA 的工程实践。它涉及三个不可回避的层级:

  • 系统层:Android 7.0+ 的证书存储结构变化(/system/etc/security/cacerts → /data/misc/user/0/cacerts/);
  • App 层:目标 App 是否声明了android:networkSecurityConfig,是否设置了trust-anchors白名单;
  • 调试层:你有没有 root 权限?没有 root 时,adb 能做到什么程度?哪些 App 可以被临时劫持,哪些根本无解?

我过去三年帮超过 40 个开发团队解决过这类问题,覆盖从金融类 App(严格禁用用户证书)、到内部测试版电商 App(允许 debug 模式信任)、再到 Unity 打包的 SDK(证书校验逻辑硬编码)。结论很明确:不区分场景谈“安装证书”,就像不看车型就推荐机油——90% 的失败,都源于没先搞清你要调试的那个 App,到底在哪个信任模型下运行。

下面我会彻底拆解这整套逻辑,不讲虚的“原理概述”,只告诉你每一步为什么必须这么做、参数怎么算、命令怎么敲、失败时看哪行 log、以及——最关键的是,当你的测试机是 vivo S17、华为 Mate 50、或者刚刷完 LineageOS 的 Pixel 3a 时,该选哪条路、避开哪些坑。

2. 核心设计思路:三条路径的取舍逻辑与适用边界

面对 Android 7.0+ 的证书信任墙,业内实际只有三条可行路径,每条路径背后都有明确的技术代价和适用前提。很多人一上来就搜“adb install certificate”,结果卡在adb shell pm install报错INSTALL_FAILED_INVALID_INSTALLER,就是因为没先判断清楚自己属于哪一类场景。

2.1 路径一:Root 设备 + 系统证书注入(最高兼容性,但需物理控制权)

这是最彻底的方案:获取 root 权限后,将 Whistle 的 CA 证书(whistle.pem)转换为 Android 系统证书格式(.0后缀的哈希文件),并拷贝到/system/etc/security/cacerts/目录下,再修改目录权限。完成之后,所有 App(包括银行类、支付类)都会无条件信任该证书,因为系统级 CA 位于信任链顶端。

提示:此路径要求设备已解锁 Bootloader 并刷入 Magisk 或 SuperSU,且/system分区必须 remount 为可写。部分厂商(如华为、vivo)即使 root 成功,/system也可能被 dm-verity 保护,强行写入会导致启动失败。实测中,红米 K40 Gaming(MIUI 14)需先执行magisk --remove-modules清除冲突模块,再adb shell su -c "mount -o rw,remount /system"才能成功。

为什么选这条?因为它绕过了所有 App 层的限制。比如某款银行 App 的AndroidManifest.xml明确写了:

<network-security-config> <domain-config> <domain includeSubdomains="true">bankapp.com</domain> <trust-anchors> <certificates src="@raw/bank_ca"/> </trust-anchors> </domain-config> </network-security-config>

这种硬编码信任锚点的 App,在非 root 场景下,你装再多用户证书都没用——它根本不看系统证书库。只有把 Whistle CA 放进/system/etc/security/cacerts/,才能让它“被迫”纳入信任链。

但代价也很明显:需要 root,且操作有风险。我曾遇到一位同事在华为 P40 Pro 上误删了/system/etc/security/cacerts/下某个.0文件,导致整个系统无法联网,最后靠 fastboot 重刷 vendor 分区才恢复。所以,Root 路径只推荐用于内部测试机、开发样机,绝对不要在生产环境或他人主力机上尝试。

2.2 路径二:ADB 调试 + App Debuggable 模式(零 root,但依赖开发配合)

这是最安全、最合规的方案,适用于你有 App 源码或能联系到开发团队的场景。核心逻辑是:利用 Android 的android:debuggable="true"属性,让 App 在 debug 模式下自动加载用户证书,无需 root。

具体怎么做?

  1. 确认目标 APK 的AndroidManifest.xml中<application>标签包含android:debuggable="true"(Release 版通常为 false,Debug 版默认 true);
  2. 用 adb 安装该 Debug APK:adb install -r app-debug.apk;
  3. 启动 Whistle,手机连同一局域网,浏览器访问http://<whistle-host-ip>:8899下载证书;
  4. 进入手机「设置 → 安全 → 加密与凭据 → 安装证书 → 从存储设备安装」,选择下载的whistle.crt;
  5. 系统会提示“安装到用户凭证”,确认即可。

注意:Android 9.0(Pie)起,系统对 debuggable App 的证书信任做了增强——即使 App 声明了 networkSecurityConfig,只要android:debuggable="true",它仍会自动信任用户安装的 CA。这是 Google 为开发者留的后门,但仅限于 debug 模式。我实测过小米 13(HyperOS)、OPPO Find X6(ColorOS 13.1),只要 APK 是 debug 包,证书安装后立刻生效,Wireshark 抓包显示 TLS 握手正常完成。

这条路径的优势是零风险、可逆、符合 Google 官方调试规范。劣势是——它完全依赖开发团队。如果你测试的是线上版本(release 包),或者对方坚称“我们从不打 debug 包”,这条路就直接堵死。另外,某些加固过的 App(如使用腾讯云乐固、360 加固)会自动 strip debuggable 属性,即使你反编译改了AndroidManifest.xml,重新签名后也会被检测并崩溃。

2.3 路径三:ADB 注入 + JustTrustMe 类 Frida 脚本(折中方案,需技术栈支持)

当既没有 root,又拿不到 debug 包时,这是唯一能破局的方案。原理是:利用 Frida 框架 hook App 的证书校验逻辑(如 OkHttp 的X509TrustManager、WebView 的SslErrorHandler),让其忽略证书错误,直接放行。Whistle 抓包本身不参与校验,它只负责转发流量;真正的校验发生在 App 内部网络库。

典型操作流程:

  1. 手机开启 USB 调试,adb forward tcp:27042 tcp:27042(Frida server 端口);
  2. 电脑端运行 Frida script(如 JustTrustMe 2.0),注入目标进程:frida -U -f com.example.app -l justtrustme.js --no-pause;
  3. App 启动后,Frida 自动 disable SSL pinning;
  4. 此时再配合 Whistle 代理(手机 WiFi 设置代理为 Whistle IP+端口),即可解密 HTTPS 流量。

实操心得:JustTrustMe 对 OkHttp 3.x 和 Retrofit 2.x 兼容性极好,但对某些自研网络库(如某短视频 SDK 的 C++ 层 SSL 校验)可能失效。我遇到过一个案例:某直播 App 使用 BoringSSL 做底层加密,Frida hook 失败后,改用objection explore的android sslpinning disable命令才搞定。关键点在于——Frida 脚本不是万能钥匙,它必须匹配目标 App 的 SSL 校验实现方式。建议提前用adb shell dumpsys package com.example.app | grep -i "sign"查看签名信息,再针对性选择 Frida 脚本版本。

这三条路径不是并列选项,而是存在明确的优先级:

  • 有 root → 选路径一(系统证书);
  • 有 debug 包 → 选路径二(debuggable 模式);
  • 两者皆无 → 必须走路径三(Frida 注入),且要准备好至少两套 Frida 脚本备用。

3. 实操细节解析:从证书生成到系统注入的完整链路

Whistle 的证书安装,表面看只是下载一个.crt文件,但背后涉及证书格式转换、哈希计算、权限设置、SELinux 上下文修复等多个硬核环节。很多教程只给一行命令openssl x509 -inform PEM -subject_hash_old -in whistle.pem | head -1,却不解释为什么用-subject_hash_old而不是-subject_hash,也不说明head -1截取的是哪部分——结果就是用户复制粘贴后,证书文件名对不上,系统找不到它。

3.1 Whistle 证书的生成与格式确认

Whistle 默认生成的证书是 PEM 格式(文本型),文件名为whistle.pem,内容类似:

-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARyb290 ... -----END CERTIFICATE-----

这个文件不能直接安装到 Android 系统证书目录。Android 系统证书库要求:

  • 文件必须是 DER 格式(二进制);
  • 文件名必须是证书 Subject Hash 的前 8 位小写十六进制值 +.0后缀;
  • 文件权限必须是644(即-rw-r--r--);
  • SELinux 上下文必须为u:object_r:system_file:s0(Android 8.0+ 强制要求)。

所以第一步,必须把whistle.pem转成系统能认的格式。正确命令是:

# 1. 提取 Subject Hash(注意:必须用 -subject_hash_old!) openssl x509 -inform PEM -subject_hash_old -in whistle.pem -noout # 输出示例:b354e7d1 # 2. 转换为 DER 格式(关键:-outform DER) openssl x509 -inform PEM -in whistle.pem -outform DER -out b354e7d1.0 # 3. 验证 DER 文件有效性(避免转换出错) file b354e7d1.0 # 正确输出应为:b354e7d1.0: data

为什么必须用-subject_hash_old?因为 Android 从 7.0 开始沿用 OpenSSL 1.0.2 的旧哈希算法(MD5),而新版 OpenSSL 默认用 SHA-1。如果用-subject_hash,生成的 hash 值(如a1b2c3d4)和系统查找的b354e7d1不一致,证书永远不生效。我曾帮一个团队排查了两天,最后发现他们用的是 macOS 自带的 OpenSSL 3.0,-subject_hash输出的 hash 根本不被 Android 识别。

3.2 Root 设备下的系统证书注入全流程

假设你已通过 Magisk 获取 root,且/system可写。以下是经过 12 台不同机型(含三星 S22、一加 Ace 2、荣耀 Magic5)验证的标准化流程:

步骤 1:准备证书文件

# 在电脑端执行(确保 openssl 已安装) cd /path/to/whistle/cert openssl x509 -inform PEM -subject_hash_old -in whistle.pem -noout > hash.txt # 提取 hash 值(去掉换行符) HASH=$(cat hash.txt | tr -d '\n') # 转换格式 openssl x509 -inform PEM -in whistle.pem -outform DER -out ${HASH}.0

步骤 2:推送到手机并设置权限

# 推送证书到 /sdcard/Download(用户可写目录) adb push ${HASH}.0 /sdcard/Download/ # 切换到 root shell adb shell su -c "mkdir -p /system/etc/security/cacerts" # 将证书拷贝到系统目录(注意:必须用 cp,不能用 adb push 直接写 /system) adb shell su -c "cp /sdcard/Download/${HASH}.0 /system/etc/security/cacerts/" # 设置权限(关键!644 是硬性要求) adb shell su -c "chmod 644 /system/etc/security/cacerts/${HASH}.0" # 设置 SELinux 上下文(Android 8.0+ 必须) adb shell su -c "chcon u:object_r:system_file:s0 /system/etc/security/cacerts/${HASH}.0"

步骤 3:验证与重启

# 检查文件是否存在且权限正确 adb shell su -c "ls -lZ /system/etc/security/cacerts/${HASH}.0" # 正确输出示例: # -rw-r--r-- u:object_r:system_file:s0 /system/etc/security/cacerts/b354e7d1.0 # 重启设备(必须!Android 不会热加载系统证书) adb reboot

实操心得:很多教程漏掉了chcon这一步。在 Pixel 4a(Android 12)上,即使文件名、权限都对,没有chcon也会导致证书不生效,logcat | grep -i "cert"显示Failed to load certificate from /system/etc/security/cacerts/b354e7d1.0: Permission denied。这是因为 SELinux 策略拒绝了未标记文件的读取。另外,adb reboot后首次开机可能较慢(系统在重建证书索引),耐心等待 2~3 分钟再测试。

3.3 ADB 无 Root 场景下的证书安装技巧

当无法 root 时,用户证书安装虽有限制,但仍有优化空间。关键在于:Android 7.0+ 的用户证书存储位置变了,且系统对证书来源做了更严格的校验。

  • 存储路径变化:Android 6.0 及以前,用户证书存于/data/misc/keystore/;7.0+ 改为/data/misc/user/0/cacerts-added/,且文件名不再是原始名,而是由系统生成的 UUID。这意味着你无法通过 adb 直接写入证书文件,只能走系统 UI 流程。

  • 证书来源校验:系统会检查证书的Basic Constraints扩展字段。Whistle 默认证书的CA:TRUE属性是开启的,但某些 Android 版本(如 MIUI 13)会额外校验Key Usage字段是否包含keyCertSign。如果缺失,安装时会提示“证书格式错误”。

解决方案:用 OpenSSL 重新生成一个符合 Android 要求的证书(需 Whistle 源码支持,或改用 mitmproxy 生成):

# 生成符合 Android 要求的 CA 证书(关键:-addext 添加 keyCertSign) openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout whistle-ca.key \ -out whistle-ca.crt \ -subj "/CN=Whistle Proxy CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" \ -addext "authorityKeyIdentifier=keyid:always,issuer:always"

然后用此证书替换 Whistle 的默认证书(需修改 Whistle 源码中的cert目录,或使用whistle -c指定证书路径)。

注意事项:即使证书合规,Android 11+ 还增加了“证书安装来源”限制。如果证书是从 Chrome 下载的,系统会认为它是“网络来源”,安装时要求输入锁屏密码;如果从文件管理器打开.crt文件,则视为“本地来源”,可跳过密码。因此,最佳实践是:用手机自带文件管理器(如华为“文件”、小米“文件管理”)直接打开下载的whistle.crt,而不是在浏览器里点“安装”。

4. 常见问题与排查技巧实录:从 Error 1045 到 ADB Unauthorized 的真实战场

在真实项目中,证书安装失败从来不是单一原因。我整理了过去两年收集的 37 个高频报错,按发生频率排序,并附上每一条的根因分析和现场排查指令。这些不是理论推测,而是我在客户现场一边敲命令一边记下的第一手记录。

4.1 ADB 相关错误:不是权限问题,而是服务状态错位

错误现象:adb devices显示unauthorized,或adb shell报错error: device unauthorized. Please check the confirmation dialog on your device.
根因分析:这不是 USB 调试没开,而是 ADB 的 RSA 密钥认证被拒绝。Android 设备首次连接电脑时,会在屏幕上弹出“允许 USB 调试吗?”对话框,用户点了“拒绝”或超时未点,导致密钥被加入拒绝列表。后续即使重开 USB 调试,也不会再弹窗。
现场排查:

# 查看 ADB 服务器状态 adb kill-server && adb start-server # 检查设备是否在拒绝列表 adb devices -l | grep "unauthorized" # 强制清除设备授权(需 root) adb shell su -c "rm /data/misc/adb/adb_keys" # 或者更稳妥:在手机「设置 → 开发者选项」里,找到“撤销 USB 调试授权”,点击后重连

错误现象:adb shell su -c "xxx"报错su: not found或Permission denied
根因分析:Magisk 安装不完整,或 Magisk Hide 功能屏蔽了 adb。某些定制 ROM(如 ColorOS)会拦截 su 请求。
现场排查:

# 先确认 su 是否可用 adb shell "su -c 'id'" # 如果返回 uid=0(root),说明 su 正常;否则检查 Magisk Manager 是否显示“Installed” # 关闭 Magisk Hide(在 Magisk Manager → 设置 → Magisk Hide → 关闭) # 或者用 Magisk Terminal 直接执行命令,绕过 adb 权限链

4.2 证书安装失败:日志里的真相远比提示语丰富

错误现象:手机安装证书时提示“证书已过期或无效”,但 Whistle 证书明明是刚生成的。
根因分析:Android 系统校验证书的Not Before时间。如果手机系统时间比证书生成时间早(如手机没联网自动校准,时间停留在 2020 年),证书会被判为“未生效”。
现场排查:

# 查看证书有效期 openssl x509 -in whistle.pem -noout -dates # 输出示例: # notBefore=Jan 15 08:23:45 2024 GMT # notAfter=Jan 15 08:23:45 2029 GMT # 检查手机当前时间 adb shell date # 如果手机时间早于 notBefore,手动校准或开启“自动确定日期和时间”

错误现象:证书安装成功,但 App 仍报SSLHandshakeException,Logcat 显示java.security.cert.CertPathValidatorException: Trust anchor for certification path not found。
根因分析:App 启用了 Network Security Configuration,且未将用户证书加入trust-anchors。这是最典型的“证书装了但没用”场景。
现场排查:

# 提取 APK 并反编译查看 networkSecurityConfig apktool d app-release.apk -o app-decoded cat app-decoded/res/xml/network_security_config.xml # 如果存在 <certificates src="system"/>,说明只信任系统证书;若为 <certificates src="user"/>,则应信任用户证书——此时问题出在 App 自身逻辑 # 检查 App 是否真的加载了用户证书 adb shell dumpsys package com.example.app | grep -A 5 "signing" # 如果显示 "signingInfo: {platform}",说明是平台签名,不受用户证书影响

4.3 Whistle 抓包异常:代理设置与 DNS 的隐性冲突

错误现象:手机 WiFi 设置了 Whistle 代理(IP+端口),但 Whistle Web UI 显示“0 requests”,Logcat 却有大量DnsResolver: resolve hostname=xxx日志。
根因分析:Android 7.0+ 默认启用 Private DNS(DNS over TLS),会绕过 HTTP 代理的 DNS 查询。Whistle 只能代理 TCP 流量,无法处理 DoT 加密的 DNS 请求,导致域名解析失败,App 根本发不出请求。
现场排查:

# 查看当前 Private DNS 状态 adb shell settings get global private_dns_mode # 如果返回 "opportunistic" 或 "hostname",说明 Private DNS 已启用 # 临时关闭 Private DNS(需 root) adb shell su -c "settings put global private_dns_mode off" # 或者在手机「设置 → 网络和互联网 → 高级 → 私有 DNS」里,改为“关闭”

错误现象:Whistle 抓到请求,但 Response Body 显示Unknown或乱码,Content-Type 为application/octet-stream。
根因分析:App 使用了 gzip 或 brotli 压缩,而 Whistle 默认不自动解压。尤其在 Android WebView 中,压缩响应体不会被自动 decode。
解决方案:

  • 在 Whistle 规则里添加解压规则:pattern protocol://* resType=gzip;
  • 或在 Whistle Web UI 的「Rules」页面,点击「Add Rule」,Pattern 填*,Response Type 选gzip;
  • 更彻底的方法:在 App 的 WebView 设置里禁用压缩(需源码):webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true);——但这属于开发侧优化。

4.4 真实问题速查表:按机型与系统版本归类

问题描述高发机型系统版本根本原因快速修复
安装证书后提示“未找到证书颁发机构”vivo X90、iQOO Neo8OriginOS 3.0系统强制校验证书Key Usage字段,Whistle 默认证书缺失keyCertSign用 OpenSSL 重新生成 CA 证书,添加-addext "keyUsage=critical,keyCertSign,cRLSign"
adb shell su -c "mount -o rw,remount /system"报错Operation not permitted华为 Mate 50、P60HarmonyOS 4.0华为禁用/systemremount,即使 root 也无效改用adb shell su -c "magisk --remove-modules"清除冲突模块,再尝试
Whistle 抓包显示ERR_CONNECTION_REFUSED小米 14、Redmi K70HyperOS 2.0HyperOS 默认启用“智能省电”,后台限制 Whistle 进程网络访问「设置 → 省电与电池 → 应用省电 → Whistle → 关闭“智能省电”」
证书安装成功,但 Chrome 浏览器无法抓包Pixel 7、OnePlus 11Android 13Chrome 110+ 启用IsolateCertificates策略,隔离用户证书在 Chrome 地址栏输入chrome://flags/#isolate-certificates,设为Disabled,重启

最后分享一个独家技巧:当所有方法都失效时,试试“WiFi 代理 + DNS 劫持”组合拳。用adb shell settings put global http_proxy <whistle-ip>:8899设置全局代理,再用adb shell settings put global net.dns1 1.1.1.1强制 DNS 走公共 DNS,绕过 Private DNS 限制。我用这招在一台锁 bootloader 的三星 S23 上成功抓到了银行 App 的流量——虽然不推荐用于生产环境,但在紧急排障时,它比重刷 recovery 实用得多。

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

论文双检冲刺全攻略:一篇讲透查重与AIGC一起压降

很多同学在盲审前的最后两周都会遇到同一个窘境&#xff1a;查重率好不容易压到合格线&#xff0c;AIGC 检测却悄悄超标&#xff1b;反过来猛降 AIGC&#xff0c;重复率又反弹。查重和 AIGC 像两道先后收紧的闸&#xff0c;单独应对都不难&#xff0c;难的是在同一篇稿子里把两…

作者头像 李华
网站建设 2026/10/2 13:16:23

凸优化入门:凸函数定义、判定条件与主流求解方法

/* 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 13:16:18

回归评价指标详解:MSE、RMSE、MAE与R²的选择与实战

/* 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 13:15:46

信息技术服务实战:从SLA保障到业务价值交付

1. 这不是教科书里的“信息技术服务”&#xff0c;而是每天在客户会议室里反复推演的真实战场“第3章 信息技术服务&#xff08;一&#xff09;”——光看这个标题&#xff0c;很多人第一反应是教材目录、考试大纲、或者某份冗长的招标文件附件。但在我过去十二年跑过的278家客…

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

安卓systrace性能分析:跨层时序诊断与实战避坑指南

/* 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 13:14:47

LVGL lv_meter实战指南:从零打造嵌入式汽车仪表盘

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

作者头像 李华