Android游戏逆向重构实战:从植物大战僵尸源码2到可运行工程
简介本资源为《植物大战僵尸》Android平台开源实现的完整工程源码面向Android游戏开发初学者与进阶者聚焦塔防类游戏架构设计、图形渲染与状态管理等核心实践。压缩包共173个文件含20个Java源文件涵盖GameScene、Tower、Npc等关键逻辑模块、120个PNG与13个JPG图像资源用于植物、僵尸及UI素材、7个TMX地图文件支撑关卡布局、4个XML配置与2个PLIST文件管理场景数据与属性整体仅2.78MB轻量易读。已有1376人学习下载适合通过真实项目理解Android游戏生命周期、资源加载机制、模块化分层设计逻辑层/视图层/数据层及cocos2d-android框架集成方式。源码结构清晰含完整AndroidManifest配置、assets资源加载逻辑与gen/R.java生成规范是掌握Android原生游戏开发流程的优质入门范例。1. 这不是“植物大战僵尸”的复刻工程而是 Android 游戏逆向与轻量级重构的实战切口很多人搜“植物大战僵尸 android 源码2”第一反应是找现成 APK 反编译出来的 Java/Kotlin 代码想改个阳光数值、加个新植物甚至移植到新设备。但现实很骨感官方从未开源所有所谓“源码”均来自第三方反编译产物——dex2jar jadx 生成的 Java 伪代码无资源绑定逻辑、无 JNI 层调度、无 AssetManager 生命周期管理更不包含 OpenGL ES 渲染管线和帧同步机制。它本质是一份可读但不可直接编译运行的逆向快照价值不在“拿来即用”而在于用它练出 Android 游戏底层的肌肉记忆。适合三类人刚学完 Android 四大组件想接触游戏循环的新手做定制 ROM 或教育类模拟器需要理解老游戏资源加载路径的嵌入式开发者以及正在为某跨平台系统适配经典游戏逻辑、需厘清原始状态机设计边界的中间件工程师。本文不提供“一键打包安装包”只带你从反编译产物出发把碎片化的 Activity、SurfaceView、AssetManager 调用链重新焊回一个能跑通主界面、响应点击、加载图集的最小可验证体——这才是“源码2”真正该被打开的方式。2. 从反编译产物重建可编译工程结构还原与依赖锚定拿到常见的“植物大战僵尸 android 源码2”压缩包通常含src/、res/、assets/、AndroidManifest.xml别急着 import 到 Android Studio。第一步是识别它的真实技术栈底座——这不是现代 Jetpack Compose 项目而是基于 Android 2.3–4.4 时代的SurfaceView Canvas渲染范式核心线程模型为“主线程处理输入 子线程渲染循环”且大量使用BitmapFactory.decodeStream()直接读 assets 下 PNG无任何图片加载库抽象层。2.1 工程骨架重建为什么必须手动新建而非直接导入直接将反编译src/导入新项目会触发至少三类编译失败R.java引用全红反编译R.java是硬编码整型常量与当前build.gradle生成的R类冲突android.R.*被误引用反编译代码中大量android.R.drawable.xxx实际应指向项目内R.drawable.xxxIDE 无法自动修正AssetManager初始化路径错位反编译代码假设getAssets()在Activity构造时已就绪但真实生命周期中onCreate()才可用。提示不要尝试修复反编译R.java。正确做法是彻底删除src/中所有R.java和BuildConfig.java让 Gradle 重新生成。# 在空项目根目录执行清理残留 find . -name R.java -delete find . -name BuildConfig.java -delete find . -name *.iml -delete rm -rf .idea重建步骤严格按以下顺序创建空的 Android Studio 项目Minimum SDK 设为 API 10即 Android 2.3将反编译src/全量复制到app/src/main/java/保留包名如com.popcap.pvz将res/全量覆盖app/src/main/res/将assets/全量复制到app/src/main/assets/用反编译AndroidManifest.xml替换app/src/main/AndroidManifest.xml但必须手动校验application标签内android:theme是否为android:style/Theme.NoTitleBar.Fullscreen——这是 SurfaceView 全屏渲染的关键开关漏掉会导致黑屏。2.2 关键依赖补全Canvas 渲染链的三个锚点反编译代码中频繁出现GamePanel extends SurfaceView implements SurfaceHolder.Callback但缺失SurfaceHolder的实际绑定逻辑。需在GamePanel构造函数末尾强制注入// app/src/main/java/com/popcap/pvz/GamePanel.java public GamePanel(Context context, AttributeSet attrs) { super(context, attrs); getHolder().addCallback(this); // 锚点1必须显式注册回调 setFocusable(true); setFocusableInTouchMode(true); this.context context; // 锚点2初始化 Bitmap 资源池避免 decodeStream 在子线程反复调用 initBitmaps(); }initBitmaps()必须在SurfaceHolder就绪前完成否则decodeStream()会因AssetManager未初始化而抛NullPointerException。典型实现如下private void initBitmaps() { try { // 锚点3所有 assets 路径必须以 / 开头且区分大小写 sunBitmap BitmapFactory.decodeStream( context.getAssets().open(images/sun.png) ); zombieBitmap BitmapFactory.decodeStream( context.getAssets().open(images/zombie.png) ); // ... 加载其他图集 } catch (IOException e) { Log.e(GamePanel, Failed to load bitmap from assets, e); } }注意assets/images/sun.png路径必须与app/src/main/assets/images/下实际文件结构完全一致。反编译包常存在路径大小写错误如Sun.png写成sun.PNGAndroid 设备对大小写敏感模拟器可能容忍真机必崩。3. 主循环与状态机缝合让“僵尸”真正动起来反编译代码里最混乱的是游戏主循环Game Loop与 UI 线程的耦合逻辑。原始GamePanel中常见while(running) { update(); draw(); }写法但这在 Android 上是定时炸弹——它会阻塞SurfaceHolder的surfaceCreated()回调导致Surface永远无法创建界面卡死白屏。必须解耦为标准Thread Handler模式并严格遵循SurfaceHolder生命周期。3.1 渲染线程安全改造SurfaceHolder 的三阶段契约SurfaceHolder.Callback的三个方法构成硬性契约任何渲染操作都必须在其约束下进行方法触发时机必须做的事禁止做的事surfaceCreated()Surface 第一次创建启动渲染线程调用canvas.drawBitmap()surfaceChanged()Surface 尺寸/格式变更重置画布尺寸、重启线程修改Bitmap加载逻辑surfaceDestroyed()Surface 销毁如切后台running false、thread.join()调用canvas任何方法// GamePanel.java 中重写 Callback Override public void surfaceCreated(SurfaceHolder holder) { running true; gameThread new Thread(new GameLoop()); gameThread.start(); } Override public void surfaceDestroyed(SurfaceHolder holder) { boolean retry true; running false; while (retry) { try { gameThread.join(); retry false; } catch (InterruptedException e) { // 重试 join } } }3.2 状态机驱动的 update()从“僵尸移动”看帧同步陷阱反编译代码中update()常写成zombie.x 1;看似简单实则埋雷问题1无帧率控制→ 高性能设备上僵尸飞奔低端机上龟速爬行问题2无 delta time→ 移动速度与设备刷新率强绑定无法跨设备一致问题3无碰撞检测时机→update()和draw()不在同一线程锁内僵尸位置更新后可能被 draw 读到中间态。解决方案引入固定时间步长Fixed Timestep 双缓冲位置变量// GamePanel.java 成员变量 private static final int TARGET_FPS 60; private static final long TIME_STEP 1000000000L / TARGET_FPS; // 纳秒 private long lastTime System.nanoTime(); private float accumulator 0f; // GameLoop.run() 中 Override public void run() { while (running) { long now System.nanoTime(); float frameTime (now - lastTime) / 1000000000f; lastTime now; accumulator frameTime; // 固定步长更新避免帧率漂移 while (accumulator 1f / TARGET_FPS) { update(1f / TARGET_FPS); // 传入固定 delta accumulator - 1f / TARGET_FPS; } draw(); } } private void update(float deltaTime) { // 使用 deltaTime 计算位移确保跨设备一致 zombie.x zombie.speed * deltaTime * 60; // *60 补偿 1/60 秒基准 // 碰撞检测必须在此处完成且用 volatile 或 synchronized 保护共享状态 checkCollision(); }注意zombie.speed若来自 assets/json 配置必须在surfaceCreated()后、gameThread.start()前完成加载否则update()中首次访问为 null。4. 资源加载与 AssetManager 黑匣子为什么图集总显示为黑块90% 的“源码2”编译后出现黑屏或图块全黑根源不在代码逻辑而在AssetManager对资源路径、压缩格式、解码参数的隐式要求。反编译包常忽略这些细节导致BitmapFactory.decodeStream()返回 null 或全透明 Bitmap。4.1 assets 路径的四个致命细节绝对路径前缀context.getAssets().open(images/sun.png)中images/sun.png是相对路径但open()方法内部会拼接assets/前缀因此assets/文件夹本身不能出现在字符串中大小写敏感sun.png≠Sun.PNG真机必报FileNotFoundException路径分隔符必须用/Windows 风格\在 Android 上直接失败无扩展名陷阱某些反编译包中图集被重命名为sun无 .png需检查assets/images/下真实文件名。验证命令在项目根目录执行# 检查 assets 结构是否与代码路径匹配 find app/src/main/assets/ -type f | grep -i sun\|zombie | head -5 # 输出应为app/src/main/assets/images/sun.png4.2 PNG 解码的三个隐藏参数BitmapFactory.Options中三个字段决定解码成败字段推荐值作用不设后果inScaledfalse禁止自动缩放高分辨率设备上图变糊、失真inPreferredConfigBitmap.Config.ARGB_8888强制 32 位色深旧设备默认RGB_565PNG 透明通道丢失变黑inJustDecodeBoundstrue预检时仅读取宽高不加载像素首次加载时 OOM// 安全加载模板替换所有 decodeStream 调用 private Bitmap safeDecodeAsset(String path) { try { InputStream is context.getAssets().open(path); BitmapFactory.Options options new BitmapFactory.Options(); options.inScaled false; options.inPreferredConfig Bitmap.Config.ARGB_8888; Bitmap bitmap BitmapFactory.decodeStream(is, null, options); is.close(); return bitmap; } catch (IOException e) { Log.e(AssetLoader, Failed to load path, e); return null; } }提示“血泪经验”某次调试发现zombie.png加载返回null用adb shell ls -l assets/images/发现文件权限为-rwxr-xr-x而 Android 要求 assets 文件权限为-rw-r--r--。用chmod 644 assets/images/*.png修复后正常——这是 Gradle 构建时未重置文件权限导致的玄学问题。5. 避坑五个让“源码2”永远跑不起来的真实翻车现场以下是我在多个模拟项目X 中反复验证的高频崩溃点每一条都对应真实日志和修复动作非理论推测。5.1 现象App 启动闪退Logcat 报java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/core/app/ActivityCompat;原因反编译AndroidManifest.xml中声明了android.permission.READ_EXTERNAL_STORAGE等危险权限但代码中未调用ActivityCompat.requestPermissions()且targetSdkVersion被设为 30Android 11。新版本强制运行时权限而反编译代码仍用旧版checkSelfPermission()逻辑。解决降级targetSdkVersion至 28Android 9并在AndroidManifest.xml中添加android:requestLegacyExternalStoragetrue仅限测试。5.2 现象主界面显示但点击植物无反应Logcat 无报错原因反编译GamePanel中setFocusable(true)被注释或删除导致SurfaceView无法接收触摸事件。Android 触摸事件传递链要求View必须可聚焦才能触发onTouchEvent()。解决在GamePanel构造函数末尾强制添加setFocusable(true); setFocusableInTouchMode(true);并在onTouchEvent()开头加requestFocus();。5.3 现象僵尸移动一卡一卡Logcat显示Skia相关警告原因Bitmap复用时未调用recycle()导致Bitmap对象堆积触发 Skia 渲染引擎 GC 暂停。反编译代码中常见bitmap BitmapFactory.decodeStream(...)覆盖旧引用但旧Bitmap未释放。解决在surfaceDestroyed()中遍历所有成员Bitmap并调用recycle()例如if (sunBitmap ! null !sunBitmap.isRecycled()) { sunBitmap.recycle(); }。5.4 现象切换横竖屏后SurfaceView黑屏surfaceChanged()未被调用原因AndroidManifest.xml中Activity标签缺少android:configChangesorientation|screenSize导致屏幕旋转时 Activity 重建SurfaceView被销毁后未重建。解决在AndroidManifest.xml对应Activity添加android:configChangesorientation|screenSize并在Activity中重写onConfigurationChanged()手动调用gamePanel.surfaceChanged()。5.5 现象APK 安装后图标显示但点击立即崩溃Logcat 报Unable to instantiate activity ComponentInfo原因反编译AndroidManifest.xml中activity android:name.MainActivity的android:name值与实际 Java 类名不匹配常见于包名混淆如反编译后com.popcap.pvz.MainActivity被误写为com.example.pvz.MainActivity。解决用find . -name *.java | xargs grep public class MainActivity确认真实类名再同步修改AndroidManifest.xml中android:name。6. 进阶验证用 ADB Logcat 构建你的“后悔药”调试体系当代码逻辑看似无误却仍不工作别急着重写——老手都靠三板斧定位日志打点 线程快照 资源存在性验证。这套组合拳比断点调试更贴近 Android 真实运行态。6.1 日志打点给每个关键节点装“传感器”在GamePanel的生命周期方法中插入带线程名的日志避免被其他模块日志淹没Override public void surfaceCreated(SurfaceHolder holder) { Log.i(PVZ-Lifecycle, [ Thread.currentThread().getName() ] surfaceCreated); // ... 启动线程 } private void update(float deltaTime) { Log.v(PVZ-Update, zombie.x zombie.x , time deltaTime); // ... 更新逻辑 }提示Log.v()用于高频日志发布时用BuildConfig.DEBUG控制开关Log.i()用于关键节点永不关闭。6.2 线程快照确认渲染线程是否真在跑在GameLoop.run()开头加入线程存活日志并用 ADB 实时抓取Override public void run() { Log.i(PVZ-Thread, GameLoop started on thread: Thread.currentThread().getName()); while (running) { // ... 主循环 } Log.i(PVZ-Thread, GameLoop exited); }ADB 命令实时监控# 过滤 PVZ 相关日志-T 显示时间戳-S 显示线程名 adb logcat -T -S | grep PVZ # 查看当前进程所有线程确认 GameLoop 线程是否存在 adb shell ps -t -p $(adb shell pidof com.popcap.pvz)6.3 资源存在性验证绕过代码直击 assets当怀疑assets路径问题用 ADB 直接检查 APK 内部结构比看代码更可靠# 1. 先获取 APK 路径假设已安装 adb shell pm path com.popcap.pvz # 输出package:/data/app/~~abc123/com.popcap.pvz-xyz.apk # 2. 将 APK 拉到本地并解压 adb pull /data/app/~~abc123/com.popcap.pvz-xyz.apk ./pvz.apk unzip -l pvz.apk | grep -i sun\|zombie # 3. 关键验证assets 下是否有 images/ 目录文件名是否全小写 # 正确输出应含assets/images/sun.png # 错误输出如assets/Images/Sun.PNG → 必须重命名6.4 最小化验证表五步确认你的“源码2”已活验证项命令/操作期望结果失败含义APK 安装成功adb install -r app-debug.apkSuccess签名或包名冲突Activity 启动adb shell am start -n com.popcap.pvz/.MainActivity界面弹出AndroidManifest.xml中MainActivity未声明或类名错Surface 创建adb logcat | grep surfaceCreated出现日志行SurfaceView未正确 attach 或setContentView()顺序错Bitmap 加载adb logcat | grep Failed to load无输出assets路径或权限问题渲染线程存活adb shell ps -t | grep GameLoop显示线程名surfaceCreated()未触发或running为 false我一般会在每次git commit前跑一遍这张表哪怕只是改了一行注释——因为“源码2”的脆弱性在于它不是设计出来的工程而是逆向出来的化石。你修复的从来不是 bug而是时间在代码上刻下的氧化层。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

基于线性回归的PM2.5预测系统Python源码实战解析

基于线性回归的PM2.5预测系统Python源码实战解析

简介:基于线性回归的PM2.5预测系统源码,是一套面向Python学习者、机器学习入门者及大气环境数据分析场景的小型完整项目。代码以单文件Python脚本承载数据读取、特征构造、模型训练与结果预测等关键流程,配套原始训练/测试CSV表、处理后的特征…

2026/10/11 22:50:35 阅读更多 →
PgQue 监控实战:5 个必须告警的队列健康指标 + 如何揪出卡住的消费者

PgQue 监控实战:5 个必须告警的队列健康指标 + 如何揪出卡住的消费者

【免费下载链接】PgQue PgQue – Zero-bloat Postgres queue built on top of on battle-proven Skypes PgQ. One SQL file to install, pg_cron to tick https://pgque.dev 项目地址: https://gitcode.com/gh_mirrors/pg/PgQue 点击查看 免费下载 PgQue 是一个零膨…

2026/10/11 22:49:35 阅读更多 →
农行银企直联全链路实战:从密钥申请到转账对账的Java避坑指南

农行银企直联全链路实战:从密钥申请到转账对账的Java避坑指南

简介:这份资源面向使用 Java 对接农业银行银企直联的开发者,聚焦企业财务系统与银行系统之间的电子数据交换场景,帮助解决转账、余额查询、支付等业务自动化处理中的接口开发与安全控制问题。压缩包共 20 个文件,约 23KB&#xff…

2026/10/11 22:49:35 阅读更多 →

最新新闻

同城家政服务平台搭建,多商户派单方案详解

同城家政服务平台搭建,多商户派单方案详解

同城家政服务平台搭建:多商户入驻与智能派单方案详解同城家政行业早已从单一门店自营模式,转向多商户平台化联营发展。平台整合全城多家家政公司、个体服务商、持证服务师傅,统一承接用户订单,通过智能调度完成订单分发与履约。相…

2026/10/11 23:39:46 阅读更多 →
PDF加密权限解除实战:用qpdf免费命令行一键解锁

PDF加密权限解除实战:用qpdf免费命令行一键解锁

上周同事甩过来一个PDF,说打印店打不了,让我帮忙看看。我一看,文件本身没坏,是加了权限限制——允许查看,但打印和复制都被锁了。这种问题我一年能遇到几十次:文档在手机上看一点毛病没有,真要用…

2026/10/11 23:39:46 阅读更多 →
大数据缓存实战:Redis与Alluxio定位配置与踩坑

大数据缓存实战:Redis与Alluxio定位配置与踩坑

干大数据这行的人,迟早会被一个词拦住:慢。任务跑得慢、查询出得慢、报表刷得慢,追根问底,大多不是因为计算引擎不给力,而是存储访问拖了后腿。我在几个大数据平台的项目里折腾过缓存方案,常用的两样是Redi…

2026/10/11 23:39:46 阅读更多 →
基于蝴蝶优化算法的IEEE30节点无功优化Matlab实现与参数调优

基于蝴蝶优化算法的IEEE30节点无功优化Matlab实现与参数调优

1. 从"网损"到算法:先搞懂无功优化到底在优化什么说到电力系统优化调度,"有功优化"大家都很熟——机组出多少钱、发多少有功,直接影响运行成本。但大部分人第一次接触"无功优化"时都会有一个疑问:无…

2026/10/11 23:39:46 阅读更多 →
四月修复版H5农场养殖鸡蛋理财鸡源码部署与支付对接避坑指南

四月修复版H5农场养殖鸡蛋理财鸡源码部署与支付对接避坑指南

简介:最新修复版H5农场牧场养殖理财鸡游戏运营源码,定位为可直接运营的网站游戏项目,适合有建站基础、希望搭建休闲理财类H5游戏的个人或团队二次开发。资源包共2271个文件,约88.4MB,主体由HTML页面、JavaScript逻辑、…

2026/10/11 23:39:46 阅读更多 →
改进版Q-learning实战:Double Q、n步回报与经验回放

改进版Q-learning实战:Double Q、n步回报与经验回放

简介:基于Q-learning的改进版强化学习算法项目,聚焦路径规划场景,面向MATLAB用户及强化学习入门者。项目针对经典Q-learning收敛慢的问题,融合学习率衰减、动态ε-greedy探索、经验回放、目标网络与双线性更新等改进策略&#xff…

2026/10/11 23:38:45 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/11 14:36:54 阅读更多 →