ASM插桩--多线程运行监测
Posted Android Developer
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASM插桩--多线程运行监测相关的知识,希望对你有一定的参考价值。
最近需要优化App启动的时间,现有代码存在以下问题:
- 线程未复用(使用new Thread\\HandlerThread),创建线程数过多
- 使用HandlerThread,使用后未销毁(Looper一直等待),占用内存
- 提早start线程,却未使用
- 部分业务方过早初始化业务代码(虽然是异步),影响启动时间
由于存在上述问题,需要扫描App从冷启动开始到首页展示出来,中间执行的子线程和主线程执行的情况。
需要监测的数据如下:
- 创建的线程情况,包括数量和使用情况
- 执行的runnable.run、AsyncTask.doInBackground等函数的执行时间
对于创建的线程数,android Studio的profiler->cpu->threads中可以看出,但是我手机只要一打开profiler,就卡的不行,根本没法用。
基于上述需求,使用ASM对线程代码进行插桩。
- 执行时间:计算run、doInBackground等的执行时间很简单,只需要在这些方法前面和最后面加入代码,然后就可以计算时间。
- 统计线程数:由于Thread是系统代码,无法在
Thread.run
方法中进行插桩,也无法在Thread.start
后面插代码,因为像线程池这种情况,也都是系统代码。只能在业务代码中进行插桩。因此考虑在以下代码中插入代码:
- Runnable.run
- AsyncTask.doInBackground
- Callable.call
- Handler.handleMessage、Handler.Callback.handleMessage
- Thread.run
- TimerTask.run
在这些方法都是运行在线程中,在这些方法中可以通过Thread.currentThread()
获取到当前线程数据。 这样已经能覆盖到大部分情况了,因为大部分都是有任务才会创建线程,使用线程和创建线程的时间是挨着的。但是有一种特殊情况就是HandlerThread,他可以先创建,然后Looper一直等待直到有任务才执行,因此如果该looper一直等不到任务,用刚刚的办法就统计不到,因此这种情况要特殊处理。我们要逐行扫描是否有new HandlerThread
的代码,如果扫描到的话,也要统计到线程创建里去。
到这里,基本上讲完我们线程插桩的思路了。那么如何插桩呢?就是使用ASM库,他的原理是在class文件打包到dex之前,使用Gradle中的Transform对class文件进行插桩(具体可以自行查询,这里不细讲)。 我们要利用gradle的插件对代码进行插桩,如何进行插件开发呢?有以下两种方式:
- 本工程中创建buildSrc模块(该模块名字专门用于插件开发)进行开发。
- 独立工程开发
具体可以查看该文章。
本章采用第一种,在demo工程中创建了buildSrc。然后创建一个插件groovy文件:
class ThreadInjectPlugin implements Plugin<Project>
@Override
void apply(Project project)
def android
if (project.plugins.hasPlugin(AppPlugin))
android = project.extensions.getByType(AppExtension)
else
android = project.extensions.getByType(LibraryExtension)
//处理runnable等方法
android.registerTransform(new ThreadRunTransform())
//处理new HandlerThread的
android.registerTransform(new HandlerThreadTransform())
复制代码
重点是这两个Transform,第一个Transform就是扫描到runnable这些,在函数前(onMethodEnter
)和函数结束(onMethodExit
)插入代码。第二个Transform是专门处理new HandlerThread的。
先看ThreadRunTransform:
class ThreadRunTransform extends Transform
@Override
String getName()
return "ThreadTransform"
@Override
Set<QualifiedContent.ContentType> getInputTypes()
return TransformManager.CONTENT_CLASS
@Override
Set<? super QualifiedContent.Scope> getScopes()
return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT)
@Override
boolean isIncremental()
return false
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException
super.transform(transformInvocation)
transformInvocation.inputs.each TransformInput input ->
input.directoryInputs.each DirectoryInput directoryInput ->
transformDirectory(directoryInput, transformInvocation.outputProvider)
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())
//-------------重点代码,拿到class类代码,通过ClassVisitor来扫描,并插入代码
ClassReader classReader = new ClassReader(fileInputStream)
ClassWriter classWriter = new ClassWriter(classReader, 0)
ClassVisitor visitor = new ThreadRunVisitor(className, 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)
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 lambda$main$0(String str)
System.out.println(str);
public class Java8$1 implements Java8.Logger
public Java8$1()
@Override
public void log(String s)
//这里调用 Java8 方法的静态方法
Java8.lambda$main$0(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;
@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);
this.mv.visitMethodInsn(INVOKESTATIC, "com/example/project/AopUtil", "addThread", "(Ljava/lang/Thread;)V", false);
else
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
;
public void getInjectMethods(ClassNode classnode)
for (MethodNode method : classnode.methods)
for (int i = 0; i < method.instructions.size(); i++)
AbstractInsnNode insnNode = method.instructions.get(i);
if (insnNode.getOpcode() == Opcodes.NEW)
TypeInsnNode methodInsnNode = (TypeInsnNode) insnNode;
if (HANDLER_THREAD.equals(methodInsnNode.desc) || THREAD.equals(methodInsnNode.desc))
injectMethods.add(new Method(method.name, method.desc));
else if (insnNode instanceof InvokeDynamicInsnNode)
//判断是否为runnable的lambda表达式
if (((InvokeDynamicInsnNode) insnNode).desc.contains("Ljava/lang/Runnable;")
&& ((InvokeDynamicInsnNode) insnNode).name.equals("run"))
lambdaMethods.add(((Handle) ((InvokeDynamicInsnNode) insnNode).bsmArgs[1]).getName());
private boolean hasLambda(String name)
for (int i = 0; i < lambdaMethods.size(); i++)
if (lambdaMethods.get(i).contains(name))
return true;
return false;
static class Method
String name;
String desc;
public Method(String name, String desc)
this.name = name;
this.desc = desc;
@Override
public boolean equals(Object o)
Method temp = (Method) o;
return name.equals(temp.name) && desc.equals(temp.desc);
复制代码
至此全部代码已经讲完。看下我们AopUtils中的代码:
public class AopUtil
static HashSet<Long> allThread = new HashSet<>();
static HashSet<Long> usedThread = new HashSet<>();
static ConcurrentHashMap<String, Long> threadRunStartTime = new ConcurrentHashMap<>();
public static void runStart()
logThreadUsage(Thread.currentThread(), true);
threadRunStartTime.put(getKey(), System.currentTimeMillis());
private static String getKey()
String stackTrace = Log.getStackTraceString(new Throwable());
stackTrace = stackTrace.split("\\n\\t")[3]; //获取到第几行执行run函数,作为key存储
return stackTrace.substring(0,stackTrace.indexOf("("));
public static void runEnd()
String key = getKey();
Long start = threadRunStartTime.get(key);
if (start != null)
Log.d("ThreadAop-runCost", key + "cost time:" + (System.currentTimeMillis() - start));
threadRunStartTime.remove(key);
public static void addThread(Thread thread)
logThreadUsage(thread, false);
private static void logThreadUsage(Thread thread, boolean isFromRun)
if (thread.getName().equals("main"))
return;
synchronized (AopUtil.class)
if (usedThread == null)
usedThread = new HashSet<>();
if (isFromRun)
Log.d("ThreadAop-used1", "thread is used: " + thread.getId() + ", name is " + thread.getName());
usedThread.add(thread.getId());
if (allThread == null)
allThread = new HashSet<>();
if (allThread.contains(thread.getId()))
Log.d("ThreadAop", Log.getStackTraceString(new Throwable()));
return;
allThread.add(thread.getId());
Log.d("ThreadAop-1", "current size:" + allThread.size() + " add new thread:" + thread.getName() + ", usedThread:" + usedThread.size());
Log.d("ThreadAop", Log.getStackTraceString(new Throwable()));
复制代码
现在插件已经开发完成,在我们工程里引入:
首先在buildSrc模块中,添加如下文件:
然后在app模块中的build.gradle中增加如下代码:
plugins
id 'thread-inject'
或者是apply plugin: 'thread-inject'
复制代码
打包运行app后,就可以看到我们插桩代码的日志输出了。至此代码讲述完毕。
本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
以上是关于ASM插桩--多线程运行监测的主要内容,如果未能解决你的问题,请参考以下文章