使用 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()VALOAD 0GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayer;INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()VRETURNMAXSTACK = 1MAXLOCALS = 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()VALOAD 0GETFIELD com/unity3d/player/UnityPlayerActivity.mUnityPlayer : Lcom/unity3d/player/UnityPlayerForActivityOrService ;INVOKEVIRTUAL com/unity3d/player/UnityPlayer.destroy ()VRETURNMAXSTACK = 1MAXLOCALS = 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);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {super.visit(version, access, name, signature, superName, interfaces);}@Overridepublic 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();}@Overridepublic 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;}
相关文章:

使用 ASM 修改字段类型,解决闪退问题
问题 我的问题是什么? 在桥接类 UnityBridgeActivity 中处理不同 unity 版本调用 mUnityPlayer.destroy(); 闪退问题。 闪退日志如: 闪退日志说在 UnityBridgeActivity中找不到类型为 UnityPlayer 的属性 mUnityPlayer。 我们知道,Android…...

【python】python社交交友平台系统设计与实现(源码+数据库)【独一无二】
👉博__主👈:米码收割机 👉技__能👈:C/Python语言 👉公众号👈:测试开发自动化【获取源码商业合作】 👉荣__誉👈:阿里云博客专家博主、5…...

Linux 实验报告3-4
(大家好,今天我们来学习Linux的相关知识,大家可以在评论区进行互动答疑哦~加油!💕) 目录 实验三 vi编辑器 一、实验目的 二、实验内容 三、主要实验步骤 实验报告 1.进入 vi。 2.建立一个文件&…...

网络安全之BGP详解
BGP;边界网关协议 使用范围;BGP范围,在AS之间使用的协议。 协议的特点(算法):路径矢量型,没有算法。 协议是否传递网络掩码:传递网络掩码,支持VLSM,CIDR …...
【MySQL精通之路】SQL优化(1)-查询优化(8)-嵌套联接优化
主博客: 【MySQL精通之路】SQL优化(1)-查询优化-CSDN博客 上一篇: 【MySQL精通之路】SQL优化(1)-查询优化(7)-嵌套循环联接-CSDN博客 下一篇: 【MySQL精通之路】SQL优化(1)-查询优化(9)-外部联接优化-CSDN博客 与SQL标准相比,…...
30V降8V、12V、24V3.5A车充降压芯片IC H4112 5V-30V
H4112确实是一款功能强大的异步降压型DC-DC转换器,它具备多种出色的特性和优势,使得它在电源管理领域有着广泛的应用。以下是对H4112主要特性和功能的详细解释: 内置30V耐压MOS: H4112内部集成了30V耐压的MOS管,这有…...

保护共享资源的方法(互斥锁)
我最近开了几个专栏,诚信互三! > |||《算法专栏》::刷题教程来自网站《代码随想录》。||| > |||《C专栏》::记录我学习C的经历,看完你一定会有收获。||| > |||《Linux专栏》࿱…...

树的非递归遍历(层序)
层序是采用队列的方式来遍历的 就比如说上面这颗树 他层序的就是:1 24 356 void LevelOrder(BTNode* root) {Que q;QueueInit(&q);if (root){QueuePush(&q, root);}while (!QueueEmpty(&q)){BTNode* front QueueFront(&q);QueuePop(&q);print…...
解决SpringBoot使用@Transactional进行RestTemplate远程调用导致查询数据记录为null的bug
开启事务过程中,如果远程调用查询当前已经开启但没有提交的事务,就会查不到数据。 示例代码 import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Transactional; import o…...
pl/sql基础语法操作
oracle pl/sql语言(procedural language/sql)是结合了结构化查询与oracle自身过程控制为一体的强大语言。 语法执行块 语法结构: [ declare 可选 声明变量部分--declaration statements (1);]begin --执行部分--executable statements (2)…...
Vue 父组件向子组件传递数据
1、在子组件中,你需要声明你期望从父组件接收哪些props。这可以通过props选项完成,可以是一个数组或对象形式: export default {props: [message],props:{message:String }props: {message: String, // 类型检查count: {type: Nu…...

二十五、openlayers官网示例CustomOverviewMap解析——实现鹰眼地图、预览窗口、小窗窗口地图、旋转控件
官网demo地址: Custom Overview Map 这个示例展示了如何在地图上增加一个小窗窗口的地图并跟随着地图的旋转而旋转视角。 首先加载了一个地图。其中 DragRotateAndZoom是一个交互事件,它可以实现按住shift键鼠标拖拽旋转地图。 const map new Map({int…...
K8S Secret管理之SealedSecrets
1 关于K8S Secret 我们通常将应用程序使用的密码、API密钥保存在K8S Secret中,然后应用去引用。对于这些敏感信息,安全性是至关重要的,而传统的存储方式可能会导致密钥在存储、传输或使用过程中受到威胁,例如在git中明文存储密码…...
Gone框架介绍25 - Redis模块参考文档
文章目录 Redis 参考文档配置项import 和 bury使用分布是缓存 redis.Cache接口定义使用示例 使用分布式锁 redis.Locker接口定义使用示例 操作Key,使用 redis.Key接口定义 使用 Provider 注入 redis 接口使用示例 直接使用redis连接池接口定义使用示例 Redis 参考文…...
SpringBoot前置知识02-spring注解发展史
springboot前置知识01-spring注解发展史 spring1.x spring配置只能通过xml配置文件的方式注入bean,需要根据业务分配配置文件,通过import标签关联。 spring1.2版本出现Transactional注解 <?xml version"1.0" encoding"UTF-8"?> <be…...

C++ TCP发送Socket数据
DEVC需要加入ws2_32库 #include <iostream> #include <winsock2.h>#pragma comment(lib, "ws2_32.lib")void sendData(const char* ip, int port, const char* data) {WSADATA wsaData;SOCKET sockfd;struct sockaddr_in server_addr;// 初始化Winsock…...

鸿蒙HarmonyOS开发中的易混点归纳-持续补充中
相关文章目录 鸿蒙HarmonyOS开发术语全解:小白也能看懂! 文章目录 相关文章目录前言一、build()函数和Builder装饰器?二、自定义组件和系统组件(内置组件)三、组件和页面四、自定义弹窗和其他弹窗总结 前言 一、build…...

ue引擎游戏开发笔记(45)——添加游戏音效
1.需求分析: 截至目前,我们仍然在一个无声的世界游玩游戏,所以有必要为游戏增添一些声音,例如开火声,子弹撞击声等等。 2.操作实现: 1.这是一个较为简单的功能,类似特效的实现方法,…...

202472读书笔记|《首先你要快乐,其次都是其次》——快乐至上,允许一切发生
202472读书笔记|《首先你要快乐,其次都是其次》——快乐至上,允许一切发生 《首先你要快乐,其次都是其次》作者林小仙,挺轻松的小漫画,清新的文字。 生而为人,我很抱歉,大可不必。 生活已经很难…...

8.STL中Vector容器的常见操作(附习题)
目录 1.vector的介绍 2 vector的使用 2.1 vector的定义 2.2 vector iterator 的使用 2.3 vector 空间增长问题 2.3 vector 增删查改 2.4 vector 迭代器失效问题 2.5 vector 在OJ中的使用 1.vector的介绍 vector是表示可变大小数组的序列容器。 就像数组一样࿰…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...

【VLNs篇】07:NavRL—在动态环境中学习安全飞行
项目内容论文标题NavRL: 在动态环境中学习安全飞行 (NavRL: Learning Safe Flight in Dynamic Environments)核心问题解决无人机在包含静态和动态障碍物的复杂环境中进行安全、高效自主导航的挑战,克服传统方法和现有强化学习方法的局限性。核心算法基于近端策略优化…...