本文作者
作者:Omooo
链接:
https://juejin.cn/post/7199113019446607932
本文由作者授权发布。
引言
所以在一开始呢,就需要先回答这两个问题。
字节码:ASM 操作的目标是字节码,字节码即 JVM 执行的一种指令格式,它既可以来自 Java 代码编译,也可以来自于 Kotlin、Grovvy 代码编译,只要是符合 JVM 规范的字节码即可。 操作:即增删改查。ASM 可以修改已有类的字节码,或者直接生成二进制格式的类。 库:ASM 是一个工具类库,开箱即用。
ASM 依赖库体积小、字节码操作速度快,这也是它有别于其他字节码操作库的原因。
ASM 的应用极其广泛,Java 中 Lambda 表达式调用点 的生成、反射的动态实现,Android 中 BuildConfig 类的生成等,都是通过 ASM 来实现了。http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/e5b1823a897e/src/share/classes/sun/reflect/MethodAccessorGenerator.java
https://github.com/jrodbx/agp-sources/blob/d33681de938188ab30a173b626e6dca6949b13c1/7.2.2/com.android.tools.build/gradle/com/android/build/gradle/tasks/GenerateBuildConfig.kt
除此之外,一些 Android 的质量优化框架 Booster、ByteX、Matrix 等,也都是使用 ASM 来操作字节码。了解这些 APM 库的实现原理,也是需要我们首先熟悉 ASM 的使用。
https://github.com/didi/booster
https://github.com/bytedance/ByteX
https://github.com/Tencent/matrix
这次分享的重点 不在于讲解 ASM 怎样使用,而在于了解 ASM 有什么用。
// Core API
implementation "org.ow2.asm:asm:9.4"
// Tree API
implementation "org.ow2.asm:asm-tree:9.4"
Core Api 是以事件回调的形式访问字节码,这种方式占用内存小、访问速度快;而 Tree Api 是把整个字节码全部读到内存,占用内存大,但 Api 使用简单、能够很好的契合函数式编程。
读取 ArrayList 类
private fun readArrayListByTreeApi() {
// 1. 从类的全限定名、或字节数组、或二进制字节流中读取字节码
val classReader = ClassReader(ArrayList::class.java.canonicalName)
// 2. 以 ClassNode 形式表示字节码
val classNode = ClassNode(Opcodes.ASM9)
classReader.accept(classNode, ClassReader.SKIP_CODE)
classNode.apply {
println("name: $name\n")
// 3. 读取属性
fields.take(2).forEach {
println("field: ${it.name} ${Modifier.toString(it.access)} ${it.desc} ${it.value}")
}
println()
// 4. 读取方法
methods.take(2).forEach {
println("method: ${it.name} ${Modifier.toString(it.access)} ${it.desc}")
}
}
}
// 输出
name: java/util/ArrayList
field: serialVersionUID private static final J 8683452581122892189
field: DEFAULT_CAPACITY private static final I 10
method: <init> public (I)V
method: <init> public ()V
输出特定方法耗时
接下来就是网上举的最多的一个例子,输出方法的耗时。我们以 ASM 来实现类似 hugo 的功能,输出以注解 @MeasureTime 标记的方法的耗时,修改前的 Java 文件如下:
public class MeasureMethodTime {
@MeasureTime
public void measure() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class MeasureMethodTimeTreeClass {
public MeasureMethodTimeTreeClass() {
}
@MeasureTime
public void measure() {
long var3 = System.currentTimeMillis();
long var5;
try {
Thread.sleep(2000L);
} catch (InterruptedException var7) {
RuntimeException var10000 = new RuntimeException(var7);
var5 = System.currentTimeMillis();
System.out.println(var5 - var3);
throw var10000;
}
var5 = System.currentTimeMillis();
System.out.println(var5 - var3);
}
}
classNode.methods.forEach { methodNode ->
// 该方法的注解列表中包含 @MeasureTime
if (methodNode.invisibleAnnotations?.map { it.desc }
?.contains(Type.getDescriptor(MeasureTime::class.java)) == true) {
val localVariablesSize = methodNode.localVariables.size
// 在方法的第一个指令之前插入 System.currentTimeMillis()
val firstInsnNode = methodNode.instructions.first
methodNode.instructions.insertBefore(firstInsnNode, InsnList().apply {
add(MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"))
add(VarInsnNode(Opcodes.LSTORE, localVariablesSize + 1))
})
// 在方法 return 指令之前插入
methodNode.instructions.filter {
it.opcode.isMethodReturn()
}.forEach {
methodNode.instructions.insertBefore(it, InsnList().apply {
add(MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"))
// 注意,Long 是占两个局部变量槽位的,所以这里要较之前 +3,而不是 +2
add(VarInsnNode(Opcodes.LSTORE, localVariablesSize + 3))
add(FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"))
add(VarInsnNode(Opcodes.LLOAD, localVariablesSize + 3))
add(VarInsnNode(Opcodes.LLOAD, localVariablesSize + 1))
add(InsnNode(Opcodes.LSUB))
add(MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V"))
})
}
}
}
删除方法里面的日志语句
public class DeleteLogInvoke {
public String print(String name, int age) {
System.out.println(name);
String result = name + ": " + age;
System.out.println(result);
System.out.println("Delete current line.");
System.out.println("name = " + name + ", age = " + age);
System.out.printf("name: %s%n", name);
System.out.println(String.format("age: %d", age));
return result;
}
}
public class DeleteLogInvokeCoreClass {
public DeleteLogInvokeCoreClass() {
}
public String print(String var1, int var2) {
String var3 = var1 + ": " + var2;
return var3;
}
}
可能又有同学会问为啥不用 Proguard 呢?Proguard 提供了 assumenosideeffects 来 移除日志代码:
https://www.guardsquare.com/manual/configuration/examples#logging
-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("MainActivity", "onCreate: $packageName")
invoke-virtual {p0}, Landroid/content/Context;->getPackageName()Ljava/lang/String;
move-result-object p1
const-string v0, "onCreate: "
invoke-static {v0, p1}, kotlin.jvm.internal.Intrinsics.stringPlus(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
move-result-object p1
const-string v0, "MainActivity"
invoke-static {v0, p1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
invoke-virtual {p0}, Landroid/content/Context;->getPackageName()Ljava/lang/String;
move-result-object p1
const-string v0, "onCreate: "
invoke-static {v0, p1}, Landroidx/constraintlayout/widget/R$id;->kotlin.jvm.internal.Intrinsics.stringPlus(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
其实 Proguard 提供了 assumenoexternalsideeffects、assumenoexternalreturnvalues 来删除这些中间调用,但在 R8 (在 AGP 3.4及其以上,R8 是 默认编译器 了)上已经不支持该配置了:
> Task :app:minifyReleaseWithR8
WARNING: R8: Ignoring option: -assumenoexternalsideeffects
WARNING: R8: Ignoring option: -assumenoexternalreturnvalues
线程重命名
该示例是来源于 Booster - 线程重命名。
https://booster.johnsonlee.io/zh/guide/performance/multithreading-optimization.html#%E7%BA%BF%E7%A8%8B%E9%87%8D%E5%91%BD%E5%90%8D
可能存在低优先级的子线程抢占 CPU,导致主线程 UI 响应能力降低、主线程空等子线程的锁等,过多的资源竞争意味着过多的资源都浪费在了线程调度上。 不可控的线程创建可能导致 OOM。 线程命名默认是以 Thread-{N} 形式命名,不知道线程是在哪个模块哪个类创建的,不利于问题排查。
public class ThreadReName {
public static void main(String[] args) {
// 不带线程名称
new Thread(new InternalRunnable()).start();
// 带线程名称
Thread thread0 = new Thread(new InternalRunnable(), "thread0");
System.out.println("thread0: " + thread0.getName());
thread0.start();
Thread thread1 = new Thread(new InternalRunnable());
// 设置线程名字
thread1.setName("thread1");
System.out.println("thread1: " + thread1.getName());
thread1.start();
}
}
public class ThreadReNameTreeClass {
public ThreadReNameTreeClass() {
}
public static void main(String[] var0) {
(new ShadowThread(new InternalRunnable(), "sample/ThreadReNameTreeClass#main-Thread-0")).start();
ShadowThread var1 = new ShadowThread(new InternalRunnable(), "thread0", "sample/ThreadReNameTreeClass#main-Thread-1");
System.out.println("thread0: " + var1.getName());
var1.start();
ShadowThread var2 = new ShadowThread(new InternalRunnable(), "sample/ThreadReNameTreeClass#main-Thread-2");
var2.setName(ShadowThread.makeThreadName("thread1", "sample/ThreadReNameTreeClass#main-Thread-3"));
System.out.println("thread1: " + var2.getName());
var2.start();
}
}
thread0: sample/ThreadReNameTreeClass#main-Thread-1#thread0
thread1: sample/ThreadReNameTreeClass#main-Thread-3#thread1
public class ReplaceMethodInvoke {
public static void main(String[] args) {
// throw NPE
new Toast().show();
}
}
public class Toast {
private String msg = null;
public void show() {
System.out.println("Toast: " + msg + ", msg.length: " + msg.length());
}
}
序列化检查
该示例是来源于 ByteX - 序列化检查。
https://github.com/bytedance/ByteX/blob/master/serialization-check-plugin/README.md
实现了 Serializable 的类未提供 serialVersionUID 字段。 实现了 Serializable 的类包含非 transient、static 的字段,这些字段并未实现 Serializable 接口。 未实现 Serializable 接口的类,包含 transient、serialVersionUID 字段。 实现了 Serializable 的非静态内部类,它的外层类并未实现 Serializable 接口。
public class SerializationCheck implements Serializable {
private ItemBean1 itemBean1;
private ItemBean2 itemBean2;
private transient ItemBean3 itemBean3;
private String name;
private int age;
static class ItemBean1 {
}
static class ItemBean2 implements Serializable {
}
static class ItemBean3 {
}
}
Attention: Non-serializable field 'itemBean1' in a Serializable class [sample/SerializationCheckCoreClass]
Attention: This [sample/SerializationCheckCoreClass] class is serializable, but does not define a 'serialVersionUID' field.
这种类静态分析的能力,应用场景也比较多。比如检查是否存在调用不存在的字段或方法、隐私合规 Api 调用监测等等。
收集项目中所有的 AAR 包含的 assets 资源名。 在 Transform 阶段,check 以下调用,收集所有已使用的 assets 资源名。
context.getAssets().open("fileName.json")
Static Analysis 静态分析:序列化检查、检查是否存在调用不存在的字段或方法、隐私合规 Api 调用监测等等,都属于该应用范畴。 AOP 面向切面编程:输出特定方法耗时、线上代码覆盖率监测等都属于该应用范畴。 Hook:对原有代码逻辑做增强,一般实现手段有反射和静态/动态代理;线程重命名、点击防手抖、修复第三方库或系统 bug 等都属于该应用范畴。
更多
ASM-Task
https://github.com/Omooo/ASM-Task
Chapter 4. The class File Format
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.1
内部分享的 PPT
https://github.com/Omooo/Android-Notes/tree/master/PPT/ASM%20%E5%BA%94%E7%94%A8%E4%B8%8E%E5%AE%9E%E8%B7%B5
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
点击 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!