| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android字节码框架ByteX [method_call_opt] 源码分析 -> 正文阅读 |
|
[移动开发]Android字节码框架ByteX [method_call_opt] 源码分析 |
前言ByteX?是字节团队推出的一个基于?Gradle?Transform?Api?和?ASM?的字节码插件平台。 近期在学习研究字节码相关的技术,所以会整理一个系列文章着重分析ByteX各种插件的实现原理和思想。 阅读本文需要初步了解ASM技术,如果不了解也影响不大。 目录 插件介绍[method_call_opt]插件属于ByteX的插件之一,顾名思义,旨在用来干净地删除某些方法调用,如Log.d等一些非业务必须的冗余代码,大概能给抖音带来1w2k处修改,数百kb的缩减。 简单的实现方法移除在正式的分析之前,笔者也曾自己通过ASM实现简单的方法调用移除MethodRemovePlug.java,代码太长就不贴了。 基于Core?API实现,原理是重写MethodVisitor的visitMethodInsn方法,在查找到需要移除的方法位置,提前return,实现抹除本次方法调用的目的。 首先举个栗子: 我们实现一个最简单的方法,打印一行日志:
通过AS插件ASM?Bytecode?Viewer,我们可以看到它的字节码是这样的:
主要有几个步骤: 3:将"tag"压入操作数栈 4:将"A"压入操作数栈 5:将操作数栈中的两个元素弹出作为参数调用Log.d,结果再压入操作栈中 6:弹栈,方法调用到此为止,栈也随之清空。 在经过MethodRemovePlug.java中字节码处理后,查看它的class文件:
可以看到打印Log日志的一行已经没有了,再看一下字节码:
可以看到,与之前相比,INVOKESTATIC?android/util/Log.d?(Ljava/lang/String;Ljava/lang/String;)I?这一句已经被移除掉了,基本实现了对Log.d方法的移除。 但是这样做有一个最大的问题就是,参数定义的操作还留存着,并没有被删掉,依然很占大小。 ?[method_call_opt]?源码分析主要实现源码在MethodCallClassVisitor中实现,代码上千行这里就不贴出来了。 MethodCallClassVisitor是ClassVisitor的子类,重写visitMethod方法将MethodVisitor替换为MethodCallOptMethodVisitor。 MethodCallOptMethodVisitor继承自MethodNode,MethodNode是ASM中的Tree?api,比起Core?api,抽象出了类和方法节点,更加适合做插桩操作。 总体思想可以概括为通过以下几个步骤
1.找到目标方法和结束位置我们要删除一个方法,那么首先要从项目中茫茫多的 具体的实现从MethodCallClassVisitor?92行开始:
instructions是整个方法的字节码节点集合 这里定义了一个index下标,意图从instructions的尾部开始倒序遍历 一个optimizedIns数组,用来储存后面需要删除的指令。 另外还特别定义了一个mParamsStack栈用来存储后续用到的操作符:
接下来是一个while循环(96~201),大概意思如下:
需要说明下第2点,如何判断返回值没有被使用?这里是通过观察下一个操作符是否是POP或者POP2来判断的,因为如果其他地方要用到这个返回值,就不会直接弹栈。 第六点,ASM源码中异常的原文:
说明两个frame中一定要有操作符,不能直接相连。 储存起来待删除的节点后,在209行中:
通过instructions直接遍历删除对应的节点。 2.找到起始点位置找到起始点位置,即前文提到的optimize(index)方法。该方法是本插件实现的核心,也是最难的部分。 整体代码如下:
可以看到,主要是通过index节点从结尾向前回溯遍历,在过滤了标签和行号以后,通过一个大型的switch?case囊括了几乎所有的操作符。 可以概括为: 在指令的入栈或者出栈操作时,进行反向操作,并存储在我们自己定义的栈mParamsStack中,因为在前文中已经将方法的执行类型做了入栈操作,所以这里mParamsStack的初始大小是1. 接下来进行递归操作,当mParamsStack的大小成为0是,说明我们已经找到了方法的起始节点。 为了方便理解optimize方法,这里同样以上文的栗子来举例:
进入到optimize方法入口,当前mParamsStack元素为[Ljava/lang/String,Ljava/lang/String] 当前下标index为12,其对应字节码操作为INVOKESTATIC?android/util/Log.d?(Ljava/lang/String;Ljava/lang/String;)I 这个时候找到上一个Node,通过node.getOpcode()可知对应的是LDC,是字节码中的?LDC?"A" 执行case:
因为LDC是把元素压栈,所以这里执行反向操作POP,然后递归进入下一次循环 pop之后的mParamsStack元素为[Ljava/lang/String]
通过index--找到上一个节点,还是LDC,对应字节码中的LDC?"tag",继续执行循环1中的操作 pop之后当前mParamsStack为空
此时返回index?=?2,通过查找操作符集合
可知对应的是LDC?"tag"这一行,因此已经找到了目标方法的起始位置。 运行剩下的逻辑,结束以后,可以看到class文件以及变成了:
再看看字节码: 原来的方法相关的参数以及调用指令已经被完全删干净了。 总结总体说来实现的逻辑和思路其实是很清晰的,主要是通过定义一个栈来进行回溯查找操作(其中反转栈的思想有点像leetcode的题用栈实现队列),来实现在尽可能不影响业务逻辑的情况下达到字节码缩减的目的,但是诸多的细节和对所有操作符的兼容处理,也令人感受到原作者的匠心与辛勤。 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 17:02:14- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |