使用 ASM 修改字段类型,解决闪退问题

在这里插入图片描述

问题

我的问题是什么?

在桥接类 UnityBridgeActivity 中处理不同 unity 版本调用 mUnityPlayer.destroy(); 闪退问题。

闪退日志如:

在这里插入图片描述

闪退日志说在 UnityBridgeActivity中找不到类型为 UnityPlayer 的属性 mUnityPlayer。


我们知道,Android unity 游戏开发中通常只有一个 Activity 为游戏主页面,且该 Activity 需要继承自 UnityPlayerActivity unity 的实现,内部有一个重要类是 UnityPlayer 通过它进一步调用接口渲染游戏等。

在我们的 sdk 中 Activity 之间的关系大概是这样,也就是在桥接类中是可以访问父类的成员 mUnityPlayer 的。
在这里插入图片描述

接下来介绍的两个不同版本运行表现不一样。

在这里插入图片描述

1、正常版本

UnityPlayerActivity(例如 unity 版本 2021)
这个版本引擎导出的 unity-class.jar 是这样的,UnityPlayerActivity 具有成员 mUnityPlayer 类型是UnityPlayer 类型。

UnityPlayer
从 unity 导出的抽象类,不同 unity 版本可能有不同实现

public abstract class UnityPlayer {
	public void destroy() {
	   //... ...
	}
}
package com.unity3d.player;

public class UnityPlayerActivity extends Activity {
    protected UnityPlayer mUnityPlayer;
}

这个版本打包运行是不会闪退的正常包,我们看 sdk 内调用 destroy() 的大概实现,如下:

UnityBridgeActivity(sdk 桥接类)

public class UnityBridgeActivity extends UnityPlayerActivity {
	private void quitGame(){
		//other ... ...
		mUnityPlayer.destroy();
	}
}

我们查看此段代码的字节码(通过 Android studio 的 ASM 插件可方便查阅)

  • mUnityPlayer 字段名
  • com/unity3d/player/UnityPlayerActivity 字段所在类
  • Lcom/unity3d/player/UnityPlayer; 字段类型
  • INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()V 调用字段的 destroy 方法

这段字节码是正确的,没毛病能运行正常,这段代码被编译、打包进 sdk 被外部使用。

  private invokerUnityV1()V
    ALOAD 0
    GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayer;
    INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

2、闪退版本

UnityPlayerActivity(例如 unity 版本 2022)
这个版本引擎导出的 unity-class.jar 是这样的,UnityPlayerActivity 具有相同成员 mUnityPlayer,但是类型是 UnityPlayerForActivityOrService,与上述不同。

package com.unity3d.player;

public class UnityPlayerActivity extends Activity {
    protected UnityPlayerForActivityOrService mUnityPlayer;
}

UnityPlayerForActivityOrService

package com.unity3d.player;

public class UnityPlayerForActivityOrService extends UnityPlayer{

}

问题出现!

已知,sdk 编译得到的字节码,明确获取的 mUnityPlayer 字段类型是 UnityPlayer;

GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayer;

但是我们发现在 unity 版本 2022,字段 mUnityPlayer 的类型是 UnityPlayerForActivityOrService,这里就和闪退日志对上了,明确抛出这个版本中没有 UnityPlayer 类型的这个字段。

解决

字段名称是不变的,只是类型发生了变化导致的闪退,那么我们是否可以针对特定的版本执行不同的分支,获取正确类型的 mUnityPlayer?

1、如何解决

可以在 Android 打包 transform 过程中定位桥接类 UnityBridgeActivity.class (调用 destroy 方法的地方),并读取作为字节数组输入 ASM,利用 ASM 字节码操作修改 mUnityPlayer 的类型,也就是修改指令。

GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayer;

修改为

GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayerForActivityOrService;

编码、测试

为了方便修改、测试,可以在桥接类单独抽出这个方法作为新的分支,解决 unity 2022 版本的兼容问题。

private void invokerUnityV2022() {
 	super.mUnityPlayer.destroy();
}

通过 ASM 把这个方法修改为这样即可,却别仅在于 mUnityPlayer 的类型。

  private invokerUnityV2022()V
    ALOAD 0
    GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayerForActivityOrService ;
    INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

两个版本、两个分支

收集当前类对应的所有字段

package com.primer.unitybridge;

import java.lang.reflect.Field;
import java.util.List;

/**
 * 创建者:村长
 * 时间:2024/5/22 11:58
 */
public class FieldInfo {
    public List<Field> fields;
    public String className;

    public FieldInfo(String classname, List<Field> fields) {
        this.className = classname;
        this.fields = fields;
    }
}
final String superClassname = getClass().getSuperclass().getName();

public void invokeUnityDestroy(String superClassname) {
        final String unityClassName = "com.unity3d.player.UnityPlayer";
        final String unityClassName2 = "com.unity3d.player.UnityPlayerForActivityOrService";

        try {
            Class<?> clazz = Class.forName(superClassname);
            List<FieldInfo> fieldInfoList = getAllFields(clazz);
            for (FieldInfo fieldInfo : fieldInfoList) {
                if (fieldInfo.fields == null || fieldInfo.fields.size() == 0) {
                    continue;
                }

                for (Field field : fieldInfo.fields) {
                	//获取字段数据类型,不同类型走不同分支
                    String fieldType = field.getType().getName();
                    if (unityClassName.equals(fieldType)) {
                    	//原先版本保留,可以直接这样调用,因为 sdk 内部字节码对应是正确的
                        super.mUnityPlayer.destroy();
                    } else if (unityClassName2.equals(fieldType)) {
                    	//兼容 unity 2022 版本,抽出单独的方法,方便编码、测试
                        invokerUnityV2022();
                    }
                }
            }
        } catch (ClassNotFoundException e) {
            LogUtil.d(TAG, "invokeUnityDestroy 1 =" + e);
        } catch (SecurityException e) {
            LogUtil.d(TAG, "invokeUnityDestroy 2 =" + e);
        } catch (IllegalArgumentException e) {
            LogUtil.d(TAG, "invokeUnityDestroy 3 =" + e);
        }
    }

    private void invokerUnityV2022() {
        super.mUnityPlayer.destroy();
    }

    private static List<FieldInfo> getAllFields(Class<?> clazz) {
        List<FieldInfo> fieldInfos = new ArrayList<>();
        while (clazz != null) {
            List<Field> fields = new ArrayList<>();
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            fieldInfos.add(new FieldInfo(clazz.getName(), fields));

            clazz = clazz.getSuperclass();
        }

        return fieldInfos;
    }

2、ASM

基于 groovy 编写 gradle 插件,干预 class 文件生成,具体看 asm 相关代码。

UnityClassvisitor

package com.primer.plugin.common.asm;

import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.RETURN;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public class UnityClassvisitor extends ClassVisitor {

    public UnityClassvisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                     String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        //找到该类中的 invokerUnityV2022 方法
        if (name.equals("invokerUnityV2022") && descriptor.equals("()V")) {
            // 生成新的方法体
            generateNewBody(mv);
            // 情况原来的方法体
            return null;
        }

        return mv;
    }

    /**
     * 原方法体:
     * private invokerUnityV1()V
     * ALOAD 0
     * GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer :
     * Lcom/unity3d/player/UnityPlayer;
     * INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()V
     * RETURN
     * MAXSTACK = 1
     * MAXLOCALS = 1
     *
     * @param mv
     */
    private void generateNewBody(MethodVisitor mv) {
		//这段代码可以通过 Android studio asm 插件轻松获取
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, "com/unity3d/player/UnityPlayerActivity", "mUnityPlayer",
                "Lcom/unity3d/player/UnityPlayerForActivityOrService;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/unity3d/player/UnityPlayer", "destroy", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

找到需要修改的类,返回修改后的字节数组,在 transform 阶段覆盖原先的字节数组,使其生成新的 class 文件并打包到 jar 中。

  public static byte[] byUnityClassVisitor(byte[] originBytes, String originClassFileName) {
		//找到 UniWbActivity.class 调用 mUnityPlayer 的类
        if (!"com/primer/unitybridge/UniWbActivity.class".equals(originClassFileName)) {
            return null;
        }
        if (originBytes == null || originBytes.length == 0) {
            return null;
        }

        ClassReader classReader = new ClassReader(originBytes);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        UnityClassvisitor clazzVisitor = new UnityClassvisitor(Opcodes.ASM9, classWriter);
        classReader.accept(clazzVisitor, ClassReader.SKIP_DEBUG);
        byte[] bytes = classWriter.toByteArray();
        if (bytes != null || bytes.length != 0) {
            return bytes;
        }
        return null;
    }
  • 28
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值