ASM 插桩 -- 多线程运行监测,2021Android 大厂面试经验分享
//------------重点代码结束------------------
} catch (Exception e) {
}
}
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
private boolean isAppClass(String className, String path) {
//检查该 class 是不是 app 工程下的包,不包括第三方的包
return className.endsWith(".class") && !className.contains("R$") && !"R.class".equals(className) && !"BuildConfig.class".equals(className);
}
}
复制代码
public class ThreadRunVisitor extends ClassVisitor {
private String className;
private boolean needInject;
public ThreadRunVisitor(String className, ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
this.className = className;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//判断是否需要给该类的方法注入
this.needInject = isInjectClass(className, interfaces, superName);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions);
boolean isInject = this.needInject && isInjectMethod(name, descriptor);
if (!isInject) {
return methodVisitor;
}
return (MethodVisitor) new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) {
@Override
protected void onMethodEnter() {
super.onMethodEnter();
//在方法前插入你想要插入的代码(这个代码在你 app 工程里)
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runStart", "()V", false);
}
@Override
protected void onMethodExit(int opcode) {
//在方法结束时插入你想要插入的代码(这个代码在你 app 工程里)
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runEnd", "()V", false);
}
};
}
public boolean isInjectClass(String className, String[] interfaces, String superName) {
if (className == null)
return false;
//1、支持 runnable 和 android.os.Handler.Callback
if (interfaces != null) {
for (String inter : interfaces) {
if ("java/lang/Runnable".equals(inter)
|| "android/os/Handler$Callback".equals(inter))
return true;
}
}
//2、支持 ExtendsAsyncTask
if ("android/os/AsyncTask".equals(superName)) {
return true;
}
//3、支持 Handler.handleMessage
if ("android/os/Handler".equals(superName)) {
return true;
}
//4、支持 Thread.run
if ("java/lang/Thread".equals(superName)) {
return true;
}
return false;
}
public boolean isInjectMethod(String methodName, String methodDesc) {
if (methodName == null || methodDesc == null)
return false;
//1、runnable 和 thread 的 run 方法
if (methodName.equals("run") && methodDesc.equals("()V"))
return true;
//2、extendedAsyncTask.doInBackground 方法
if (methodName.equals("doInBackground"))
return true;
//3、handler 和 callback 的 handleMessage 方法
if (methodName.equals("handleMessage")) {
return methodDesc.equals("(Landroid/os/Message;)V") || methodDesc.equals("(Landroid/os/Message;)Z");
}
return false;
}
}
复制代码
这里说明一下,在以下代码中,new Runnable 这个会被编译为内部类,在代码扫描时,会创建两个类,分别是 Test.class 和 Test$1.class,并且分别被扫描:
public class Test{
void test(){
new Thread(new Runnable(){
@override
public void run(){
//xxxx
}
}).start();
}
}
复制代码
来看第二个 Transform,由于不清楚是哪个方法中会存在new HandlerThread
,无法像第一个 Transform 那样根据 method 的名字和 desc 来匹配,只能逐行扫描。因此这里需要用到 ClassNode 来扫描,获取到这个类的 methods 列表,然后再拿到每个 method 的 instructions(被编译后执行的指令),从指令中去分析中有没有这个语句。
HandlerThreadTransform 的代码和 ThreadRunTransform 的代码类似,看下重点代码部分:
private void transformDirectory(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def className = file.name
def path = file.path
if (isAppClass(className, path)) {
try {
FileInputStream fileInputStream = new FileInputStream(file.getAbsolutePath())
ClassReader classReader = new ClassReader(fileInputStream)
ClassWriter classWriter = new ClassWriter(classReader, 0)
ClassVisitor visitor = new HandlerThreadVisitor(classReader,classWriter)
classReader.accept(visitor, 0)
byte[] code = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + className)
fos.write(code)
fos.close()
} catch (Exception e) {
e.printStackTrace()
}
}
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
复制代码
首先在 HandlerThreadVisitor 的 visit 方法中,需要先扫描出来该类中需要被插桩的方法列表。 如何判断该方法是否需要被插桩呢?当通过ClassNode
去解析一个 class 文件时,可以通过 classNode.methods 拿到该类的方法列表。遍历列表,拿到MethodNode
,取出每个方法被编译后能被 JVM 执行的 instructions(指令,AbstractInsnNode
)。 每个 AbstractInsnNode 中,都会有一个 int 值:opcode(具体值在org.objectweb.asm.Opcodes
中),这个指令能说明当前执行的内容。举例说明:
加载变量的 opcode 数值:
Opcodes.ILOAD(加载 int 类型的变量)=21
Opcodes.LLOAD(加载 long 类型的变量)=22
Opcodes.FLOAD(加载 float 类型的变量)=23
Opcodes.DLOAD(加载 double 类型的变量)=24
Opcodes.ALOAD(加载引用类型的变量)=25
给某类型的变量赋值
Opcodes.ISTORE = 54、LSTORE = 55、FSTORE = 56、DSTORE = 57、ASTORE = 58
调用某个类的方法:
Opcodes.INVOKESTATIC(调用 static 方法) = 184
Opcodes.INVOKEVIRTUAL(调用实例的方法,非 static)=182
Opcodes.INVOKESPECIAL(调用实例的构造方法,非 static)=183
Opcodes.INVOKEDYNAMIC(lambda 脱糖方法,后续会解释)=186
创建变量
Opcodes.NEW(加载某个类的构造函数)
复制代码
举个代码的例子:
public void test(){
HandlerThread ht = new HandlerThread("xxx");
ht.start();
}
上述这段代码会被编译成如下指令
//new handlerThread("xxx")这一句被翻译成如下指令
TypeInsnNode(opcodes:187, desc:android/os/HandlerThread)
LdcInsnNode(opcodes:18, cst:xx) ----加载常量
MethodInsnNode(opcodes:183, owner:android/os/HandlerThread, name:<init>, desc:(Ljava/lang/String;)V) --调用构造方法
VarInsnNode(opcodes:58, var:1) ----将上面构造方法创建的对象,赋值给第二个变量(第一个是该类的 this,var 是按照变量创建顺序,如果方法有参数,会排在 this 位置后)
//ht.start()被翻译成如下指令
VarInsnNode(opcodes:25, var:1) ----加载第
二个变量,即 thread 变量
MethodInsnNode(opcodes:182, owner:android/os/HandlerThread, name:start, desc:()V) ----调用加载变量的 start 方法。
复制代码
我们想要的监测效果代码效果如下:
public void test(){
HandlerThread ht = new HandlerThread("xx");
ht.start();
AopUtil.addThread(ht); //插入我们自己的监测代码
复制代码
因为 Thread 在初始化是,其 thread id 和 thread name 已经确定。因此当我们检测到 thread.start 方法执行后,在其后面追加如下指令即可:
VarInsnNode(opcodes:25, var:1) ----加载 thread 变量
MethodInsnNode(opcodes:184, owner:com/example/project/AopUtil, name:addThread, desc:(Ljava/lang/Thread;)V)
复制代码
当然我们要在调用 start 方法前,记住编译器加载的是哪个变量,也就是记住 VarInsnNode.opcodes 为 25(OpCodes.ALOAD)时,VarInsnNode.var 的值,方便我们后续加载这个变量去调用我们的插桩方法。
那么问题来了,有时候业务方代码是这么写的:
new HandlerThread("xxx").start();
这段代码被翻译成指令如下:
TypeInsnNode(opcodes:187, desc:android/os/HandlerThread)
LdcInsnNode(opcodes:18, cst:xx) ----加载常量
MethodInsnNode(opcodes:183, owner:android/os/HandlerThread, name:<init>, desc:(Ljava/lang/String;)V) --调用构造方法
MethodInsnNode(opcodes:182, owner:android/os/HandlerThread, name:start, desc:()V) ----调用加载变量的 start 方法。
和刚刚唯一的区别就是这段指令少了 Opcodes.ASTORE 和 Opcodes.ALOAD
复制代码
此时该方法中没有 Thread 的变量,因此我们要增加指令,在 start 指令前,增加创建变量(newLocal)、存储对象(Opcodes.ASTORE)、读取变量(Opcodes.ALOAD)指令即可。因此我们要记住在 start 方法前的指令,若指令直接为调用 init 构造方法的指令,则需要增加刚刚说的指令,若在 start 方法前,调用的是 ALOAD 指令,那我们只需要记住 ALOAD 指令中的 var 参数即可。
看下核心代码(稍后有全部代码):
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions);
boolean injectLambda = hasLambda(name);
boolean isInject = injectMethods.contains(new Method(name, descriptor));
if (!isInject && !injectLambda) {
return methodVisitor;
}
return new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) {
int lastThreadVarIndex = -1; //记住 thread 变量的位置
String lastThreadInstruction; //上一条执行 thread 的 instruction
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
if(isInject) {
if (opcode == ALOAD) {
lastThreadInstruction = VISIT_VAR_INSN_LOAD;
lastThreadVarIndex = var;
}
}
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (isInject) {
if (!THREAD.equals(owner) && !HANDLER_THREAD.equals(owner)) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
return;
}
if (!"<init>".equals(name) && !"start".equals(name)) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
return;
}
//如果走到了 thread.start 或者是 handler thread.start 方法
if ("<init>".equals(name)) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
lastThreadInstruction = VISIT_METHOD_THREAD_INIT;
} else if ("start".equals(name)) {
//先检测之前 thread 是否被存储为本地变量
if (lastThreadInstruction.equals(VISIT_METHOD_THREAD_INIT)) {
//如果 start 的上一句话是 init,则说明 thread 没有被存储为本地变量,那么创建本地变量
Type threadType = Type.getObjectType("java/lang/Thread");
lastThreadVarIndex = newLocal(threadType);
this.mv.visitVarInsn(ASTORE, lastThreadVarIndex);
this.mv.visitVarInsn(ALOAD, lastThreadVarIndex);
}
//继续调用 start 方法
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
if (lastThreadVarIndex > 0) {
//拿到上一个 thread 变量
this.mv.visitVarInsn(ALOAD, lastThreadVarIndex);
//获取 thread id 值
// this.mv.visitMethodInsn(INVOKEVIRTUAL, owner, "getId", "()J", false);
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "addThread", "(Ljava/lang/Thread;)V", false);
}
}
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
复制代码
讲完这里,大部分情况已经实现,接下来将 ASM 对 lambda 表达式的处理。
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
sayHi(s -> System.out.println(s));
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
}
上述代码在编译后变成:
public class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
//这里使用 Logger 的实现类 Java8$1
sayHi(s -> new Java8$1());
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
//方法体中的内容移到这里
static void lambda0(String str){
System.out.println(str);
}
}
public class Java8$1 implements Java8.Logger {
public Java8$1(){
}
@Override
public void log(String s) {
//这里调用 Java8 方法的静态方法
Java8.lambda0(s);
}
}
复制代码
在 main 函数中,会有一个 Opcodes.INVOKEDYNAMIC 的指令(InvokeDynamicInsnNode),查看下该指令中的参数:
首先我们判断该指令中的 desc 是不是包含 java/lang/Runnable,且 name 为 run,如果匹配成功,则获取该方法被脱糖后真正的执行函数(bsmArgs[1]),在该函数中增加我们插桩代码。 查看具体代码:
public class HandlerThreadVisitor extends ClassVisitor {
public static final String HANDLER_THREAD = "android/os/HandlerThread";
public static final String THREAD = "java/lang/Thread";
private final String VISIT_VAR_INSN_LOAD = "visitVarInsn-Load";
private final String VISIT_METHOD_THREAD_INIT = "visitMethod-ThreadInit";
private ClassNode classnode;
ArrayList<Method> injectMethods = new ArrayList<>();
ArrayList<String> lambdaMethods = new ArrayList<>();
public HandlerThreadVisitor(ClassReader classReader, ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
classnode = new ClassNode();
classReader.accept(classnode, 0);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
getInjectMethods(classnode);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = this.cv.visitMethod(access, name, descriptor, signature, exceptions);
boolean injectLambda = hasLambda(name);
boolean isInject = injectMethods.contains(new Method(name, descriptor));
if (!isInject && !injectLambda) {
return methodVisitor;
}
return new AdviceAdapter(groovyjarjarasm.asm.Opcodes.ASM5, methodVisitor, access, name, descriptor) {
int lastThreadVarIndex = -1;
String lastThreadInstruction;
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (injectLambda) {
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runStart", "()V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (injectLambda) {
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "runEnd", "()V", false);
}
}
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
if(isInject) {
if (opcode == ALOAD) {
lastThreadInstruction = VISIT_VAR_INSN_LOAD;
lastThreadVarIndex = var;
评论