面板支持热更:视图里挂稳定容器(编辑器类加载器建的 Table),面板内容放在容器里;后台线程轮询 build/classes/com/spineview/ObjectPanel.class 的 mtime,一变就用新的子类加载器重建内容换进容器 —— 改完代码只需 build.sh,编辑器不用重启、场景与选择状态保留

This commit is contained in:
tianmo committed 2026-10-16 16:35:23 +08:00
1 parent f8d2cda17c
commit be0ed2352b
1 file changed
+94 -1
+94 -1
View File
@@ -70,11 +70,19 @@ public final class ViewInstaller {
System.err.println("[view] 找不到 Skin");
return false;
}
// 视图里挂的是**稳定容器**(由编辑器类加载器建),面板内容放在它里面:
// 这样热更时只要换容器里的内容,编辑器不用重启、场景与选择状态都保留。
Class<?> tableClass = Class.forName("et", true, loader);
Object holder = tableClass.getConstructor().newInstance();
callStatic(tableClass, Obf.TABLE_SET_FILL_PARENT, new Class<?>[] {boolean.class}, holder,
Boolean.TRUE);
Object panel = buildPanel(loader, skin);
addChild(tableClass, holder, panel);
startHotReload(loader, skin, holder);
for (Field field : viewEnum.getDeclaredFields()) {
if (!field.getType().getName().equals(ACTOR)) continue;
field.setAccessible(true);
field.set(view, panel);
field.set(view, holder);
System.err.println("[view] 已把 Spine View 面板挂到视图 " + view);
showView(loader, view);
return true;
@@ -113,6 +121,91 @@ public final class ViewInstaller {
}
}
/** 调一个"静态/实例"都行的方法:这里用来调 Table.setFillParent(实例方法)。 */
private static void callStatic(Class<?> owner, String name, Class<?>[] parameterTypes,
Object receiver, Object... arguments) throws Exception {
for (Method method : owner.getMethods()) {
if (!method.getName().equals(name)) continue;
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
method.setAccessible(true);
method.invoke(receiver, arguments);
return;
}
throw new NoSuchMethodException(owner.getName() + "." + name);
}
/** 往容器里加 Actor(Table.add(Actor))。 */
private static void addChild(Class<?> tableClass, Object table, Object actor) throws Exception {
for (Method method : tableClass.getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) continue;
if (!types[0].isAssignableFrom(actor.getClass())) continue;
if (method.getReturnType() == void.class) continue;
method.setAccessible(true);
method.invoke(table, actor);
return;
}
throw new NoSuchMethodException("找不到 add(Actor)");
}
/** 清空容器(Table.clearChildren())。 */
private static void clearChildren(Class<?> tableClass, Object table) throws Exception {
for (Method method : tableClass.getMethods()) {
if (!method.getName().equals(Obf.TABLE_CLEAR_CHILDREN)) continue;
if (method.getParameterTypes().length != 0) continue;
method.setAccessible(true);
method.invoke(table);
return;
}
throw new NoSuchMethodException("找不到 clearChildren()");
}
/**
* 热更:轮询 `viewpanel/build/classes/com/spineview/ObjectPanel.class` 的修改时间,
* 一变就用**新的子类加载器**(parent = 编辑器加载器)重建面板内容,换进稳定容器里。
* 这样改完代码只要 `./viewpanel/build.sh`,编辑器不用重启。
*/
private static void startHotReload(final ClassLoader loader, final Object skin,
final Object holder) {
final String classesDir = System.getProperty("spine.classes");
if (classesDir == null) {
System.err.println("[view] 没有 -Dspine.classes,热更关闭");
return;
}
final java.io.File marker = new java.io.File(classesDir, "com/spineview/ObjectPanel.class");
final long[] lastSeen = new long[] {marker.lastModified()};
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
long stamp = marker.lastModified();
if (stamp == lastSeen[0] || stamp == 0) continue;
lastSeen[0] = stamp;
Thread.sleep(400); // 等编译落盘
java.net.URL classesUrl = new java.io.File(classesDir).toURI().toURL();
java.net.URLClassLoader child = new java.net.URLClassLoader(
new java.net.URL[] {classesUrl}, loader);
Class<?> panelClass = Class.forName("com.spineview.ObjectPanel", true, child);
Object fresh = panelClass.getMethod("build", ClassLoader.class, Object.class)
.invoke(null, child, skin);
Class<?> tableClass = Class.forName("et", true, loader);
clearChildren(tableClass, holder);
addChild(tableClass, holder, fresh);
System.err.println("[view] 热更完成:面板已用新的类加载器重建(未重启编辑器)");
} catch (InterruptedException ignored) {
return;
} catch (Throwable error) {
System.err.println("[view] 热更失败: " + error);
}
}
}
}, "spineview-hot-reload");
thread.setDaemon(true);
thread.start();
}
/** Skin 挂在 OG 的静态字段上(字节码里 OG 有 LSkin;D)。 */
private static Object findSkin(ClassLoader loader) throws Exception {
Class<?> holder = Class.forName("OG", true, loader);