import java.lang.instrument.Instrumentation; import java.lang.reflect.*; import java.net.URLClassLoader; /** * 只读探针 6:把面板的**整棵控件树**连坐标打出来(含 stage 坐标), * 用来定位"列表里的内容被画到面板外"。 * * 取值方法来自字节码(javap -p -c): * dl.c_()F=getX dl.d_()F=getY dl.L()F=getWidth dl.I()F=getHeight * dl.D()Ldo; = getParent do.D()Lhq; = getChildren hq extends fI(Array),size 字段在 fI 里 * ⚠️ et(Table) 自己也声明了 `int L()` / `int I()`(列数/行数),所以必须**按返回类型 float 过滤**。 */ public final class GeomProbe6 { static Instrumentation INST; static Object STATE; static Class HQL, ARR, AO; 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("[geom6] " + m); } static void run() throws Exception { for (Class c : INST.getAllLoadedClasses()) { if (!c.getName().equals("com.spineview.ObjectPanel")) continue; try { Field f = c.getDeclaredField("LAST_STATE"); f.setAccessible(true); Object v = f.get(null); if (v != null) STATE = v; } catch (Throwable ignored) { } } if (STATE == null) { log("LAST_STATE 拿不到"); return; } ClassLoader payload = null; for (Class c : INST.getAllLoadedClasses()) { if (c.getClassLoader() instanceof URLClassLoader) { payload = c.getClassLoader(); break; } } HQL = Class.forName("hq", true, payload); ARR = Class.forName("fI", true, payload); AO = Class.forName("dl", true, payload); Class og = null; for (Class c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c; 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; } } onRender.setAccessible(true); onRender.invoke(null, new Runnable() { @Override public void run() { try { work(STATE); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); } } }); } static void work(Object state) throws Exception { Object root = state("root"); log("=========== 面板控件树(缩进=层级;坐标为 stage 绝对坐标)==========="); walk(root, 0, 0f, 0f, 6); log("=========== 面板根再往上(谁给它多大地方)==========="); Object p = parent(root); float ax = 0f, ay = 0f; for (int i = 0; i < 6 && p != null; i++) { log(" 上级[" + i + "] " + p.getClass().getName() + " " + geom(p)); Object np = parent(p); if (np == p) break; p = np; } log("=========== 关键对象定位 ==========="); Object scroll = state("listScroll"), table = state("listTable"); log(" listScroll " + geom(scroll) + " 子控件=" + size(childrenOf(scroll))); log(" listTable " + geom(table) + " 行数=" + size(childrenOf(table))); log(" listTable 的 stage 坐标 = " + stageX(table) + "," + stageY(table)); log("======================================================="); } static Object state(String name) throws Exception { Field f = STATE.getClass().getDeclaredField(name); f.setAccessible(true); return f.get(STATE); } static void walk(Object actor, int depth, float px, float py, int maxDepth) { if (actor == null || depth > maxDepth) return; StringBuilder ind = new StringBuilder(); for (int i = 0; i < depth; i++) ind.append(" "); float x = fl(actor, "c_"), y = fl(actor, "d_"); Object kids = childrenOf(actor); int n = size(kids); log(ind + actor.getClass().getName() + " " + geom(actor) + " stage=(" + r(px + x) + "," + r(py + y) + ") 子控件=" + n); for (int i = 0; i < n; i++) { Object child = get(kids, i); walk(child, depth + 1, px + x, py + y, maxDepth); } } static float stageX(Object actor) { float x = 0f; Object a = actor; for (int i = 0; i < 8 && a != null; i++) { x += fl(a, "c_"); a = parent(a); } return r(x); } static float stageY(Object actor) { float y = 0f; Object a = actor; for (int i = 0; i < 8 && a != null; i++) { y += fl(a, "d_"); a = parent(a); } return r(y); } static String geom(Object actor) { if (actor == null) return "null"; return "[local x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_")) + " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I")) + " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]"; } static Object parent(Object actor) { if (actor == null) return null; for (Method m : actor.getClass().getMethods()) { if (m.getParameterTypes().length != 0) continue; if (!m.getName().equals("D")) continue; if (!m.getReturnType().getName().equals("do")) continue; try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { } } return null; } static Object childrenOf(Object actor) { if (actor == null) return null; for (Method m : actor.getClass().getMethods()) { if (m.getParameterTypes().length != 0) continue; if (m.getReturnType() != HQL) continue; try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { } } return null; } static int size(Object array) { if (array == null) return 0; for (Class t = ARR; 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 (Method m : array.getClass().getMethods()) { if (!m.getName().equals("D")) continue; Class[] t = m.getParameterTypes(); if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue; try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { } } return null; } static float fl(Object actor, String name) { if (actor == null) return -1f; for (Method m : actor.getClass().getMethods()) { if (!m.getName().equals(name)) continue; if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue; try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { } } return -1f; } static float r(float v) { return Math.round(v * 100f) / 100f; } }