写点什么

ASM 插桩 -- 多线程运行监测,2021Android 大厂面试经验分享

用户头像
Android架构
关注
发布于: 10 小时前

//------------重点代码结束------------------


} 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) ----加载第


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


二个变量,即 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;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
ASM插桩--多线程运行监测,2021Android大厂面试经验分享