IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: 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?的字节码插件平台。

Github:GitHub - bytedance/ByteX: ByteX is a bytecode plugin platform based on Android Gradle Transform API and ASM. 字节码插件开发平台

近期在学习研究字节码相关的技术,所以会整理一个系列文章着重分析ByteX各种插件的实现原理和思想。

阅读本文需要初步了解ASM技术,如果不了解也影响不大。

目录

前言

插件介绍

简单的实现方法移除

?[method_call_opt]?源码分析

1.找到目标方法和结束位置

2.找到起始点位置

总结

插件介绍

[method_call_opt]插件属于ByteX的插件之一,顾名思义,旨在用来干净地删除某些方法调用,如Log.d等一些非业务必须的冗余代码,大概能给抖音带来1w2k处修改,数百kb的缩减。

简单的实现方法移除

在正式的分析之前,笔者也曾自己通过ASM实现简单的方法调用移除MethodRemovePlug.java,代码太长就不贴了。

基于Core?API实现,原理是重写MethodVisitor的visitMethodInsn方法,在查找到需要移除的方法位置,提前return,实现抹除本次方法调用的目的。

首先举个栗子:

我们实现一个最简单的方法,打印一行日志:

????public?static?void?tryRemoveMethod(Context?context)?{
????????Log.d("tag","A");
????}

通过AS插件ASM?Bytecode?Viewer,我们可以看到它的字节码是这样的:

?		L0
????LINENUMBER?94?L0
????LDC?"tag"
????LDC?"A"
????INVOKESTATIC?android/util/Log.d?(Ljava/lang/String;Ljava/lang/String;)I
????POP

主要有几个步骤:

3:将"tag"压入操作数栈

4:将"A"压入操作数栈

5:将操作数栈中的两个元素弹出作为参数调用Log.d,结果再压入操作栈中

6:弹栈,方法调用到此为止,栈也随之清空。

在经过MethodRemovePlug.java中字节码处理后,查看它的class文件:

public?static?void?tryRemoveMethod(Context?context)?{
????????String?var10000?=?"tag";
????????String?var10001?=?"A";
????}

可以看到打印Log日志的一行已经没有了,再看一下字节码:

??L0
????LINENUMBER?94?L0
????LDC?"tag"
????LDC?"A"
????POP

可以看到,与之前相比,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. 找到目标方法

  2. 找到终止指令位置startIndex(有返回值即POP位置,没有的话就是方法调用的位置)

  3. 找到起始指令位置endIndex(即向上逆查找到参数定义的起始位置)

  4. 删除startIndex?-?endIndex之间所有的指令,实现完全干净的方法移除。

1.找到目标方法和结束位置

我们要删除一个方法,那么首先要从项目中茫茫多的

具体的实现从MethodCallClassVisitor?92行开始:

int?index?=?instructions.size()?-?1;
List<List<AbstractInsnNode>>?optimizedIns?=?new?ArrayList<>();

instructions是整个方法的字节码节点集合

这里定义了一个index下标,意图从instructions的尾部开始倒序遍历

一个optimizedIns数组,用来储存后面需要删除的指令。

另外还特别定义了一个mParamsStack栈用来存储后续用到的操作符:

????private?final?Stack<Type>?mParamsStack?=?new?Stack<>();

接下来是一个while循环(96~201),大概意思如下:

while?(index?>=?0)?{
	AbstractInsnNode?node?=?instructions.get(index);
	if(node节点是方法调用指令)
??	1.通过开发者传入的owner,name,desc筛选并找到需要过滤的方法
????2.判断该方法的返回值,必须是void或者返回值没有被使用,不然会影响其他业务逻辑,不可进行移除操作
????3.如果是静态方法,将方法的owner(this)类型入栈,否则将方法的desc类型入栈
????int?succeedIndex?=?optimize(index);
????4.通过optimize方法逆向回溯找到方法的起始节点,optimize后文分析
????5.当前index理论上就是结束节点了,如果返回值不是void则index需要+1,因为有返回值的方法后面需要POP弹栈操作,需要把这一行也给囊括进来
		6.这里还需要一个特殊处理,因为在移除一段字节码操作符以后,如果上下相连连续两个FRAME操作符,会导致MethodWriter
????的visitFrame爆一个IllegalStateException,所以这里需要向下查找下一个FRAME节点位置,作为结束节点,然后找到上一个FRAME节点
????位置,并作为新的起始节点。
????7.将起始节点和结束节点之间所有的操作符储存起来。
	index--;
}

需要说明下第2点,如何判断返回值没有被使用?这里是通过观察下一个操作符是否是POP或者POP2来判断的,因为如果其他地方要用到这个返回值,就不会直接弹栈。

第六点,ASM源码中异常的原文:

if?a?frame?is?visited?just?after?another?one,?without?any
?instruction?between?the?two?(unless?this?frame?is?a?Opcodes#F_SAME?frame,?in?which?case?it
?is?silently?ignored).

说明两个frame中一定要有操作符,不能直接相连。

储存起来待删除的节点后,在209行中:

instructions.remove(node);

通过instructions直接遍历删除对应的节点。

2.找到起始点位置

找到起始点位置,即前文提到的optimize(index)方法。该方法是本插件实现的核心,也是最难的部分。

整体代码如下:

if?(mParamsStack.size()?==?0)?{
????return?index;
}
if?(index?<=?0)?{
????throw?new?MethodCallOptException("Can?not?match?the?method?call?params:index="?+?index);
}
final?int?next?=?index?-?1;
final?AbstractInsnNode?node?=?instructions.get(next);
if?(node.getOpcode()?<?0)?{
????if?(node?instanceof?LineNumberNode?||?node?instanceof?LabelNode)?{
????????//LABEL?or?LINENUMBER...
????????//过滤标签和行号
????????return?optimize(next);
????}?else?{
????????//frame
????????return?SKIP_INDEX_FRAME;
????}
}
Type?pop1,?pop2,?pop3,?pop4,?pop5,?pop6;
final?Type?type?=?mParamsStack.peek();
//boolean?byte?char?short?->int
switch?(node.getOpcode())?{
	236~1031行..
??对应各种节点的操作
}

可以看到,主要是通过index节点从结尾向前回溯遍历,在过滤了标签和行号以后,通过一个大型的switch?case囊括了几乎所有的操作符。

可以概括为:

在指令的入栈或者出栈操作时,进行反向操作,并存储在我们自己定义的栈mParamsStack中,因为在前文中已经将方法的执行类型做了入栈操作,所以这里mParamsStack的初始大小是1.

接下来进行递归操作,当mParamsStack的大小成为0是,说明我们已经找到了方法的起始节点。

为了方便理解optimize方法,这里同样以上文的栗子来举例:

	L0
????LINENUMBER?94?L0
????LDC?"tag"
????LDC?"A"
????INVOKESTATIC?android/util/Log.d?(Ljava/lang/String;Ljava/lang/String;)I
????POP
  • 第一次循环

进入到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:

case?Opcodes.LDC:
?????LdcInsnNode?ldcInsnNode?=?(LdcInsnNode)?node;
?????if?..
?????else?if?(ldcInsnNode.cst?instanceof?String?&&?typeOf(Type.getType(String.class),?type))?{
????????????????????//lcd?string
????????????????????pop();
????????????????????return?optimize(next);
?????}?else?...

因为LDC是把元素压栈,所以这里执行反向操作POP,然后递归进入下一次循环

pop之后的mParamsStack元素为[Ljava/lang/String]

  • 第二次循环

通过index--找到上一个节点,还是LDC,对应字节码中的LDC?"tag",继续执行循环1中的操作

pop之后当前mParamsStack为空

  • 跳出循环

此时返回index?=?2,通过查找操作符集合

instructions.get(2)

可知对应的是LDC?"tag"这一行,因此已经找到了目标方法的起始位置。

运行剩下的逻辑,结束以后,可以看到class文件以及变成了:

public?static?void?tryRemoveMethod(Context?context)?{
}

再看看字节码:

原来的方法相关的参数以及调用指令已经被完全删干净了。

总结

总体说来实现的逻辑和思路其实是很清晰的,主要是通过定义一个栈来进行回溯查找操作(其中反转栈的思想有点像leetcode的题用栈实现队列),来实现在尽可能不影响业务逻辑的情况下达到字节码缩减的目的,但是诸多的细节和对所有操作符的兼容处理,也令人感受到原作者的匠心与辛勤。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-08 22:39:08  更:2022-03-08 22:41:23 
 
开发: 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-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码