import java.lang.instrument.Instrumentation; import java.lang.reflect.*; import java.net.URLClassLoader; import java.util.*; /** * 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来, * 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。 * * 依据(都不是猜的): * - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY, * scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float); * - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height; * - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、 * `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。 */ public final class GeomProbe2 { static Instrumentation INST; static ClassLoader L; static Object STATE; static Class OG; static final String[] FLOAT_NAMES = { "x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation" }; public static void agentmain(String args, Instrumentation inst) { INST = inst; try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); } } static void log(String m) { System.err.println("[geom2] " + m); } static void run() throws Exception { for (Class c : INST.getAllLoadedClasses()) { if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; } } if (L == null) { log("找不到 URLClassLoader"); return; } Class panel = null; int copies = 0; for (Class c : INST.getAllLoadedClasses()) { if (!c.getName().equals("com.spineview.ObjectPanel")) continue; copies++; if (panel == null) panel = c; } log("已加载的 ObjectPanel 份数 = " + copies + " 用 " + panel + " (loader=" + (panel == null ? "-" : panel.getClassLoader()) + ")"); if (panel == null) { log("ObjectPanel 没加载"); return; } Field last = panel.getDeclaredField("LAST_STATE"); last.setAccessible(true); STATE = last.get(null); log("LAST_STATE = " + STATE); if (STATE == null) { return; } for (Class c : INST.getAllLoadedClasses()) { if (c.getName().equals("OG")) { OG = c; break; } } if (OG == null) { log("OG 没加载"); return; } Method onRender = null; for (Method m : OG.getDeclaredMethods()) { Class[] t = m.getParameterTypes(); if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; } } if (onRender == null) { log("找不到 OG.B(Runnable)"); return; } onRender.setAccessible(true); onRender.invoke(null, new Runnable() { @Override public void run() { try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); } } }); } static void work() throws Exception { Object root = state("root"); Object buttons = state("buttons"); Object scroll = state("listScroll"); Object listTable = state("listTable"); Object status = state("status"); log("==================== 面板控件几何 ===================="); dump("root ", root); dump("buttons ", buttons); dump("listScroll ", scroll); dump("listTable ", listTable); dump("logLabel ", status); log("---- root 的父链(往上看 4 层,看谁给了多少高度)----"); Object p = parent(root); for (int i = 0; i < 4 && p != null; i++) { dump("parent[" + i + "] ", p); p = parent(p); } log("---- listScroll 的父链 ----"); p = parent(scroll); for (int i = 0; i < 3 && p != null; i++) { dump("scroll.parent[" + i + "] ", p); p = parent(p); } log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----"); dumpScrollPaneFields(scroll); log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----"); Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D"); int n = size(children); log(" 子控件数 = " + n); for (int i = 0; i < n; i++) { if (i > 2 && i < n - 2) continue; Object child = get(children, i); dump(" child[" + i + "] ", child); } log("---- 关键判定 ----"); Object widget = findWidget(scroll); log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable) + " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")"); log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3) + " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3)); Object stage = callActor(root, "D"); // Actor.getStage() if (stage != null) { log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B")); } log("====================================================="); } static Object state(String name) throws Exception { Field f = STATE.getClass().getDeclaredField(name); f.setAccessible(true); return f.get(STATE); } static void dump(String label, Object actor) { if (actor == null) { log(label + " = null"); return; } StringBuilder sb = new StringBuilder(); sb.append(label).append(' ').append(actor.getClass().getName()); int i = 0; for (Class t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (f.getType() != float.class) continue; f.setAccessible(true); try { String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i; sb.append(' ').append(name).append('=').append(round(f.getFloat(actor))); i++; } catch (Throwable ignored) { } } if (t.getSuperclass() == null) break; } sb.append(" |getHeight()=").append(round(floatOf(actor, "init"))); Object parent = parent(actor); sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName()); log(sb.toString()); } static void dumpScrollPaneFields(Object pane) { java.util.TreeSet names = new java.util.TreeSet(); for (Class t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; Class type = f.getType(); if (type != float.class && type != int.class && type != boolean.class) continue; f.setAccessible(true); try { Object v = f.get(pane); if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue())); names.add(t.getSimpleName() + "." + f.getName() + "=" + v); } catch (Throwable ignored) { } } } StringBuilder sb = new StringBuilder(); for (String s : names) sb.append(' ').append(s); log(" " + sb); } static Object findWidget(Object pane) { for (Class t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; if (f.getType().getName().equals("dl")) { f.setAccessible(true); try { return f.get(pane); } catch (Throwable ignored) { } } } } return null; } static Object parent(Object actor) { try { for (Method m : actor.getClass().getMethods()) { if (m.getParameterTypes().length != 0) continue; if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) { m.setAccessible(true); Object v = m.invoke(actor); if (v != null) return v; } } } catch (Throwable ignored) { } return null; } static Object callActor(Object actor, String name) { try { for (Method m : actor.getClass().getMethods()) { if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue; m.setAccessible(true); return m.invoke(actor); } } catch (Throwable ignored) { } return null; } static Object call(Class owner, Object target, String name) { try { for (Method m : owner.getMethods()) { if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue; m.setAccessible(true); return m.invoke(target); } } catch (Throwable ignored) { } return null; } static float fl(Object actor, int index) { int i = 0; for (Class t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (f.getType() != float.class) continue; if (i == index) { f.setAccessible(true); try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; } } i++; } } return -1f; } static float floatOf(Object target, String name) { try { for (Method m : target.getClass().getMethods()) { if (!m.getName().equals(name)) continue; if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue; m.setAccessible(true); return ((Float) m.invoke(target)).floatValue(); } } catch (Throwable ignored) { } return -1f; } static int size(Object array) { if (array == null) return 0; for (Class t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (f.getType() != int.class) continue; f.setAccessible(true); try { return f.getInt(array); } catch (Throwable ignored) { } } } return 0; } static Object get(Object array, int index) { if (array == null) return null; for (Class t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) { for (Field f : t.getDeclaredFields()) { if (f.getType() != Object[].class) continue; f.setAccessible(true); try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { } } } return null; } static float round(float v) { return Math.round(v * 100f) / 100f; } }