Files
Spine_lab/阶段一分析.md
T
tianmo 2e7ab7c33c 阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示),
点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。

本轮打通:
1. 编辑器在我们自己的进程里完整启动
   - 类加载器必须是 URLClassLoader 本身(自检要求)
   - Editor.main 必须在 AWT EDT 上调用
   - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava
   - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身
2. 3.8.99 全量解密 → 可读源码
   - 字符串 17251/17461(伪造同名调用帧对齐 key)
   - 混淆动态派发还原 53871 处(调引导方法取 MemberName)
3. 自建视图
   - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检)
   - 面板 Actor 由外部反射填进 us.spineView.D
   - 复用已有图标做别名(不新建图片资源)
   最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开

另外:
- ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩
- DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成
- 阶段四-视图面板挂载.md:完整记录做法与踩过的坑

不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、
Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
2026-10-16 13:34:18 +08:00

175 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spine 加功能 · 阶段一分析
目标:在 Spine 编辑器内部增加自己的功能(最终形态是「粒子系统」面板),不破坏原有功能,且不改动用户真实的 `/Applications/Spine.app`。
---
## 1. 技术栈结论
| 层 | 结论 | 依据 |
| --- | --- | --- |
| 原生外壳 | Objective-C / Cocoa(AppKit) | `Spine` 二进制只依赖 Foundation/AppKit/Cocoa,Info.plist 里 `DTXcode 1540` |
| 启动方式 | 自定义 launcher,`dlopen` + `dlsym` 后直接 `JNI_CreateJavaVM` | 二进制字符串与反汇编 |
| 运行时 | 内置 OpenJDK;x86_64 为 Azul Zulu 10.0.2,Apple Silicon 为 arm64 定制运行时 | `libjvm` 版本字符串 |
| 界面/渲染 | Java + libGDX(Scene2D UI)+ LWJGL/GLFW + OpenAL + FreeType | 编辑器载荷里的 `com/badlogic/gdx/**`、`org/lwjgl/**` 引用 |
| 资源管线 | 内置 ffmpeg / oxipng | `launcher/2/bin/` |
---
## 2. 打包与加固(关键障碍)
1. **`launcher.jar` 实际未被使用**:把它移走后程序照常启动。
2. **启动类与编辑器类内嵌在 `libjvm` 里**:arm64 运行时的 `libjvm.dylib` 约 56MB,其中 `__data` 段近 49MB,明显是内嵌的(加密)类档案。
3. **JVMTI agent 加载被裁掉**:`-agentpath` 会被解析但不加载 agent(`Agent_OnLoad` 字符串都不存在),`-javaagent` 同理无效。
4. **classpath 注入无效**:追加 `-Djava.class.path` 不生效;自定义类加载器 `s.LPt4` 自己管理类来源。
5. **类被混淆**:libGDX 核心类与编辑器类被塞进 `s.*` 包(例如 Actor = `s.AmK`、Group = `s.Xgs`),但部分类保留原名(`TextButton`、`Label`、`ImageButton`…)。
6. 编辑器载荷缓存在 `~/Library/Application Support/Spine/updates/<版本>`,4.0 之后为加密数据。
---
## 3. 我们采用的注入方案(已跑通)
因为常规插件/agent/classpath 全部被堵死,改为在原生层注入:
1. **原生化注入器** `native-hook/hook.c`
- 通过 `DYLD_INSERT_LIBRARIES` 载入;
- fishhook 式重绑定 `_dlsym`(含 `_dyld_register_func_for_add_image`,覆盖后加载的 `libjvm`);
- 拦截 `JNI_CreateJavaVM`,拿到 `JavaVM*`。
2. **用 JNI 直接注入类**
- `DefineClass` 把我们的类定义进 JVM(绕开 classpath/加载器限制);
- `RegisterNatives` 暴露 `defineEditorClasses` / `capturedClass` / `readField` 给我们的 Java 引导类。
3. **引导类** `spine-lab/build/plugin-src/com/spineparticle/Boot.java`
- 等编辑器 UI 起来后,用 JNI 读私有字段遍历对象图;
- 目前已经能定位 UI 控件、修改它们的状态。
**已验证能力**:把左侧工具栏第一个按钮的文字改成「粒子系统」,界面上真实渲染出来了。
---
## 4. 运行环境隔离(不影响用户真实安装)
- 实验室副本:`spine-lab/Spine.app`
- 数据目录隔离:注入 `-Duser.home=spine-lab/home`
- 启动脚本:`spine-lab/启动实验室Spine.command`、`spine-lab/run-lab.sh`
- 日志:`/tmp/spine-lab-inject.txt`(引导类)、`/tmp/spine-native-hook.log`(原生 hook)
---
## 5. 运行期已探明的对象结构
| 对象 | 真实类名 | 说明 |
| --- | --- | --- |
| Actor 基类 | `s.AmK` | 所有 UI 控件的基类 |
| Group(容器) | `s.Xgs` | 出现在 2919 个 Actor 字段上,确认为父容器类 |
| Stage 候选 | `s.okh` / `s.RUj` 等 | 有 `addActor(Actor)`+`draw()`+`act(float)`,非 Actor |
| 编辑器类加载器 | `s.LPt4` | 内部 `mc` 字段是 `ConcurrentHashMap`,**含 2365 个类名**,可枚举全部类 |
| 「骨骼」文字持有者 | `Label` | 位于工具栏按钮内 |
| UI 根 Group | `s.Xgs` | 从 Label 沿 `parent` 爬到的顶点 |
| 面板标题标签页 | `s.XDB` | **实际是 `ImageTextButton`**,不是面板本体 |
父链示例(工具栏按钮):`Label → s.fgD → s.nHS → s.NXg → s.nHS → s.YDW → s.BTV → s.KJW → s.ILX ×3 → s.FCx → s.Xgs`
父链示例(层级树面板标题):`s.zFH → s.XDB(ImageTextButton) → s.nHS → s.KJW → s.ILX → s.FCx → s.Xgs`
---
## 6. 已解决 / 未解决
**已解决**
- 代码能进 Spine 的 JVM 并执行;
- 可枚举全部类(2365 个);
- 可读任意对象的私有字段(JNI 绕过模块限制);
- 可定位并**修改正在渲染的现有控件**(工具栏按钮文字已改,界面生效)。
**未解决**
- 手工 `addActor` 塞进去的新控件(按钮副本、面板副本)**不渲染**;
- 原因推测:libGDX 的 Table 布局、Skin 样式与 layout 缓存要求走编辑器自己的注册/布局路径;
- 面板的本体("标题栏 + 内容"的组合容器)尚未定位,目前只找到了它的标题标签页;
- 面板管理器/注册 API 还没确定(静态扫描给出过 `s.JBH._(String,nHS)->void` 这类候选)。
---
## 7. 下一步计划(阶段二)
1. 利用「预览」面板的**关闭 → 打开**动作做差分:在注入端对对象快照做前后对比,锁定该动作涉及的对象;
2. 由该对象向上/向侧找到面板本体与其停靠容器;
3. 确认注册 API(候选签名 `(String, 容器)`),注册我们自己的「粒子」面板;
4. 面板内容先用占位控件,验证"面板出现 + 可交互"后,再接入粒子系统数据。
---
## 8. 产物清单
```
native-hook/
hook.c 原生注入器(fishhook + JNI_CreateJavaVM + DefineClass)
hook.dylib 编译产物(arm64)
agent/ JVMTI agent 探针(备用,本运行时不可用)
spine-lab/
Spine.app/ 实验室副本(未改动用户真实安装)
home/ 隔离的数据目录
build/plugin-src/ 引导类源码(com/spineparticle/Boot.java)
build/boot/ 编译产物(当前注入使用的类)
启动实验室Spine.command 双击启动
阶段一分析.md 本文档
```
---
## 9. 阶段二进度:面板机制定位(实测)
**实验方法**:注入端每 6 秒对对象图做一次快照,对比每个 Actor 的数值指纹(float/int/boolean 等字段),
由用户手工在「视图」菜单里反复开关面板(插槽颜色 / 权重 / 皮肤 / 网格工具 / 层级树 / 指标 / 轮廓 / 预览)。
**结论**
1. 开关面板**不会创建新对象**(每轮都是 `新增=0`,但 `变化>0`)——面板在启动时已全部创建,开关只改变状态与布局。
2. 反复变化的类集中在 **`s.fOM`**(多个不同实例,对应不同面板)与 `com.badlogic.gdx.scenes.scene2d.ui.ImageTextButton`(面板标签页)。
3. `s.fOM` 实例的父链一致为:
```
s.fOM → s.FEZ → s.BTV → s.KJW → s.ILX ×5 → s.FCx → s.Xgs(UI 根)
```
即:**`s.fOM` = 面板内容节点,`s.FEZ` = 面板停靠区域**。
4. 面板标签页是 `ImageTextButton`,父链为 `s.nHS → s.NXg → s.nHS → s.YDW → s.BTV → s.KJW → …`。
**下一步**
- 方案 A:新建 `s.fOM` 实例 + 对应标签页,按上述父链挂进 `s.FEZ` 与标签栏;
- 方案 B(更稳):找到面板管理器的注册 API(静态扫描曾出现 `s.JBH._(String,nHS)->void` 这类候选),
通过它注册,让布局/标签栏/视图菜单一并由编辑器自己处理。
---
## 10. 阶段二结论与最终选型(已落地)
**实测结论**
- 新增控件(`addActor`)**不渲染**;
- 改写编辑器内部对象(面板标签等)**会破坏其内部一致性并触发 fatal error**(当天累计多份崩溃报告);
- 面板没有运行期注册 API(扫遍 2365 个类只找到布局用的 `s.oxX`,没有"注册面板"的入口)。
因此"在编辑器窗口内部新增面板"这条原生路线,在这套**被二次打包并加固**的运行时上代价过高、风险过大。
**最终选型:方案 A(已实现)**
粒子面板做成**独立原生面板窗口**,外观与编辑器一致,并在启动时**自动贴到 Spine 窗口右侧**:
```
spine-lab/panel/Panel.swift 面板源码(Swift / AppKit,暗色 NSWindow,置顶)
spine-lab/panel/SpineParticlePanel 编译产物
spine-lab/启动Spine+粒子面板.command 一键启动:Spine 实验室实例 + 面板
```
面板内容(原型):粒子系统列表、参数滑条(发射速率/生命周期/初速度/重力/缩放)、
「+ 新建粒子」「导出动画数据」按钮。定位方式是 `CGWindowListCopyWindowInfo` 找到 Spine 窗口后贴合其右侧。
**后续可扩展**
- 面板内接入真正的粒子编辑器(可直接复用 `SpineParticle` 项目里已有的 Vue 逻辑,改成面板窗口内的 WebView 或重写为 AppKit 控件);
- 通过文件/命令与 Spine 联动:面板导出 Spine JSON 动画数据 → 用已验证的连接器流程 `Spine --import` 导入编辑器。