0%

自定义Gradle插件(二)

1. 前言

本文在上一篇自定义gradle插件(一)的基础上,通过使用Android提供的transform-API和ApsectJ代码插桩,完成Android统计主线程方法耗时功能。

2. 思路分析

如果使用传统方法,需要在每个方法的开始和结束的地方分别打点,这个工作量太大,显然不可行。而通过AspectJ强大的代码注入功能,可以让我们方便地对方法进行操作。

如何定制自己的transform可以查看参考

对AspectJ不熟悉的可以参考

有了这个基础,就可以打印所有线程方法的执行时间了。对于主线程,只需根据线程ID过滤一下即可。不过使用AspectJ会导致方法数增加一倍,具体为何,后面会提到。

3. 编写代码

定制自己的transform大致可以分为两步:

  1. 编写一个继承Transform的类
  2. 在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
    101
    public class AspectjTransform extends Transform {
    static final String ASPECTJ_RUNTIME = "aspectjrt"

    @Override
    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() {
    @Override
    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
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
public class AspectPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
/**
* Create an aspectj extension so you can use in your build.gradle file.
* e.g.
* <p>
* aspectj {
* includeJarFilter com.XXX.XXX, org.XXX.XXX
* excludeJarFilter
* }
* </p>
*/
project.extensions.create("aspectj", AspectjExtension)
def hasAppPlugin = project.plugins.hasPlugin(AppPlugin)
def hasLibPlugin = project.plugins.hasPlugin(LibraryPlugin)
if (hasAppPlugin || hasLibPlugin) {
AspectjTransform aspectjTransform = new AspectjTransform(project)
project.android.registerTransform(aspectjTransform)
} else {
throw new GradleException("Aspectj: " +
"The 'com.android.application' or 'com.android.library' " +
"plugin is required.")
}
}
}

3.3 编写AspectjWeave进行代码插桩

在步骤3.1中,我们把代码插桩的逻辑放到了AspectjWeave中,主要就是配置参数,并使用Aspect定制的编译器进行代码插桩

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
public class AspectjWeave {
AspectjWeave(Project project) {
this.project = project
}
void weaveCode() {
final def log = project.logger
def args = [
"-showWeaveInfo",
"-encoding", encoding,
"-source", sourceCompatibility,
"-target", targetCompatibility,
"-d", destDir,
"-classpath", classPath.join(File.pathSeparator),
"-bootclasspath", bootClassPath
]
MessageHandler handler = new MessageHandler(true);
Main m = new Main();
m.run(args as String[], handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
throw new GradleException(message.message, message.thrown)
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
m.quit()
}
}

至此,Plugin相关工作已经完成,接下来我们需要根据AspectJ语法对需要计算时间的方法打点计时

3.4 编写计算方法耗时代码

使用Around标识可以截获到原方法,可以在方法执行前后计算时间,最后根据线程Id过滤一下即可。对于Before、Around、After标识的作用可以看步骤2中的推荐教程,这里不再赘述

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
/**
* 对com.netease.demo包名下面的都全部进行代码插桩
*
*/
@Aspect
public class TraceAspect {
private static final String POINTCUT_METHOD =
"execution(* com.netease.demo..*.*(..))";
/**
* 截获原方法,并替换
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(POINTCUT_METHOD)
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
if (Thread.currentThread().getId() == BaseApplication.sUiThreadId) {
Log.e("Aspectj", joinPoint.getSignature().getName() + " 总用时:" + (end-start) + "ms");
}
return result;
}
}

3.5 查看结果

图1

通过上图可以看到onCreate方法执行了29毫秒

  1. 原理分析
    原始代码

图2

编译后的代码

图3

通过对比,我们可以发现AspectJ对于原始代码中的方法,都插入了相应的方法。以onCreate方法为例,
原始onCreate方法中的处理逻辑都挪到了onCreate_aroundBody0()方法中,这样如果想在原始onCreate方法前后做些操作,只需在AjcClosure1类中的run方法前后做即可;如果想替换原始onCreate方法实现,则可以不调用AjcClosure1类中的run方法

因为代码插桩操作是编译时期就进行的,所以除了增加方法数外,对原来业务逻辑的影响可以说是微乎其微的。这正是AspectJ的强大之处!

5. 总结

看了上面的结果你可能想:AspectJ竟能如此强大!既然可以动态替换方法,那岂不是可以作为热补丁修复代码?我可以明确告诉你,这完全可行。当然了,只能是方法级别的代码修复。理论上所有的业务代码都能进行代码插桩,但是有个缺点:方法数会增加一倍。不过有了Mutidex分包方案后,方法数这个问题也就迎刃而解了。这种热补丁修复方案实现简单、兼容性好、而且热补丁代码非常小,基本上(10K以内),后面我会花时间把它整理出来,敬请期待!

6. 源码下载

AspectJDemo

7. 参考

本文中大部分代码都参考了aspectjx项目,特此感谢
aspectjx项目