Files
Spine_lab/阶段一分析.md
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

8.7 KiB
Raw Permalink Blame History

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 导入编辑器。