Android代码清理插件-Delogger

今天想和大家分享一个在编译时清理Log语句的插件。

问题描述

在平时的开发中我们经常使用安卓的Log方法来打印一些调试信息。由于有很多日志只是在开发时期有需要,所以我们一般通常都会自定义一个Logger,这个Logger只在我们的Debug包中输入日志信息,而在Release包中则不输出,大致的逻辑如下所示:

1
2
3
4
5
6
7
8
9
Logger.i(TAG, "name is:" + name + "and age is: " + age)
class Logger{
pulic static void i(String tag, String msg){
if(!isDebug){
Log.i(tag, msg);
}
}
}

这种模式可以确保在release保重是不会打印调试信息的,但是却仍然存在性能问题,因为在调用Logge的i方法之前要先计算出实参的值,也就是说会执行:

1
"name is:" + name + "and age is: " + age

如果以上Logger的使用发生在onDraw等调用非常频繁的方法中的话,则对性能的影响更为严重。

解决方案

其实有过c/c++开发的同学就会想到,用宏可以完美解决这个问题,但奈何java不支持宏定义。那我们有没有其他的方案来实现呢?笔者一开始想用javassist来实现,可是查了一圈资料之后发现javassist并没有符合我们需求的api。最终确定的方案是,在gradle插件运行java compile任务之前对java源码进行扫描,并删除以”Log.”和”Logger.”开头的行。大致逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void doClean(File source, String[] prefixList){
if(source.isFile()){
def outputFile = new File(source.parentFile, "tmp_${source.name}\$")
for (singleLine in source.readLines()) {
def trimmed = singleLine.trim()
if (!startWith(trimmed, prefixList)) {
outputFile.append(singleLine)
}
outputFile.append("\n") //keep the line number correct
}
outputFile.renameTo(source)
}else{
def children = source.listFiles()
for(file in children){
doClean(file, prefixList)
}
}
}

处理逻辑已经实现了,那么我们在什么时候执行删除操作呢?在安卓的编译过程中有一个compile${variantName}JavaWithJavac,我们可以在该方法之前执行:

1
2
3
4
5
//hook javac compile
Task compileJavaTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
compileJavaTask.doFirst {
doClean()
}

通过以上的代码,我们则可以将源代码中的Logger语句移除了。但是,我们可以直接操作将源代码中的Logger语句移除吗?不行的,我们应该先将源代码复制到一个临时目录中,然后将这些临时目录中的源码中的Logger移除,最后将compile${variantName}JavaWithJavac 任务的输入目录改为这个临时目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//hook javac compile
Task compileJavaTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
compileJavaTask.doFirst {
if(compileJavaTask instanceof AndroidJavaCompile){
def androidJavaCompile = (AndroidJavaCompile) compileJavaTask
def sourceField = SourceTask.class.getDeclaredField("source")
sourceField.setAccessible(true)
def source = (List<DefaultConfigurableFileTree>)sourceField.get(androidJavaCompile)
def iterator = source.iterator()
while(iterator.hasNext()){
def fileTree = iterator.next()
if(fileTree instanceof DefaultConfigurableFileTree) {
if (fileTree.dir.absolutePath.endsWith("src/main/java")) { //replace source java dir
def cleanDir = new File(cleanDirPath)
FileUtils.copyDirectory(fileTree.dir, cleanDir)
doClean(cleanDir, prefixList)
fileTree.from(cleanDir)
}
}
}
}
}

至此,我们便能真正实现我们最初的目的,移除java文件中的Logger了,大家可以尝试移除kotlin文件中的Logger。

总结

文中的源码可以参考Delogger。笔者希望本文中使用的预处理思路可以起到抛砖引玉的作用,大家可以头脑风暴一下这种预处理方式的其他使用场景。