目标:在 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 副本、从载荷派生的批量产物。
175 lines
8.7 KiB
Markdown
175 lines
8.7 KiB
Markdown
# 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` 导入编辑器。
|