创建 Gradle Plugin

插件名字就叫 ac_logger,创建 Gradle Plugin Module 的具体步骤请看 Android Gradle 插件开发入门指南(一)

由于 Transform 属于 Android Gradle Plugin 的 API,所以我们的插件需要依赖com.android.tools.build:gradle;我们还需要用到 ASM,所以插件也需要依赖 org.ow2.asm:asm:9.0 具体步骤请看 Android Gradle 插件开发入门指南(二)

创建 Transform 实现类

注意,插件编写支持 groovy 或者 java,使用的语言不同,类的存放位置有所差异:

java - src/main/java/...
groovy - src/main/groovy/...

逻辑比较简单,就直接上代码了,注释写的比较清楚:

publicclassLoggerTransformextendsTransform{
@Override
publicString getName{
// 转换器的名字
return"ac_logger";
}
@Override
publicSet getInputTypes {
// 返回转换器需要消费的数据类型。我们需要处理所有的 class 内容
returnTransformManager.CONTENT_CLASS;
}
@Override
publicSet superQualifiedContent.Scope> getScopes {
// 返回转换器的作用域,即处理范围。我们只处理 Project 里面的类
returnTransformManager.PROJECT_ONLY;
}
@Override
publicbooleanisIncremental{
// 是否支持增量,我们简单点不支持
returnfalse;
}
@Override
publicvoidtransform(TransformInvocation transformInvocation)throwsTransformException, InterruptedException, IOException{
super.transform(transformInvocation);
// TODO 实现转换逻辑
}
}

注册 Transform

publicclassActivityLoggerPluginimplementsPlugin< Project>{
publicvoidapply(Project project){
// Register a transform
def android = project.extensions.getByType(AppExtension)
android.registerTransform( newLoggerTransform)
}
}

遍历所有 .class 文件

Trasnform API 的输入类型有两种,一种是目录,一种是 Jar 文件(对应着三方库),我这里只处理目录输入。我们需要从目录输入中过滤出所有的 .class 文件。

@Override
publicvoid transform(TransformInvocation transformInvocation) throws TransformException,
InterruptedException, IOException {
super.transform(transformInvocation);
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider;
if(outputProvider == null) {
return;
}
// 由于我们不支持增量编译,清空 OutputProvider 的内容
outputProvider.deleteAll;
Collection transformInputs = transformInvocation.getInputs;
transformInputs. forEach(transformInput -> {
// 存在两种转换输入,一种是目录,一种是Jar文件(三方库)
// 处理目录输入
transformInput.getDirectoryInputs. forEach(directoryInput -> {
File directoryInputFile = directoryInput.getFile;
// 找到转化输入中所有的 class 文件,具体逻辑请看 github 代码
List files = filterClassFiles(directoryInputFile);
// TODO 编辑 class 文件,添加日志打印逻辑
// 有输入进来就必须将其输出,否则会出现类缺失的问题,
// 无论是否经过转换,我们都需要将输入目录复制到目标目录
File dest = outputProvider.getContentLocation(directoryInput.getName,
directoryInput.getContentTypes,
directoryInput.getScopes,
Format.DIRECTORY);
try{
FileUtils.copyDirectory(directoryInput.getFile, dest);
} catch(IOException e) {
e.printStackTrace;
}
});
// 处理 jar 输入
transformInput.getJarInputs. forEach(jarInput -> {
// 有输入进来就必须将其输出,否则会出现类缺失的问题,
// 这里我们不需要修改Jar文件,直接将其输出
File jarInputFile = jarInput.getFile;
File dest = outputProvider.getContentLocation(jarInput.getName,
jarInput.getContentTypes,
jarInput.getScopes,
Format.JAR);
try{
FileUtils.copyFile(jarInputFile, dest);
} catch(IOException e) {
e.printStackTrace;
}
});
});
}

这里需要注意的是, Transform API 要求有输入必须有输出,所以即使不需要处理的文件,也需要将其输出到目标目录。

修改 .class 文件

在 ASM 中,提供了一个 ClassReader 类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept 方法,这个方法接受一个继承于 ClassVisitor 抽象类的对象实例作为参数,然后依次调用 ClassVisitor 抽象类的各个方法。

ClassWriter 类编写器,继承于 ClassVisitor,实现了具体的字节码编辑功能。各个 ClassVisitor 通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

我们首先需要实现我们自己的 ClassVisitor,实现里面相关的 visit 函数,来添加日志打印代码:

publicclassActivityClassVisitorextendsClassVisitor{
/**
* Activity 的父类完整类名,这里我们只处理了 AppcompatActivity 的子类,
* 生产中需要处理其他的 Activity 子类
*/
privatestaticfinalString ACTIVITY_SUPER_NAME = "androidx/appcompat/app/AppCompatActivity";
privatestaticfinalString ON_PAUSE = "onPause";
privatestaticfinalString ON_RESUME = "onResume";
privateString superName = null;
privatebooleanvisitedOnPause = false;
privatebooleanvisitedOnResume = false;
publicActivityClassVisitor(ClassVisitor classVisitor){
// Opcodes.ASM9 表示我们使用的 ASM API 的版本,这里使用的最新版本的 API 9
super(Opcodes.ASM9, classVisitor);
}
@Override
publicvoidvisit( intversion, intaccess, String name, String signature, String superName,
String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
// 访问到了具体的类信息,name 当前类的完整类名,superName 表示父类完整类名,access 可访问性
// 排除掉抽象类
if((access & Opcodes.ACC_ABSTRACT) != Opcodes.ACC_ABSTRACT) {
this.superName = superName;
}
}
@Override
publicvoidvisitEnd{
super.visitEnd;
if(superName != null&& superName.equals(ACTIVITY_SUPER_NAME)) {
// 类解析结束,还没有遍历到 onPause 或者 onResume 方法,直接生成完整函数
if(!visitedOnResume) {
visitedOnResume = true;
insertMethodAndLog(ON_RESUME);
}
if(!visitedOnPause) {
visitedOnPause = true;
insertMethodAndLog(ON_PAUSE);
}
}
}
@Override
publicMethodVisitor visitMethod( intaccess, String name, String deor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, deor, signature, exceptions);
if(superName != null&& superName.equals(ACTIVITY_SUPER_NAME)) {
// AppcompatActivity 的子类
if(ON_PAUSE.equals(name)) {
// onPause 方法
visitedOnPause = true;
addLogCodeForMethod(mv, name);
} elseif(ON_RESUME.equals(name)) {
// onResume 方法
visitedOnResume = true;
addLogCodeForMethod(mv, name);
}
}
returnmv;
}
privatevoidaddLogCodeForMethod(MethodVisitor mv, String methodName){
mv.visitLdcInsn( "lenebf");
// 新建一个 StringBuilder 实例
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
// 调用 StringBuilder 的初始化方法
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "V", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
// 获取当前类的 SimpleName
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "Ljava/lang/String;", false);
// 将当前类的 SimpleName 追加进 StringBuilder
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// 将方法名追加进 StringBuilder
mv.visitLdcInsn( ": "+ methodName);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// 调用 StringBuilder 的 toString 方法将 StringBuilder 转化为 String
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "Ljava/lang/String;", false);
// 调用 Log.d 方法
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
privatevoidinsertMethodAndLog(String methodName){
// 创建新方法
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PROTECTED, methodName, "V", null, null);
// 访问新方法填充方法逻辑,
mv.visitCode;
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "androidx/appcompat/app/AppCompatActivity", methodName, "V", false);
mv.visitLdcInsn( "lenebf");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "V", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn( ": "+ methodName);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd;
}
}

然后在 transform 函数中使用我们的 ClassVisitor:

@Override
publicvoidtransform(TransformInvocation transformInvocation)throwsTransformException,
InterruptedException, IOException {
......
transformInputs.forEach(transformInput -> {
// 存在两种转换输入,一种是目录,一种是Jar文件(三方库)
// 处理目录输入
transformInput.getDirectoryInputs.forEach(directoryInput -> {
File directoryInputFile = directoryInput.getFile;
List files = filterClassFiles(directoryInputFile);
for(File file : files) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try{
//对class文件的写入
ClassWriter classWriter = newClassWriter(ClassWriter.COMPUTE_MAXS);
//访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
ClassVisitor classVisitor = newActivityClassVisitor(classWriter);
//对class文件进行读取与解析
inputStream = newFileInputStream(file);
ClassReader classReader = newClassReader(inputStream);
// 依次调用 ClassVisitor接口的各个方法
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
// toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
byte[] bytes = classWriter.toByteArray;
//通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
outputStream = newFileOutputStream(file.getPath);
outputStream.write(bytes);
outputStream.flush;
} catch(Throwable throwable) {
throwable.printStackTrace;
} finally{
closeQuietly(inputStream);
closeQuietly(outputStream);
}
}
......
});
......
});
}

至此,我们的需求就完成了,具体效果如何呢?

检验结果

我们有多种方式检查我们的插件是否完成了我们的需求,最直白的就是直接运行我们的 Demo,看是否有对应的日志输出:


的确如我们所愿,输出了正确的日志信息。我们也可以直接查看 Transform 输出的 .class 文件,位于 app/build/intermediates/transforms 目录:


我们的日志打印代码准确的加进去了。还可以借助 Apktool 工具反编译我们的 Apk 查看里面的代码实现。

本文代码地址:

https://github.com/lenebf/AndroidAOPTutorial

参考资料

[1] 深入理解Android之AOP

[2] AOP 的利器:ASM 3.0 介绍

https://developer.ibm.com/zh/articles/j-lo-asm30/