1. 前言
本文在上一篇自定义gradle插件(一)的基础上,通过使用Android提供的transform-API和ApsectJ代码插桩,完成Android统计主线程方法耗时功能。
2. 思路分析
如果使用传统方法,需要在每个方法的开始和结束的地方分别打点,这个工作量太大,显然不可行。而通过AspectJ强大的代码注入功能,可以让我们方便地对方法进行操作。
如何定制自己的transform可以查看参考
- Create a Standalone Gradle plugin for Android - part 4 - the transform api
- Android Gradle API
- transform-API
对AspectJ不熟悉的可以参考
有了这个基础,就可以打印所有线程方法的执行时间了。对于主线程,只需根据线程ID过滤一下即可。不过使用AspectJ会导致方法数增加一倍,具体为何,后面会提到。
3. 编写代码
定制自己的transform大致可以分为两步:
- 编写一个继承Transform的类
- 在Plugin的apply方法中调用project.registerTransform(transform)
3.1 编写transform继承类
需要覆写几个方法
- transform() 操作入口
- getName() 名称
- getInputTypes() 输入类型
- getScopes() 作用域
- isIncremental() 是否使用增量编译
先判断配置文件中是否需要代码插桩,若需要,则先进行插桩,否则直接输出源文件即可。部分代码如下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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101public class AspectjTransform extends Transform {
static final String ASPECTJ_RUNTIME = "aspectjrt"
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
// clean
if (!isIncremental) {
outputProvider.deleteAll()
}
if (hasAjcRunTime(inputs)) {
doAspectTransform(outputProvider, inputs)
} else {
doNoThing(outputProvider, inputs)
}
}
private void doAspectTransform(TransformOutputProvider outputProvider,
Collection<TransformInput> inputs) {
AspectjWeave aspectjWeave = new AspectjWeave(project)
// step 1: read config from project.
List<String> includeJar = project.extensions.aspectj.includeJarFilter
List<String> excludeJar = project.extensions.aspectj.excludeJarFilter
for (TransformInput transformInput : inputs) {
for (DirectoryInput directoryInput : transformInput.directoryInputs) {
// put directoryInput.file into Collections.
aspectjWeave.aspectPath << directoryInput.file
aspectjWeave.inPath << directoryInput.file
aspectjWeave.classPath << directoryInput.file
}
for (JarInput jarInput : transformInput.jarInputs) {
aspectjWeave.aspectPath << jarInput.file
aspectjWeave.classPath << jarInput.file
String jarPath =jarInput.file.absolutePath
if (isIncludeFilterMatched(jarPath, includeJar)
&& !isExcludeFilterMatched(jarPath, excludeJar)) {
AspectjLog.i "includeJar---${jarPath}"
aspectjWeave.inPath << jarInput.file
} else {
AspectjLog.i "excludeJar---${jarPath}"
copyJar(outputProvider, jarInput)
}
}
}
// step 2: weave code.
aspectjWeave.weaveCode()
// step 3: merge jars file.
handleOutput(resultDir, outputProvider)
}
private void doNoThing(TransformOutputProvider outputProvider,
Collection<TransformInput> inputs) {
AspectjLog.i "There is no aspectjrt dependencies in classpath, " +
"Have you declare in Dependencies ? "
// 原样输出
inputs.each {
TransformInput input ->
input.directoryInputs.each {
DirectoryInput directoryInput ->
def dest = outputProvider.getContentLocation(
directoryInput.name, directoryInput.contentTypes,
directoryInput.scopes, Format.DIRECTORY)
// just copy dirs
FileUtil.copyDir(directoryInput.file, dest)
}
input.jarInputs.each {
JarInput jarInput ->
def dest = outputProvider.getContentLocation(
jarInput.name, jarInput.contentTypes,
jarInput.scopes, Format.JAR)
// just copy jar files
FileUtil.copyDir(jarInput.file, dest)
}
}
}
private void handleOutput(File resultDir, TransformOutputProvider outputProvider) {
//add class file to aspect result jar
if (resultDir.listFiles().length > 0) {
File jarFile = outputProvider.getContentLocation(
"aspected", outputTypes, scopes, Format.JAR)
FileUtils.mkdirs(jarFile.parentFile)
FileUtils.deleteIfExists(jarFile)
JarMerger jarMerger = new JarMerger(jarFile)
try {
jarMerger.setFilter(new JarMerger.IZipEntryFilter() {
public boolean checkEntry(String archivePath)
throws JarMerger.IZipEntryFilter.ZipAbortException {
return archivePath.endsWith(SdkConstants.DOT_CLASS)
}
});
jarMerger.addFolder(resultDir)
} catch (Exception e) {
throw new TransformException(e)
} finally {
jarMerger.close()
}
}
FileUtils.deleteFolder(resultDir)
}
}
3.2 编写Plugin继承类
Plugin继承类逻辑十分简单,主要就是调用project.android.registerTransform(transform)
1 | public class AspectPlugin implements Plugin<Project> { |
3.3 编写AspectjWeave进行代码插桩
在步骤3.1中,我们把代码插桩的逻辑放到了AspectjWeave中,主要就是配置参数,并使用Aspect定制的编译器进行代码插桩
1 | public class AspectjWeave { |
至此,Plugin相关工作已经完成,接下来我们需要根据AspectJ语法对需要计算时间的方法打点计时
3.4 编写计算方法耗时代码
使用Around标识可以截获到原方法,可以在方法执行前后计算时间,最后根据线程Id过滤一下即可。对于Before、Around、After标识的作用可以看步骤2中的推荐教程,这里不再赘述
1 | /** |
3.5 查看结果
通过上图可以看到onCreate方法执行了29毫秒
- 原理分析
原始代码
编译后的代码
通过对比,我们可以发现AspectJ对于原始代码中的方法,都插入了相应的方法。以onCreate方法为例,
原始onCreate方法中的处理逻辑都挪到了onCreate_aroundBody0()方法中,这样如果想在原始onCreate方法前后做些操作,只需在AjcClosure1类中的run方法前后做即可;如果想替换原始onCreate方法实现,则可以不调用AjcClosure1类中的run方法
因为代码插桩操作是编译时期就进行的,所以除了增加方法数外,对原来业务逻辑的影响可以说是微乎其微的。这正是AspectJ的强大之处!
5. 总结
看了上面的结果你可能想:AspectJ竟能如此强大!既然可以动态替换方法,那岂不是可以作为热补丁修复代码?我可以明确告诉你,这完全可行。当然了,只能是方法级别的代码修复。理论上所有的业务代码都能进行代码插桩,但是有个缺点:方法数会增加一倍。不过有了Mutidex分包方案后,方法数这个问题也就迎刃而解了。这种热补丁修复方案实现简单、兼容性好、而且热补丁代码非常小,基本上(10K以内),后面我会花时间把它整理出来,敬请期待!
6. 源码下载
7. 参考
本文中大部分代码都参考了aspectjx项目,特此感谢
aspectjx项目