最终发现上述字节码中,标红的部分可以删除,删除对应的字节码指令,优化完成 。
使用抖音之前开源的字节码处理框架 ByteX,可以比较方便地获取 Field 的 Class,遍历 Class 的所有方法,以及所有方法的字节码 。我们也已经将此方案进行了开源,有兴趣的同学可以前往查看详细代码:
- https://github.com/bytedance/ByteX/blob/master/field-assign-opt-plugin
public static void click() {clickSelf();Log.i("Logger", "click time:" + System.currentTimeMillis());}
一开始我们尝试了 proguard 的 -assumenosideeffects,这个指令需要我们假定要删除的方法调用没有任何的副作用,并且从程序分析的角度来说这个方法是不会修改堆上某个对象或者栈上方法参数的值 。使用如下配置,proguard 就会在 optimize 阶段帮我们删除 Log 相关的方法调用 。-assumenosideeffects class android.util.Log {public static boolean isLoggable(java.lang.String, int);public static int v(...);public static int i(...);public static int w(...);public static int d(...);public static int e(...);}
但是这种删除并不彻底,它只会删除方法调用指令本身,比如上面的代码中删除 Log.i 方法调用之后,会遗留一个 StringBuilder 对象的创建:public static void click() {clickSelf();new StringBuilder("click time:")).Append(System.currentTimeMillis();}
这个对象的创建我们人为判断的话也是无用的,但是仅从简单的静态程序指令分析的角度并不能判定其是无用的,因此 proguard 并没有将其删除 。既然 assumenosideeffects 删除不干净,我们就自己来实现更加彻底的优化方案 。
优化思路
public static void click();Code:0: invokestatic#6// Method clickSelf:()V3: ldc#7// String Logger5: new#8// class java/lang/StringBuilder8: dup9: invokespecial #9// Method java/lang/StringBuilder."<init>":()V12: ldc#10// String click time:14: invokevirtual #11// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;17: invokestatic#12// Method java/lang/System.currentTimeMillis:()J20: invokevirtual #13// Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;23: invokevirtual #14// Method java/lang/StringBuilder.toString:()Ljava/lang/String;26: invokestatic#2// Method android/util/Log.i:(Ljava/lang/String;Ljava/lang/String;)I29: pop
如上可以看到一行Log.i("Logger", "click time:" + System.currentTimeMillis());在编译完成之后会生成多条指令(从 ldc 到 pop),除了目标方法 Log.i 调用 invokestatic 指令外,还有很多参数创建和入栈指令 。我们要删除相关方法的调用的话,主要是就是找到这行代码所产生的起始指令和终止指令,然后起始到终止位置之间的指令就是我们要删除的全部指令 。
1. 查找终止指令位置
终止指令的查找相对简单,主要就是找到要删除的目标方法调用指令,再根据方法的返回值类型确定是否要包含其后的 pop 或 pop2 指令 。
比如上述代码我们通过遍历就能找到目标方法调用invokestatic #2的位置,因为 Log.i 的返回值类型是 int,终止指令就是下一条的 pop 。
注意 pop 指令的作用是主动让 int 类型的值出栈,也就是不会使用该方法的返回值,只有这种情况下我们才能安全删除目标方法,否则不能删除 。当然如果方法的返回值类型是 void,就不会有 pop 指令 。
2. 查找起始指令位置
起始指令的查找则需要我们对于 java 字码指令设计有基本的认识: java 字节码指令是基于堆栈设计的,每一条字节码指令会对应操作数栈的若干参数的入栈和出栈,并且一个完整独立代码/代码块执行前和执行后操作数栈应该是一样的 。
因此我们找到终止指令后,倒序遍历指令,根据指令的作用进行反向的入栈和出栈操作,当我们的栈中 size 减为 0 时,就找到了起始指令的位置 。注意在入栈时候要记录参数的类型,并在出栈时候做类型匹配校验 。如上面的示例:
- pop 指令效果是单 slot 参数(像 int,float)出栈 ,那我们就在栈存入一个 slot 类型的参数
- invokestatic 要看方法的参数和返回值,正常效果是对应方法的参数从右至左依次出栈,方返回值 int 入栈 。我们就根据方法返回值出栈一个 int 类型的参数,发现栈顶目前是 slot,类型匹配 。然后按照方法参数从左至右依次入栈两个 String 类型的参数 。
推荐阅读
- Android WebRTC 对 AudioRecord 的使用
- TikTok/国际版抖音/海外版抖音2万字干货教程,新手必看
- 作为Android开发,这个知识点一定要知道,官方也改了 2 次
- Android logcat日志封装
- 抖音防烧屏脚本 – Tasker 脚本分享,适用于 OLED 屏幕
- Android开发:使用Kotlin+协程+自定义注解+Retrofit的网络框架
- Android开发:当前项目以Module形式引用别的项目的步骤
- Android恶意木马伪装成游戏APP,通过华为AppGallery分发
- 2021年Android开发新技术动向,未来的路该怎么走?
- Uni-app离线打包Android APK详细教程