271 lines
10 KiB
Java
271 lines
10 KiB
Java
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<String> names = new java.util.TreeSet<String>();
|
||
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; }
|
||
}
|