自定义plugin给方法统计时间(插桩)

####1、创建新project

创建一个新的project,默认创建就行,名字叫testPlugin

先大致看一下工程目录如下:

####2、使用buildSrc的方式创建自定义plugin

为什么使用buildSrc,因为理解简单,也可以使用其他方法,比如上传到本地repo
官网说明了如何自定义plugin
https://guides.gradle.org/writing-gradle-plugins/
https://docs.gradle.org/current/userguide/custom_plugins.html

没看懂的看同行的解释,自定义Android Gradle插件的3种方式
https://blog.csdn.net/brycegao321/article/details/82754014

另外:自定义plugin可以groovy,java,kotlin语言编写
需要在build.gradle引入
app plugin: ‘java’
app plugin: ‘groovy’
app plugin: ‘kotlin’

试了一下创建一个androidlib,然后在main后面创建一个groovy文件夹,再写一个build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
*****build.gradle*****
buildscript {
ext.android_gradle_plugin_version = '3.1.3'
repositories {
jcenter()
google()
}
dependencies {
}
}

repositories {
jcenter()
google()
}

apply plugin: 'java'
apply plugin: 'groovy'

dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:$android_gradle_plugin_version"
implementation "com.android.tools.build:gradle-api:$android_gradle_plugin_version"
}

但是结束报错了,一堆红色的log

1
2
3
4
5
6
7
8
9
10
11
block1
buildSrc会出现Plugin with id 'android-library' not found
需要 apply plugin: 'groovy'

block2
buildSrc也出现Duplicate root element
直接创建文件夹,不要创建android library

tip: id写法
buildSrc/src/main/resources/META-INF/gradle-plugins/org.example.greeting.properties
放这个映射文件后就可以用id的写法

正确的创建buildSrc方法

1
2
3
4
5
6
7
1、直接创建buildSrc文件夹,
2、创建src文件夹,
3、创建main文件夹,
4、创建groovy文件夹,在这里面写自定义plugin
5、创建resources文件夹,接着META-INF,接着gradle-plugins,
在文件夹gradle-plugins里面新建一个com.kalengo.customplugin.properties,里面写名称implementation-class=CustomPlugin
这两个名称在app的build.gradle引用plugin的时候是两种写法,一种是包名,一种是id

buildSrc的目录参考

####自定义plugin
自定义plugin需要implements Plugin
覆盖void apply(Project project) 方法

1
2
3
4
5
6
7
8
9
10
可以在方法里面做添加task的操作
//./gradlew -q hello
//测试build.gradle的参数获取
def extension = project.extensions.create('greeting', GreetingPluginExtension)
project.task('hello') {
doLast {
println extension.message
println extension.greeter
}
}
1
2
3
4
也可以定义transform对class进行特殊处理
//自定义transform进行处理
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new CustomTransform(project))

####自定义transform

1
2
3
4
首先需要在自定义plugin的apply方法里面注册transform
//自定义transform进行处理
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new CustomTransform(project))
1
2
3
4
5
6
7
自定义transform要extends Transform ,并实现方法
public abstract String getName();
public abstract Set<ContentType> getInputTypes();
Set<QualifiedContent.ContentType> getInputTypes()
Set<? super QualifiedContent.Scope> getScopes()
boolean isIncremental()
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException

此时如果transform是个空方法,build 后就会报错,
需要在transform加入复制文件的代码,作用是将class复制到dest的目录,也就是说应用自定义transform后需要自己处理复制class文件的流程。
否则的话会出现打出来的包classes.dex是0字节

1
2
3
4
5
6
7
8
9
//不管对class处理不处理,都要copy file
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)

def dest = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)

####对某个注解进行asm方法代码插桩

在自定义transtorm方法里要这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
println('hello transform')
//遍历input
inputs.each { TransformInput input ->
//遍历文件input
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
//过滤,只处理该处理都class
if (name.endsWith(".class") && !name.startsWith("R\$") &&
"R.class" != name && "BuildConfig.class" != name) {
println(name)

ClassReader cr = new ClassReader(file.bytes)
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new MethodTimeClassVisitor(cw)

cr.accept(cv, ClassReader.EXPAND_FRAMES)

byte[] code = cw.toByteArray()

FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}

}
}
//不管对class处理不处理,都要copy file
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
//遍历jar包input
input.jarInputs.each { JarInput jarInput ->
//不管对jar处理不处理,都要copy file
def dest = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}

}

MethodTimeClassVisitor 需要extends ClassVisitor实现visitMethod方法

1
2
protected void onMethodEnter()
protected void onMethodExit(int opcode)

上面这两个方法就是分布在方法前后调用的。

那里面的asm代码如何写呢?

先在项目里新建一个TestAsm.java的文件
写一些方法,比如

1
2
3
4
5
6
7
8
9
10
11
public class TestAsm {
private void methodTimeStart() {
System.out.println("========start=========");
TimeCache.setStartTime("method", System.nanoTime());
}
private void methodTimeDnd() {
TimeCache.setEndTime("method", System.nanoTime());
System.out.println(TimeCache.getCostTime("method"));
System.out.println("========end=========");
}
}

这时要用到bytecode outline神器了,使用结果如下图

然后ide->code->show bytecode outline按钮
在右侧的asm面板选择ASMified的tab,要把生成的asm代码复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
mv = cw.visitMethod(ACC_PRIVATE, "methodTimeStart", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(11, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitLdcInsn("method");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(13, l2);
mv.visitInsn(RETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable("this", "Lcom/kalengo/testplugin/TestAsm;", null, l0, l3, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PRIVATE, "methodTimeDnd", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(15, l0);
mv.visitLdcInsn("method");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(16, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("newFunc");
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(17, l2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(18, l3);
mv.visitInsn(RETURN);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lcom/kalengo/testplugin/TestAsm;", null, l0, l4, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}

注意要去除一些代码,比如RETURN相关的,比如RETURN后面的,比如cw.visitMethod

整理后就变成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
protected void onMethodEnter() {
if (inject) {
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(11, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false);
}
}

@Override
protected void onMethodExit(int opcode) {
if (inject) {
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(15, l0);
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(16, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(17, l2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}

然后就可以build了。在这里可以看到经过插桩后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
TestPlugin/app/build/intermediates/classes/release/com/kalengo/testplugin

public class Logic {

@MethodTime
public void test() {
for (int i = 0;i < 1000;i++) {
System.out.println();
}
}

@MethodTime
public void test2() {
for (int i = 0;i < 100;i++) {
System.out.println();
}
}
}



public class Logic {
public Logic() {
}

@MethodTime
public void test() {
System.out.println("========start=========");
TimeCache.setStartTime("test", System.nanoTime());

for(int i = 0; i < 1000; ++i) {
System.out.println();
}

TimeCache.setEndTime("test", System.nanoTime());
System.out.println(TimeCache.getCostTime("test"));
System.out.println("========end=========");
}

@MethodTime
public void test2() {
System.out.println("========start=========");
TimeCache.setStartTime("test2", System.nanoTime());

for(int i = 0; i < 100; ++i) {
System.out.println();
}

TimeCache.setEndTime("test2", System.nanoTime());
System.out.println(TimeCache.getCostTime("test2"));
System.out.println("========end=========");
}
}

然后就可以assemble了。在这里可以看到经过插桩后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
TestPlugin/app/build/intermediates/transforms/CustomTransform/debug/31/com/kalengo/testplugin


public class Logic {
public Logic() {
}

@MethodTime
public void test() {
System.out.println("========start=========");
TimeCache.setStartTime("test", System.nanoTime());

for(int i = 0; i < 1000; ++i) {
System.out.println();
}

TimeCache.setEndTime("test", System.nanoTime());
System.out.println(TimeCache.getCostTime("test"));
System.out.println("========end=========");
}

@MethodTime
public void test2() {
System.out.println("========start=========");
TimeCache.setStartTime("test2", System.nanoTime());

for(int i = 0; i < 100; ++i) {
System.out.println();
}

TimeCache.setEndTime("test2", System.nanoTime());
System.out.println(TimeCache.getCostTime("test2"));
System.out.println("========end=========");
}
}

至于混淆,那就要反编译去看了,或者可以直接在ide里面点击apk包进行查看。

最后,运行一下,结果出来了。

1
2
04-14 19:50:30.503 4162-4162/com.kalengo.testplugin I/System.out: method: test time 367.292 ms
04-14 19:50:30.503 4162-4162/com.kalengo.testplugin I/System.out: method: test2 time 26.041 ms