- ViewInstaller:用 Table.row()/Cell.expand()+fill()/setFillParent 排出" 按钮一行、可滚动文字区一行"的面板;修掉把 setDebug 当成 setFillParent 的误用 - Scene2dPanel:按钮 isChecked/setChecked 与 Label.setText 改用对照表定名, 修掉"点一次反复触发"与"文字不刷新"两个 bug - SceneReader:面板输出改英文(编辑器缺中文字形,中文会渲染成方块) - 新增 tools/obfmap/MapMethods.java:载荷 vs 上游 libGDX 的字节码指纹比对工具 - 新增 混淆名对照表.md:类/方法/字段映射、推导办法、已踩的坑、验证状态
610 lines
25 KiB
Java
610 lines
25 KiB
Java
package com.spineview;
|
||
|
||
import java.lang.reflect.Field;
|
||
import java.lang.reflect.Method;
|
||
import java.lang.reflect.Modifier;
|
||
import java.util.ArrayList;
|
||
import java.util.Enumeration;
|
||
import java.util.List;
|
||
import java.util.zip.ZipEntry;
|
||
import java.util.zip.ZipFile;
|
||
|
||
/**
|
||
* 把"按钮 + 文字显示"面板挂进 Spine 的 scene2d 场景树。
|
||
*
|
||
* 编辑器的窗口是 jglfw 建的原生 NSWindow,不是 AWT 窗口,没有 Swing 面板可挂;
|
||
* 界面本身是 libGDX scene2d 画出来的,只能加进它的 Stage。
|
||
*
|
||
* 这套运行时把 libGDX 的方法名混淆了(类名保留):
|
||
* Stage 的实现 = uk(继承 dt)、Group = do、Actor = dl、Table = et
|
||
* 所以下面全部"按类型/签名找成员",不依赖混淆后的名字。
|
||
*
|
||
* Stage 实例在 tD 里只是局部变量,任何静态字段都没有;但 libGDX 里每个 Actor
|
||
* 都记着自己所在的 Stage,而 Fx.D 是一个静态 Actor —— 从这里就能把 Stage 捞回来。
|
||
*/
|
||
public final class Scene2dPanel {
|
||
private static final String STAGE = "dt";
|
||
private static final String GROUP = "do";
|
||
private static final String ACTOR = "dl";
|
||
private static final String SKIN = "com.badlogic.gdx.scenes.scene2d.ui.Skin";
|
||
private static final String LABEL = "com.badlogic.gdx.scenes.scene2d.ui.Label";
|
||
private static final String TEXT_BUTTON = "com.badlogic.gdx.scenes.scene2d.ui.TextButton";
|
||
private static final String WINDOW = "com.badlogic.gdx.scenes.scene2d.ui.Window";
|
||
|
||
private Scene2dPanel() {
|
||
}
|
||
|
||
public static void installWhenReady(final ClassLoader loader) {
|
||
System.err.println("[panel] 开始等 Stage/Skin…");
|
||
if (System.getProperty("spine.dumpStatics") != null) {
|
||
try {
|
||
Thread.sleep(30000);
|
||
} catch (InterruptedException ignored) {
|
||
return;
|
||
}
|
||
dumpStatics(loader, paths((java.net.URLClassLoader) loader));
|
||
return;
|
||
}
|
||
Thread thread = new Thread(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
for (int i = 0; i < 240; i++) {
|
||
try {
|
||
Object stage = stageFromActorAnchor(loader);
|
||
Object skin = null;
|
||
if (stage != null) {
|
||
Object root = firstFieldOfType(stage, GROUP);
|
||
if (root != null) skin = skinFromRoot(root);
|
||
}
|
||
if (i % 10 == 0) {
|
||
System.err.println("[panel] 第 " + i + " 轮: stage=" + nameOf(stage)
|
||
+ " skin=" + nameOf(skin));
|
||
}
|
||
if (stage != null && skin != null) {
|
||
install(loader, stage, skin, i);
|
||
return;
|
||
}
|
||
} catch (Throwable error) {
|
||
System.err.println("[panel] 查找失败: " + error);
|
||
error.printStackTrace(System.err);
|
||
}
|
||
try {
|
||
Thread.sleep(500);
|
||
} catch (InterruptedException ignored) {
|
||
return;
|
||
}
|
||
}
|
||
System.err.println("[panel] 等不到 Stage/Skin");
|
||
}
|
||
}, "spineview-panel");
|
||
thread.setDaemon(true);
|
||
thread.start();
|
||
}
|
||
|
||
private static String nameOf(Object value) {
|
||
return value == null ? "null" : value.getClass().getName();
|
||
}
|
||
|
||
/**
|
||
* 有了 Stage 实例之后挂面板。Skin 从 Stage 根节点的子节点上取
|
||
* (libGDX 的 Table 自己存着构造时用的 Skin)。
|
||
*/
|
||
public static void installForStage(final Object stage, final ClassLoader loader) {
|
||
// Stage 是先建出来的,UI 是随后才一层层加进去的,所以要等面板挂上来之后才拿得到 Skin。
|
||
Thread thread = new Thread(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
try {
|
||
for (int i = 0; i < 240; i++) {
|
||
Object skin = findSkinDeep(stage, 6, 20000);
|
||
if (skin != null) {
|
||
System.err.println("[panel] 第 " + i + " 轮拿到 Skin: "
|
||
+ skin.getClass().getName());
|
||
install(loader, stage, skin, i);
|
||
return;
|
||
}
|
||
if (i % 20 == 0) {
|
||
System.err.println("[panel] 第 " + i + " 轮还没拿到 Skin(Stage "
|
||
+ nameOf(stage) + ")");
|
||
}
|
||
Thread.sleep(500);
|
||
}
|
||
System.err.println("[panel] 一直没拿到 Skin");
|
||
} catch (Throwable error) {
|
||
System.err.println("[panel] installForStage 失败");
|
||
error.printStackTrace(System.err);
|
||
}
|
||
}
|
||
}, "spineview-skin");
|
||
thread.setDaemon(true);
|
||
thread.start();
|
||
}
|
||
|
||
private static void install(final ClassLoader loader, Object stage, Object skin, int waited)
|
||
throws Exception {
|
||
Class<?> skinClass = Class.forName(SKIN, true, loader);
|
||
Class<?> buttonClass = Class.forName(TEXT_BUTTON, true, loader);
|
||
Class<?> labelClass = Class.forName(LABEL, true, loader);
|
||
Class<?> windowClass = Class.forName(WINDOW, true, loader);
|
||
|
||
final Object button = buttonClass.getConstructor(String.class, skinClass)
|
||
.newInstance("Read Scene Objects", skin);
|
||
final Object label = labelClass.getConstructor(CharSequence.class, skinClass)
|
||
.newInstance("Click the button to list bones / slots / attachments.", skin);
|
||
|
||
// setBounds 必须各自在自己的类层次上找:Button 和 Label 只有共同的 Actor 祖先,
|
||
// 拿 Button 上找到的方法去 invoke Label 会报"object is not an instance of declaring class"。
|
||
// 先挂在场景根节点上(视口左上角,避开编辑器自己的工具条)。
|
||
findInstanceMethod(button.getClass(), 4, float.class).invoke(button, 60f, 560f, 200f, 32f);
|
||
findInstanceMethod(label.getClass(), 4, float.class).invoke(label, 60f, 300f, 540f, 250f);
|
||
Object root = firstFieldOfType(stage, GROUP);
|
||
addChild(root, button);
|
||
addChild(root, label);
|
||
System.err.println("[panel] 已把按钮和文字面板挂到 Stage(等了 " + waited + " 轮)");
|
||
|
||
wireButton(button, label, loader);
|
||
}
|
||
|
||
/**
|
||
* 把"按钮点击 → 读取场景对象 → 更新文字"这条链路接上。
|
||
*
|
||
* 按钮类的方法名被混淆了,没用名字去猜:Button 里有一对 setChecked(boolean)/isChecked(),
|
||
* 我们把每个 setter 设成 true,再看哪个无参 boolean getter 跟着变 true,就标定了这一对。
|
||
* libGDX 的 Button 自带一个 ClickListener,点一下就会翻转 isChecked,所以轮询它就等于监听点击。
|
||
*/
|
||
public static void wireButton(final Object button, final Object label, final ClassLoader loader)
|
||
throws Exception {
|
||
Class<?> buttonClass = button.getClass();
|
||
Class<?> labelClass = label.getClass();
|
||
// 优先用对照表里定好的名字:Button 上无参方法叫 "String" 的有两个(boolean 和 float),
|
||
// 只按名字取会取错,必须连返回类型一起判。
|
||
Method isChecked = findPinned(buttonClass, Obf.BUTTON_IS_CHECKED, boolean.class);
|
||
Method setChecked = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED, void.class, boolean.class);
|
||
if (isChecked == null || setChecked == null) {
|
||
// 名字对不上(换版本)就退回运行期自校准 —— 只在 Button 自己这一层里试,
|
||
// 不然会扫到继承来的方法,选出一对语义无关的 setter/getter(现象是点击被反复触发)。
|
||
System.err.println("[panel] 对照表里的 isChecked/setChecked 没找到,改用自校准");
|
||
Method[] pair = calibrateChecked(button, buttonClass);
|
||
if (pair == null) {
|
||
System.err.println("[panel] 没能标定 isChecked/setChecked");
|
||
return;
|
||
}
|
||
isChecked = pair[0];
|
||
setChecked = pair[1];
|
||
}
|
||
if (isChecked == null) {
|
||
System.err.println("[panel] 没能标定 isChecked/setChecked");
|
||
return;
|
||
}
|
||
System.err.println("[panel] isChecked=" + isChecked.getName()
|
||
+ " setChecked=" + setChecked.getName());
|
||
|
||
final Method readChecked = isChecked;
|
||
final Method resetChecked = setChecked;
|
||
// setChecked(boolean, boolean) 的第二个参数是"是否派发事件"。复位时用 false,
|
||
// 否则把 isChecked 置回 false 这件事本身又会派发一次事件,表现为按一下读两次。
|
||
final Method resetQuietly = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED_FIRE, void.class,
|
||
boolean.class, boolean.class);
|
||
// 必须连返回类型一起判:Label 上 `D(CharSequence)` 有 void(setText)和 boolean
|
||
// (textEquals)两个,按参数个数找会挑中 textEquals,表现是文字永远不刷新。
|
||
final Method setText = findPinned(labelClass, Obf.LABEL_SET_TEXT, void.class, CharSequence.class);
|
||
if (setText == null) {
|
||
System.err.println("[panel] 没找到 Label.setText,面板文字不会更新");
|
||
return;
|
||
}
|
||
final ClassLoader classLoader = loader;
|
||
Thread poll = new Thread(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
while (true) {
|
||
try {
|
||
if (Boolean.TRUE.equals(readChecked.invoke(button))) {
|
||
if (resetQuietly != null) {
|
||
resetQuietly.invoke(button, Boolean.FALSE, Boolean.FALSE);
|
||
} else {
|
||
resetChecked.invoke(button, Boolean.FALSE);
|
||
}
|
||
System.err.println("[panel] 按钮被触发,开始读取场景对象");
|
||
// Actor 自己记着所在 Stage,直接问它就行
|
||
Object stage = firstFieldOfType(button, STAGE);
|
||
String text = SceneReader.read(classLoader, stage);
|
||
System.err.println("[panel] 读取完成,共 " + text.length() + " 字符");
|
||
setText.invoke(label, text);
|
||
}
|
||
Thread.sleep(80);
|
||
} catch (Throwable error) {
|
||
System.err.println("[panel] 轮询异常: " + error);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}, "spineview-poll");
|
||
poll.setDaemon(true);
|
||
poll.start();
|
||
|
||
// 验证用:--Dspine.simulateClick=1 时自动把按钮"按"一次,方便无人值守地看结果。
|
||
if (System.getProperty("spine.simulateClick") != null) {
|
||
Thread auto = new Thread(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
try {
|
||
Thread.sleep(4000);
|
||
resetChecked.invoke(button, Boolean.TRUE);
|
||
} catch (Throwable error) {
|
||
System.err.println("[panel] 自动触发失败: " + error);
|
||
}
|
||
}
|
||
}, "spineview-autoclick");
|
||
auto.setDaemon(true);
|
||
auto.start();
|
||
}
|
||
}
|
||
|
||
/** 按"名字 + 参数类型 + 返回类型"找方法:混淆后同名方法成堆,少判一个条件就会挑错。 */
|
||
private static Method findPinned(Class<?> owner, String name, Class<?> returnType,
|
||
Class<?>... parameterTypes) {
|
||
for (Method method : owner.getMethods()) {
|
||
if (!method.getName().equals(name)) continue;
|
||
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||
if (returnType != null && method.getReturnType() != returnType) continue;
|
||
return method;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 运行期自校准:把每个 boolean setter 设成 true,看哪个无参 boolean getter 跟着变 true。
|
||
* 只在按钮类自己这一层(含父类)的方法里找 —— 扫到继承链上无关的方法会配出一对假组合,
|
||
* 现象是"点一次按钮被反复触发"。
|
||
*/
|
||
private static Method[] calibrateChecked(Object button, Class<?> buttonClass) throws Exception {
|
||
for (Class<?> type = buttonClass; type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
for (Method setter : type.getDeclaredMethods()) {
|
||
if (setter.getParameterTypes().length != 1
|
||
|| setter.getParameterTypes()[0] != boolean.class
|
||
|| setter.getReturnType() != void.class) continue;
|
||
setter.setAccessible(true);
|
||
setter.invoke(button, Boolean.TRUE);
|
||
for (Method getter : type.getDeclaredMethods()) {
|
||
if (getter.getParameterTypes().length != 0
|
||
|| getter.getReturnType() != boolean.class) continue;
|
||
getter.setAccessible(true);
|
||
if (Boolean.TRUE.equals(getter.invoke(button))) {
|
||
setter.invoke(button, Boolean.FALSE);
|
||
return new Method[] {getter, setter};
|
||
}
|
||
}
|
||
setter.invoke(button, Boolean.FALSE);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 从 Fx.D(静态 Actor)出发拿 Stage:libGDX 里每个 Actor 都记着自己所在的 Stage。 */
|
||
private static Object stageFromActorAnchor(ClassLoader loader) {
|
||
try {
|
||
Class<?> holder = Class.forName("Fx", true, loader);
|
||
Object anchor = null;
|
||
for (Field field : holder.getDeclaredFields()) {
|
||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||
if (!field.getType().getName().equals(ACTOR)) continue;
|
||
field.setAccessible(true);
|
||
anchor = field.get(null);
|
||
if (anchor != null) break;
|
||
}
|
||
if (anchor == null) return null;
|
||
return firstFieldOfType(anchor, STAGE);
|
||
} catch (Throwable error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** Stage 根节点是 do(Group);拿它第一个子节点身上的 Skin(Table 都带 Skin 字段)。 */
|
||
@SuppressWarnings("unused")
|
||
private static Object skinFromRoot(Object root) throws Exception {
|
||
Object children = firstArrayFieldOfType(root, "[L" + ACTOR + ";");
|
||
if (!(children instanceof Object[])) return null;
|
||
for (Object child : (Object[]) children) {
|
||
if (child == null) continue;
|
||
Object skin = firstFieldOfType(child, SKIN);
|
||
if (skin != null) return skin;
|
||
Object deep = skinFromActor(child, 3);
|
||
if (deep != null) return deep;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 顺着 actor 树往下找 Skin,最多 dig 层。 */
|
||
private static Object skinFromActor(Object actor, int dig) throws Exception {
|
||
if (actor == null || dig < 0) return null;
|
||
Object direct = firstFieldOfType(actor, SKIN);
|
||
if (direct != null) return direct;
|
||
if (dig == 0) return null;
|
||
Object children = firstArrayFieldOfType(actor, "[L" + ACTOR + ";");
|
||
if (!(children instanceof Object[])) return null;
|
||
for (Object child : (Object[]) children) {
|
||
Object found = skinFromActor(child, dig - 1);
|
||
if (found != null) return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static Object firstFieldOfType(Object target, String typeName) throws Exception {
|
||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (!field.getType().getName().equals(typeName)) continue;
|
||
field.setAccessible(true);
|
||
Object value = field.get(target);
|
||
if (value != null) return value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 从任意对象出发做有界广度搜索,找第一个 Skin 实例。 */
|
||
private static Object findSkinDeep(Object start, int maxDepth, int maxNodes) {
|
||
java.util.Set<Object> seen = java.util.Collections.newSetFromMap(
|
||
new java.util.IdentityHashMap<Object, Boolean>());
|
||
java.util.ArrayDeque<Object> queue = new java.util.ArrayDeque<Object>();
|
||
java.util.ArrayDeque<Integer> depths = new java.util.ArrayDeque<Integer>();
|
||
queue.add(start);
|
||
depths.add(Integer.valueOf(0));
|
||
int nodes = 0;
|
||
while (!queue.isEmpty() && nodes++ < maxNodes) {
|
||
Object current = queue.poll();
|
||
int depth = depths.poll().intValue();
|
||
if (current == null || !seen.add(current)) continue;
|
||
if (current.getClass().getName().equals(SKIN)) return current;
|
||
if (depth >= maxDepth) continue;
|
||
if (current.getClass().isArray()) {
|
||
if (!current.getClass().getComponentType().isPrimitive()) {
|
||
for (Object item : (Object[]) current) {
|
||
if (item != null) {
|
||
queue.add(item);
|
||
depths.add(Integer.valueOf(depth + 1));
|
||
}
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
String name = current.getClass().getName();
|
||
if (depth > 0 && (name.startsWith("java.") || name.startsWith("javax.")
|
||
|| name.startsWith("sun.") || name.startsWith("org.lwjgl"))) continue;
|
||
for (Class<?> type = current.getClass(); type != null; type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||
if (field.getType().isPrimitive()) continue;
|
||
try {
|
||
field.setAccessible(true);
|
||
Object value = field.get(current);
|
||
if (value != null) {
|
||
queue.add(value);
|
||
depths.add(Integer.valueOf(depth + 1));
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 跳过读不到的
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@SuppressWarnings("unused")
|
||
private static Object firstFieldOfTypeOld(Object target, String typeName) throws Exception {
|
||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (!field.getType().getName().equals(typeName)) continue;
|
||
field.setAccessible(true);
|
||
Object value = field.get(target);
|
||
if (value != null) return value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static Object firstArrayFieldOfType(Object target, String descriptor)
|
||
throws Exception {
|
||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (!field.getType().getName().equals(descriptor)) continue;
|
||
field.setAccessible(true);
|
||
Object value = field.get(target);
|
||
if (value != null) return value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 按"参数个数 + 参数类型"找方法(名字被混淆了,只能这么找)。 */
|
||
/** Table.add(Actor):一个参数、参数能容纳该 actor、返回值非 void。 */
|
||
private static Method findAddMethod(Class<?> tableClass, Class<?> actorClass)
|
||
throws NoSuchMethodException {
|
||
for (Method method : tableClass.getMethods()) {
|
||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||
Class<?>[] types = method.getParameterTypes();
|
||
if (types.length != 1) continue;
|
||
if (!types[0].isAssignableFrom(actorClass)) continue;
|
||
if (method.getReturnType() == void.class) continue;
|
||
return method;
|
||
}
|
||
throw new NoSuchMethodException("找不到 add");
|
||
}
|
||
|
||
/** Table.row():无参、返回值就是表格自己。 */
|
||
private static Method findRowMethod(Class<?> tableClass) throws NoSuchMethodException {
|
||
for (Method method : tableClass.getMethods()) {
|
||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||
if (method.getParameterTypes().length != 0) continue;
|
||
// row() 声明在 Table 上、返回 Table;Window 继承来的,所以不能要求返回值正好是 Window
|
||
Class<?> returns = method.getReturnType();
|
||
if (returns != Object.class && returns.isAssignableFrom(tableClass)) return method;
|
||
}
|
||
throw new NoSuchMethodException("找不到 row");
|
||
}
|
||
|
||
private static Method findInstanceMethod(Class<?> owner, int parameterCount,
|
||
Class<?> parameterType) throws NoSuchMethodException {
|
||
for (Method method : owner.getMethods()) {
|
||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||
Class<?>[] types = method.getParameterTypes();
|
||
if (types.length != parameterCount) continue;
|
||
boolean match = true;
|
||
for (Class<?> type : types) {
|
||
if (parameterType != null && type != parameterType) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) return method;
|
||
}
|
||
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
|
||
}
|
||
|
||
@SuppressWarnings("unused")
|
||
private static Method findMethod(Class<?> owner, int parameterCount, Class<?> parameterType)
|
||
throws NoSuchMethodException {
|
||
for (Method method : owner.getMethods()) {
|
||
Class<?>[] types = method.getParameterTypes();
|
||
if (types.length != parameterCount) continue;
|
||
boolean match = true;
|
||
for (Class<?> type : types) {
|
||
if (parameterType != null && type != parameterType) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) return method;
|
||
}
|
||
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
|
||
}
|
||
|
||
/** Group 的 addActor(Actor):找一个只有一个参数、且参数能容纳该 actor 的 void 方法。 */
|
||
private static void addChild(Object group, Object actor) throws Exception {
|
||
for (Method method : group.getClass().getMethods()) {
|
||
Class<?>[] types = method.getParameterTypes();
|
||
if (types.length != 1) continue;
|
||
if (!types[0].isAssignableFrom(actor.getClass())) continue;
|
||
if (method.getReturnType() != void.class) continue;
|
||
method.invoke(group, actor);
|
||
System.err.println("[panel] 用 " + method.getDeclaringClass().getSimpleName() + "."
|
||
+ method.getName() + " 挂上 " + actor.getClass().getSimpleName());
|
||
return;
|
||
}
|
||
throw new NoSuchMethodException("找不到接纳 " + actor.getClass().getName() + " 的单参方法");
|
||
}
|
||
|
||
/** 全量扫描静态字段(用 Unsafe,不触发类初始化)。留作备用入口。 */
|
||
static Object scanStaticNoInit(ClassLoader loader, List<String> paths, String typeName) {
|
||
try {
|
||
Object unsafe = unsafe();
|
||
Class<?> unsafeClass = unsafe.getClass();
|
||
Method base = unsafeClass.getMethod("staticFieldBase", Field.class);
|
||
Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class);
|
||
Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class);
|
||
for (String path : paths) {
|
||
try (ZipFile zip = new ZipFile(path)) {
|
||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||
while (entries.hasMoreElements()) {
|
||
ZipEntry entry = entries.nextElement();
|
||
String name = entry.getName();
|
||
if (!name.endsWith(".class")) continue;
|
||
Class<?> type;
|
||
try {
|
||
type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'),
|
||
false, loader);
|
||
} catch (Throwable ignored) {
|
||
continue;
|
||
}
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||
if (!field.getType().getName().equals(typeName)) continue;
|
||
try {
|
||
Object fieldBase = base.invoke(unsafe, field);
|
||
long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue();
|
||
Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset));
|
||
if (value != null) return value;
|
||
} catch (Throwable ignored) {
|
||
// 读不到就跳过
|
||
}
|
||
}
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 不是 zip
|
||
}
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 忽略
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static Object unsafe() throws Exception {
|
||
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
|
||
Field field = unsafeClass.getDeclaredField("theUnsafe");
|
||
field.setAccessible(true);
|
||
return field.get(null);
|
||
}
|
||
|
||
/** 把载荷里所有非空的静态字段倒出来(用 Unsafe,不触发类初始化)。 */
|
||
static void dumpStatics(ClassLoader loader, List<String> paths) {
|
||
try {
|
||
Object unsafe = unsafe();
|
||
Class<?> unsafeClass = unsafe.getClass();
|
||
Method base = unsafeClass.getMethod("staticFieldBase", Field.class);
|
||
Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class);
|
||
Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class);
|
||
for (String path : paths) {
|
||
try (ZipFile zip = new ZipFile(path)) {
|
||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||
while (entries.hasMoreElements()) {
|
||
ZipEntry entry = entries.nextElement();
|
||
String name = entry.getName();
|
||
if (!name.endsWith(".class")) continue;
|
||
Class<?> type;
|
||
try {
|
||
type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'),
|
||
false, loader);
|
||
} catch (Throwable ignored) {
|
||
continue;
|
||
}
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||
if (field.getType().isPrimitive()) continue;
|
||
try {
|
||
Object fieldBase = base.invoke(unsafe, field);
|
||
long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue();
|
||
Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset));
|
||
if (value != null) {
|
||
System.err.println("[statics] " + type.getName() + "." + field.getName()
|
||
+ " : " + field.getType().getName() + " = " + value.getClass().getName());
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 跳过
|
||
}
|
||
}
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 不是 zip
|
||
}
|
||
}
|
||
} catch (Throwable error) {
|
||
System.err.println("[statics] 失败: " + error);
|
||
}
|
||
}
|
||
|
||
static List<String> paths(java.net.URLClassLoader loader) {
|
||
List<String> paths = new ArrayList<String>();
|
||
for (java.net.URL url : loader.getURLs()) {
|
||
if (!"file".equals(url.getProtocol())) continue;
|
||
try {
|
||
paths.add(new java.io.File(url.toURI()).getAbsolutePath());
|
||
} catch (Throwable ignored) {
|
||
// 跳过
|
||
}
|
||
}
|
||
return paths;
|
||
}
|
||
}
|