最近在做启动优化,我需要打印出所有耗时的方法,或者是打印出所有耗时超过指定时间的方法,为此我写了这个工具,主要用到的技术有自定义gradle插件和asm字节码插桩。
具体效果如下, 插桩前:
private void c() {
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
插桩后:
private void c() {
TimeMonitor.i("com/maove/methodcosttimesample/MainActivity", "c", "()V");
try {
Thread.sleep(80L);
} catch (InterruptedException var2) {
var2.printStackTrace();
}
TimeMonitor.o("com/maove/methodcosttimesample/MainActivity", "c", "()V");
}
所有的插桩都发生在gradle 构建期间,对我们的代码是没有侵入的。
1、自定义grade plugin
1、首先在项目中新建目录buildSrc,buildSrc不需要在 setting.gradle 中进行include,它会被自动识别为一个模块。
2、在buildSrc中新建build.gradlew脚本和src/main/groovy 、src/main/java 、src/main/resources/META-INF/gradle-plugins
buildSrc的build脚本中配置我们编写插件需要的配置
plugins {
id 'groovy'
}
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.1.0'
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
testImplementation 'junit:junit:4.13.2'
}
sourceSets {
main {
groovy {
srcDir '../src/main/groovy'
}
java {
srcDir '../src/main/java'
}
resources {
srcDir '../src/main/resources'
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
自定义插件需要实现Plugin接口
class MethodTimePlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "apply MethodTimePlugin..."
}
}
当我们在app下的build脚本通过插件id来应用我们的插件,其apply方法就会被调用
apply plugin: 'org.maove.methodTimeBeat'
3、我们还可以为插件添加属性扩展
首先自定义一个类
class MethodTimeExt{
public boolean isOpen = true;
public String injectClass
public String methodIn ="i";
public String methodOut ="o";
}
然后创建Extension
project.extensions.create("methodTime", MethodTimeExt.class)
methodTime就是我们自定义的 Extension 了,它里面能配置的属性与类 MethodTimeExt 中的字段是一致的
这样我们就可以在使用插件时候对其进行配置
apply plugin: 'org.maove.methodTimeBeat'
methodTime{
isOpen = true
injectClass = "com/maove/libutil/TimeMonitor"
}
4、注册Transform
Gradle Transform 执行在.class转为.dex阶段,我们可以利用它的执行时机来修改class文件。
class MethodTimeTransform extends Transform{
private Project project
public MethodTimeTransform(Project project){
this.project = project
}
//当前transform的名称
@Override
String getName() {
return MethodTimeTransform.simpleName
}
//告知编译器,当前transform处理的输入类型,通常是class
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
//告知编译器,当前transform处理范围
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
//是否支持增量
@Override
boolean isIncremental() {
return false
}
//编译器会把所有的class收集封装到TransformInvocation中,然后传给这个方法
/**
* 1、遍历所有的输入
* 2、对input进行二次处理
* 3、将input拷贝到目标目录
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
//code...
}
}
在gradle中task是最小可执行单元。transform也会被转为一个task。
gradle生命周期分为3个阶段
插件定义完成只需要在transform获取到所有的class文件,对其利用ASM进行字节码注入就好了。
2、ASM介绍
使用实例:
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = closure.call(classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
ClassReader会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
ClassWriter 和 ClassReader 对应,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码以字节数组的形式输出
- ClassVisitor 访问器,当读取到class中对应信息时,对应方法会被调用。
public class LogMethodTimeClassVisitor extends ClassVisitor {
private MethodConfig methodConfig;
public LogMethodTimeClassVisitor(ClassVisitor classVisitor, MethodConfig methodConfig) {
super(Opcodes.ASM6, classVisitor);
this.methodConfig = methodConfig;
}
@java.lang.Override
public void visit(int version, int access, java.lang.String name, java.lang.String signature, java.lang.String superName, java.lang.String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
methodConfig.setClassName(name);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (methodVisitor==null){
return null;
}
return new LogMethodTimeMethodVisitor(methodVisitor,access,name,descriptor,methodConfig);
}
@java.lang.Override
public AnnotationVisitor visitAnnotation(java.lang.String descriptor, boolean visible) {
return super.visitAnnotation(descriptor, visible);
}
@java.lang.Override
public FieldVisitor visitField(int access, java.lang.String name, java.lang.String descriptor, java.lang.String signature, java.lang.Object value) {
return super.visitField(access, name, descriptor, signature, value);
}
@java.lang.Override
public void visitEnd() {
super.visitEnd();
}
}
关于ClassVisitor还有其他方法我没有列出,常用的也就这几个。
当ClassReader读取到类方法时,就会调用ClassVisitor的visitMethod方法,我们可以构造一个MethodVisitor来参与方法的处理。
通常我们不用直接继承MethodVisitor,使用MethodVisitor的复杂性也更大,我么可以使用它的子类AdviceAdapter
public class LogMethodTimeMethodVisitor extends AdviceAdapter {
protected LogMethodTimeMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor,MethodConfig methodConfig) {
super(Opcodes.ASM6, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
}
@Override
protected void onMethodExit(int opcode) {
}
@java.lang.Override
public void visitEnd() {
super.visitEnd();
}
}
当ClassReader读取到字段时,就会调用ClassVisitor的visitField方法,我们可以构造一个FieldVisitor来参与字段的处理,这里就不细说了。 在实例代码中我只是打印了方法的耗时。我们还可以结合Trace,在每个方法的开始结尾进行插桩,这样可以看到更加详细的信息。
通过plugin和asm结合对class进行插桩,我么可以监控代码耗时、代码替换、埋点、修复三方代码简单bug、性能监控、快速点击处理等等
具体代码:github
|