目标:在 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 副本、从载荷派生的批量产物。
8.7 KiB
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. 打包与加固(关键障碍)
launcher.jar实际未被使用:把它移走后程序照常启动。- 启动类与编辑器类内嵌在
libjvm里:arm64 运行时的libjvm.dylib约 56MB,其中__data段近 49MB,明显是内嵌的(加密)类档案。 - JVMTI agent 加载被裁掉:
-agentpath会被解析但不加载 agent(Agent_OnLoad字符串都不存在),-javaagent同理无效。 - classpath 注入无效:追加
-Djava.class.path不生效;自定义类加载器s.LPt4自己管理类来源。 - 类被混淆:libGDX 核心类与编辑器类被塞进
s.*包(例如 Actor =s.AmK、Group =s.Xgs),但部分类保留原名(TextButton、Label、ImageButton…)。 - 编辑器载荷缓存在
~/Library/Application Support/Spine/updates/<版本>,4.0 之后为加密数据。
3. 我们采用的注入方案(已跑通)
因为常规插件/agent/classpath 全部被堵死,改为在原生层注入:
- 原生化注入器
native-hook/hook.c- 通过
DYLD_INSERT_LIBRARIES载入; - fishhook 式重绑定
_dlsym(含_dyld_register_func_for_add_image,覆盖后加载的libjvm); - 拦截
JNI_CreateJavaVM,拿到JavaVM*。
- 通过
- 用 JNI 直接注入类
DefineClass把我们的类定义进 JVM(绕开 classpath/加载器限制);RegisterNatives暴露defineEditorClasses/capturedClass/readField给我们的 Java 引导类。
- 引导类
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. 下一步计划(阶段二)
- 利用「预览」面板的关闭 → 打开动作做差分:在注入端对对象快照做前后对比,锁定该动作涉及的对象;
- 由该对象向上/向侧找到面板本体与其停靠容器;
- 确认注册 API(候选签名
(String, 容器)),注册我们自己的「粒子」面板; - 面板内容先用占位控件,验证"面板出现 + 可交互"后,再接入粒子系统数据。
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 等字段), 由用户手工在「视图」菜单里反复开关面板(插槽颜色 / 权重 / 皮肤 / 网格工具 / 层级树 / 指标 / 轮廓 / 预览)。
结论
-
开关面板不会创建新对象(每轮都是
新增=0,但变化>0)——面板在启动时已全部创建,开关只改变状态与布局。 -
反复变化的类集中在
s.fOM(多个不同实例,对应不同面板)与com.badlogic.gdx.scenes.scene2d.ui.ImageTextButton(面板标签页)。 -
s.fOM实例的父链一致为:s.fOM → s.FEZ → s.BTV → s.KJW → s.ILX ×5 → s.FCx → s.Xgs(UI 根)即:
s.fOM= 面板内容节点,s.FEZ= 面板停靠区域。 -
面板标签页是
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导入编辑器。