使用 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;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/634756.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【webrtc】内置opus解码器的移植

m98 ,不知道是什么版本的opus,之前的交叉编译构建: 【mia】ffmpeg + opus 交叉编译 【mia】ubuntu22.04 : mingw:编译ffmpeg支持opus编解码 看起来是opus是1.3.1 只需要移植libopus和opus的webrtc解码部分即可。 linux构建的windows可运行的opus库 G:\NDDEV\aliply-0.4\C…

同旺科技 FLUKE ADPT 隔离版发布 ---- 2

所需设备&#xff1a; 1、FLUKE ADPT 隔离版 内附链接&#xff1b; 应用于&#xff1a;福禄克Fluke 12E / 15BMax / 17B Max / 101 / 106 / 107 应用于&#xff1a;福禄克Fluke 15B / 17B / 18B 正面&#xff1a; 反面&#xff1a; 侧面&#xff1a; 开孔位置&#xff08;可…

产品推荐|净气型毒害品柜

SAVEST净气型毒害品柜专为各类危险品、有毒化学品、贵重药品及科研标本等既有严格温湿度控制&#xff0c;又有高度安全保障的物品的存储管理而设计&#xff0c;可广泛应用于各个领域。 净气型毒害品柜产品特点 1. SAVEST净气型毒害品柜由双层钢板构造&#xff0c;两层钢板间隔…

2024 中青杯高校数学建模竞赛(A题)数学建模完整思路+完整代码全解全析

你是否在寻找数学建模比赛的突破点&#xff1f;数学建模进阶思路&#xff01; 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024 长三角高校数学建模竞赛&#xff08;A题&#xff09;的全面解析。这个解决方案包不仅包括完整的代码实现&#xff0c;还有详尽的建模过…

温故而知新-Java基础篇【面试复习】

温故而知新-Java基础篇【面试复习】 前言版权推荐温故而知新-基础篇【面试】解决hash冲突的方法try catch finallyException与Error的包结构OOM你遇到过哪些情况&#xff0c;SOF你遇到过哪些情况线程有哪些基本状态?Java IO与 NIO的区别堆和栈的区别对象分配规则notify()和not…

安装ollama并部署大模型并测试

Ollama介绍 项目地址&#xff1a;ollama 官网地址&#xff1a; https://ollama.com 模型仓库&#xff1a;https://ollama.com/library API接口&#xff1a;api接口 Ollama 是一个基于 Go 语言开发的简单易用的本地大语言模型运行框架。可以将其类比为 docker&#xff08;同基…

CCF20220901——如此编码

CCF20220901——如此编码 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; int main() {int n,m,cnt1,a[1000],c[1000]{1};cin>>n>>m;for(int i1;i<n;i){cin>>a[i];cnt*a[i];c[i]cnt;}int b[1000]{0};for(int i1;i<n;i)b[i](…

ESP32学习笔记:WS2812B驱动

WS2812B是一款贴片RGB灯。由于采用了单总线通讯&#xff0c;所以需要特别关注下它的通讯时序。 调试细节&#xff1a; 本来以为会是一个比较简单的调试&#xff0c;结果还是花了很长时间才调试完成。 首先是关于ESP32的纳秒级延时确定&#xff0c;当时按照空指令始终调试不出来…

springboot中使用spring-cloud-starter-openfeign遇到的问题及解决参考

声明&#xff1a;本文使用的spring boot 版本是2.7.12 在springboot中使用spring-cloud-starter-openfeign遇到的一些问题&#xff1a; Caused by: java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata java.…

webpack打包配置项

webpack打包配置项 在config.js 中 module.exports {publicPath: process.env.NODE_ENV production ? / : /, //静态资源目录outputDir: dist, //打包名称assetsDir: static,//静态资源&#xff0c;目录devServer: {port: port,open: false,overlay: {warnings: false,erro…

如何远程连接默认端口?

远程连接是指通过网络实现两个或多个计算机之间的连接和通信。在进行远程连接时&#xff0c;使用的端口号是一个重要的参数。端口号是计算机上正在运行的特定应用程序的标识符。每个应用程序都会监听一个或多个特定的端口号&#xff0c;以便接收来自其他计算机的连接请求&#…

Docker(四) 文件和网络

1 Dockerfile 1.1 什么是Dockerfile Dockerfile是一个文本文件&#xff0c;包含一系列命令&#xff0c;这些命令用于在 Docker 镜像中自动执行操作。Dockerfile 定义了如何构建 Docker 镜像的步骤和所需的操作。 Dockerfile 中包含的命令可以设置和定制容器的环境&#xff0c;…

满足a==1a==2

网上看到的一道JS面试题&#xff0c;觉得很有意思 觉得很有意思的原因是&#xff0c;这个式子乍看之下是有些反常识的。“a1&&a2”&#xff0c;它的意思似乎是“a在等于1的同时又等于2”&#xff0c;这时我们的第一反应可能就是不成立&#xff0c;一个变量怎么可能同时…

win10编译openssl

环境 Win10 64位 VS2022 openssl 3.3.0 nasm NASM version 2.16.01 compiled on Dec 21 2022 perl strawberry-5.38.2.2环境变量设置 perl加入到环境变量&#xff0c;略过nasm加入到环境变量vs的nmake加入到环境变量我的nmake位置如下&#xff1a; C:\Program…

kubeadm部署k8s v1.28

一、主机准备 主机硬件配置说明 作用IP地址操作系统配置k8s-master01192.168.136.55openEuler-22.03-LTS-SP12颗CPU 4G内存 50G硬盘k8s-node01192.168.136.56openEuler-22.03-LTS-SP12颗CPU 4G内存 50G硬盘k8s-node02192.168.136.57openEuler-22.03-LTS-SP12颗CPU 4G内存 50G…

Gitee在已有项目基础上创建仓库中遇到的问题和解决

问题一&#xff1a;fatal: remote origin already exists 解释&#xff1a;当前仓库添加了一个名为"origin"的远程仓库配置&#xff0c;此时输入 git remote add origin https://xxx就会提示上面的内容。 解决方案1:移除旧的origin git remote remove origin 解决方案…

QTextEdit 控件上显示信息:

目录 1. 使用 append 方法: 2. 使用 setPlainText 方法 3.例子&#xff1a; 1. 使用 append 方法: 如果你希望在 QTextEdit 控件上追加显示新的信息&#xff0c;可以使用 append 方法。例如&#xff0c;当你想要追加一行新的日志信息&#xff1a; self.text_edit.append(&…

金融信贷风控基础知识

一、所谓风控(What && Why) 所谓风控&#xff0c;可以拆解从2个方面看&#xff0c;即 风险和控制 风险(what) 风险 这里狭隘的特指互联网产品中存在的风险点&#xff0c;例如 账户风险 垃圾注册账号账号被泄露盗用 交易支付风险 刷单&#xff1a;为提升卖家店铺人气…

大模型难落地?聊聊大模型在智能财务应用的正确打开方式

大模型难落地&#xff1f;No&#xff0c;是你还不够了解它&#xff01; &#xff08;全文4989字&#xff0c;阅读约需10分钟&#xff09; 这两天&#xff0c;大模型几乎成了WAIC&#xff08;世界人工智能大会&#xff09;的唯一主题。大会上&#xff0c;各家企业的大模型悉数…

深度学习之基于Tensorflow模版匹配智慧停车计费系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 随着城市化进程的加快&#xff0c;停车难问题日益凸显。传统的停车管理方式已经无法满足现代社会的需…