Android编译时动态插入代码原理与实践

本文同步发布于公众号:移动开发那些事:Android编译时动态插入代码原理与实践

Android开发中,编译时动态插入代码是一种高效,并且对业务逻辑是低侵入性的方案,常用于增加通用的埋点能力,或者插入关键日志,本文以编译时动态插入日志为例来说明如何在Android实现编译时动态插入代码。

1 常见的编译时插入代码方案

  • APT
  • Transform + ASM
  • AspectJ(AOP)

1.1 APT(Annotation Processing Tool)

通过自定义注解标记目标方法/类,然后利用APT在编译期解析注解并生成包含代码逻辑的代码,其核心原理为:

  • 注解标记与解析:
    • 开发者通过自定义注解(如 @DebugLog)标记需要插入日志的方法或类;
    • 编译时,APT 的注解处理器(如继承 AbstractProcessor 的类)会扫描所有被标记的代码元素(如方法、字段);
  • 代码生成与织入
    • 生成辅助类:注解处理器使用代码生成工具(如 JavaPoet)创建新的 Java 类,这些类包含日志逻辑;
    • 逻辑注入:生成的代码会通过静态方法调用或代理模式,在目标方法的前后插入日志语句

优点

  • 代码解耦;
  • 灵活性强:支持复杂的逻辑,如参数获取,耗时统计

更适用于需要生成新类的场景,如ButterKnife,Dagger2,Arouter

1.2 Transform + ASM

基于Gradle Transform ,在编译流程的.class -> dex的阶段,通过ASMjavassit直接修改字节码,插入日志指令;其实现的核心原理为

  • 编译流程拦截:通过Transform API 拦截编译流程
    • 每个Transform是独立的Task,多个Task按注册顺序形成链式的处理
    • 通过getScopes 控制处理范围
    • 通过getInputTypes 指定数据类型,如只处理类文件;
  • ASM字节码操作
    • ClassReader:读取 .class 文件并触发访问事件;
    • ClassWriter:生成修改后的字节码。
    • ClassVisitor/MethodVisitor:在访问类或方法时插入自定义逻辑

优点

  • 兼容性强,支持第三方库和系统类修改;
  • 灵活性高,要可针对特定包,类或方法进行过滤;

适用于需要修改现有代码逻辑(如插入埋点),典型应用场景为:

  • 实现全局埋点
  • 性能监控
  • 权限校验

1.3 AspectJ(AOP)

通过切点Pointcut定义目标方法,在编译期加入(Weaving)日志逻辑,其核心原理为:

  • 编译时织入:在 Java 源码编译为字节码阶段,解析开发者定义的切面(Aspect)和切点(Pointcut),将通知(Advice)代码直接插入目标方法的前后或内部。这种织入方式无需运行时反射,性能损耗低;
  • 切点表达式: 切点表达式决定了哪些方法会被注入代码,通过语法(如 execution(* android.app.Activity.onCreate(..)))定义需要拦截的连接点(Join Point
  • 通知类型(Advice Types)
    • @Before: 在目标方法执行前插入日志(如记录方法调用时间)
    • @After : 在方法正常返回或抛出异常后插入日志
    • @Around : 完全控制方法执行,可自定义前后逻辑

优点与适用场景

  • 无侵入性:无需修改业务代码,通过声明式切面实现日志逻辑与业务解耦,适用于埋点、性能监控等场景;
  • 灵活性与高覆盖率:支持通过复杂表达式匹配任意方法(包括第三方库)
  • 性能高效:编译期静态织入避免运行时反射或动态代理开销
    适用于简单的应用场景,如方法级的日志插入,如果有更复杂的场景,需要使用Transform + ASM来实现更细粒度的控制

2 实战

2.1 APT(Annotation Processing Tool)

使用APT的步骤:

  • 定义注解,用于标记需要插入日志的方法,如DebugLog
  • 自定义注解处理器:继承于AbstractProcessor,并使用JavaPoet生成新类或增加现有类;
  • 注入代码,在生成类中播入日志调用,例如在方法前后添加Log.e语句

2.1.1 定义注解

package com.example.annotation;  import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;  // 模块名:annotation @Retention(RetentionPolicy.CLASS)  // 保留到编译期 @Target(ElementType.METHOD)        // 标记在方法上 public @interface DebugLog { } 

2.1.2 自定义注解处理器

有使用到两个依赖库,需要在项目的build.gradle文件中添加这两个依赖

 implementation 'com.google.auto.service:auto-service:1.0.1'  implementation 'com.squareup:javapoet:1.13.0' } 

注解处理器

// 模块名:compiler @AutoService(Processor.class)  // 自动注册处理器 public class DebugLogProcessor extends AbstractProcessor {     private Filer filer;       // 文件生成器     private Messager messager; // 日志输出工具      @Override     public synchronized void init(ProcessingEnvironment env) {         super.init(env);         filer = env.getFiler();         messager = env.getMessager();     }      @Override     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {         // 遍历所有被 @DebugLog 标记的方法         for (Element element : env.getElementsAnnotatedWith(DebugLog.class)) {             if (element.getKind() != ElementKind.METHOD) {                 continue;             }             ExecutableElement method = (ExecutableElement) element;             // 获取方法所在类信息             TypeElement classElement = (TypeElement) method.getEnclosingElement();             String className = classElement.getSimpleName().toString();             String packageName = elements.getPackageOf(classElement).toString();              // 生成代码             generateLogCode(className, packageName, method);         }         return true;     }      private void generateLogCode(String className, String packageName, ExecutableElement method) {         // 生成类名:原类名 + "$$Logger"         String generatedClassName = className + "$$Logger";                  // 使用 JavaPoet 构建代码(生成新类)         MethodSpec logMethod = MethodSpec.methodBuilder(method.getSimpleName().toString())                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)                 .returns(void.class)                 .addStatement("$T.d("APT", "Method called: $L")",                      ClassName.get("android.util", "Log"), method.getSimpleName())                 .build();          TypeSpec loggerClass = TypeSpec.classBuilder(generatedClassName)                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)                 .addMethod(logMethod)                 .build();          // 写入文件         try {             JavaFile.builder(packageName, loggerClass)                     .build()                     .writeTo(filer);         } catch (IOException e) {             messager.printMessage(Diagnostic.Kind.ERROR, "代码生成失败: " + e);         }     } }   

2.1.3 使用

在某个需要插入日志的方法中使用DebugLog的注解标记

public final class MainActivity { @DebugLog public void loadData(){ 	// 其他业务逻辑 }}  // 编译后,会在build/generated/source/apt 目录下,看到对应的代码 public final class MainActivity$$Logger {     public static void loadData() {         Log.d("APT", "Method called: loadData");     } } 

2.2 Transform + ASM

使用Transform的步骤:

  • 添加依赖;
  • 注册Transform: 创建gradle插件,注册自定义的Transform实现
  • 使用ASM;通过ClassVisitorMethodVisitor在目标方法中插入日志调用;
  • 优化:可通过isIncremental 方法减少重复处理(是否启动增量编译);

2.2.1 添加依赖

这里需要新建一个Gradle插件项目,在项目的build.gradle文件里添加必要的依赖

plugins {     id 'groovy'     id 'maven-publish' }  group 'com.example' version '1.0.0'  repositories {     google()     mavenCentral()     // 如果要发布到自定义的仓库,需要增加自定义仓库地址 }  dependencies { 	// 与gradle构建系统交互     implementation gradleApi()     // 本地的groovy库     implementation localGroovy()     implementation 'org.ow2.asm:asm:9.3'     implementation 'com.android.tools.build:gradle:7.4.2' }  publishing {     // 这里涉及到如何将插件发布于maven仓库的逻辑,可参考公众号的另一篇文章     // 这里只是发布的本地的配置介绍    <!--  publications {         // 定义 Maven 发布的配置         maven(MavenPublication) {             // 发布的组 ID             groupId 'com.example'             // 发布的工件 ID             artifactId 'log-insert-plugin'             // 发布的版本号             version '1.0.0'              // 从 Java 组件获取要发布的内容             from components.java         }     }     repositories {         // 配置本地 Maven 仓库的路径,用于发布插件         maven {             url "$buildDir/repo"         }     } --> }  

关于如何将插件发布于maven仓库的介绍,可参考前面的文章如何高效发布Android AAR包到远程Maven仓库

2.2.2 创建插件

首先在工程在工程src/main/groovy目录下创建一个LogInsertPlugin.groovy

package com.example  import org.gradle.api.Plugin import org.gradle.api.Project  class LogInsertPlugin implements Plugin<Project> {     @Override     void apply(Project project) {         def android = project.extensions.getByName('android')         // 注册自定义的Transform类(后面会说明这个类的实现)         android.registerTransform(new LogInsertTransform())     } } 

在工程src/main/groovy目录下创建一个LogInsertTransform.groovy

package com.example  import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.io.FileUtils import org.objectweb.asm.*  import java.util.jar.JarEntry import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.zip.ZipEntry  class LogInsertTransform extends Transform {      @Override     String getName() {     	// 标识这个transform         return "LogInsertTransform"     }      @Override     Set<QualifiedContent.ContentType> getInputTypes() {     	// 指定Transform处理的输入类型,这里是类文件         return TransformManager.CONTENT_CLASS     }      @Override     Set<? super QualifiedContent.Scope> getScopes() {     	// 指定Transform处理的范围,这里是整个项目         return TransformManager.SCOPE_FULL_PROJECT     }      @Override     boolean isIncremental() {     	// 是否支持增量编译         return true     }      // 处理输入文件并进行转换     @Override     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {         // 遍历 所有的输入文件         transformInvocation.inputs.each { TransformInput input ->             // 目录             input.directoryInputs.each { DirectoryInput directoryInput ->                 // 拿到输出的目录                 def dest = transformInvocation.outputProvider.getContentLocation(                         directoryInput.name,                         directoryInput.contentTypes,                         directoryInput.scopes,                         Format.DIRECTORY                 )                 FileUtils.copyDirectory(directoryInput.file, dest)                 // 处理输出目录中的类文件                 processDirectory(dest)             }             // 处理jar             input.jarInputs.each { JarInput jarInput ->                 def jarName = jarInput.name                 if (jarName.endsWith(".jar")) {                     jarName = jarName.substring(0, jarName.length() - 4)                 }                 def dest = transformInvocation.outputProvider.getContentLocation(                         jarName,                         jarInput.contentTypes,                         jarInput.scopes,                         Format.JAR                 )                 processJar(jarInput.file, dest)             }         }     }      private void processDirectory(File directory) {         if (directory.isDirectory()) {             directory.eachFileRecurse { File file ->                 if (file.name.endsWith('.class')) {                 	// 处理类文件                     processClassFile(file)                 }             }         }     }      // 处理jar 文件     private void processJar(File inputJar, File outputJar) {         def jarFile = new JarFile(inputJar)         def enumeration = jarFile.entries()         def tempFile = File.createTempFile("temp", ".jar")         def jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile))         while (enumeration.hasMoreElements()) {             def jarEntry = enumeration.nextElement()             def inputStream = jarFile.getInputStream(jarEntry)             def zipEntry = new ZipEntry(jarEntry.name)             jarOutputStream.putNextEntry(zipEntry)             // 处理类文件             if (jarEntry.name.endsWith('.class')) {                 def classBytes = processClass(inputStream)                 // 写到输出流                 jarOutputStream.write(classBytes)             } else {                 jarOutputStream.write(inputStream.bytes)             }             jarOutputStream.closeEntry()         }         jarOutputStream.close()         jarFile.close()         FileUtils.copyFile(tempFile, outputJar)         tempFile.delete()     }      // 处理类文件的输入流     private byte[] processClass(InputStream inputStream) {     	// 类读取器         def classReader = new ClassReader(inputStream)         // 类写入器         def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)         // 创建自定义的类访问器         def logClassVisitor = new LogClassVisitor(Opcodes.ASM9, classWriter)         // 让类读取器接受访问器的处理         classReader.accept(logClassVisitor, ClassReader.EXPAND_FRAMES)         // 返回处理后的字节码         return classWriter.toByteArray()     }      // 处理类文件     private void processClassFile(File classFile) {         def fis = new FileInputStream(classFile)         def classBytes = processClass(fis)         fis.close()         def fos = new FileOutputStream(classFile)         fos.write(classBytes)         fos.close()     } }  // 自定义类访问器,用于访问类的各个部分 class LogClassVisitor extends ClassVisitor {      LogClassVisitor(int api, ClassVisitor classVisitor) {         super(api, classVisitor)     }      // 访问方法时调用     @Override     MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {         def methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)         // 创建自定义的方法访问器         return new LogMethodVisitor(Opcodes.ASM9, methodVisitor, name)     } } // 自定义方法访问器,有两种方式: // 1 是继承于MethodVisitor // 2 继承于AdviceAdapter(更简单)  // 方法1 :自定义方法访问器,用于访问方法的各个部分 class LogMethodVisitor extends MethodVisitor {      private String methodName      LogMethodVisitor(int api, MethodVisitor methodVisitor, String methodName) {         super(api, methodVisitor)         this.methodName = methodName     }      // 访问方法代码开始时调用,     @Override     void visitCode() {         super.visitCode()         // 将日志标签压入栈(TAG)         mv.visitLdcInsn("LogInsertPlugin")         // 将日志信息压入栈 (Info)         mv.visitLdcInsn("Entering method: $methodName")         // 调用Log.d 方法         mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)         // 弹出栈项元素         mv.visitInsn(Opcodes.POP)     }      // 访问指令时调用     @Override     void visitInsn(int opcode) {         // 判断是否是返回指令或异常指令         if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {             mv.visitLdcInsn("LogInsertPlugin")             mv.visitLdcInsn("Exiting method: $methodName")             mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)             mv.visitInsn(Opcodes.POP)         }         super.visitInsn(opcode)     } }  // 方法2 :继承自AdviceAdapter,更简洁 public class LogMethodVisitor extends AdviceAdapter {     private String methodName;      protected LogMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {         super(api, mv, access, name, desc);         this.methodName = name;     }      @Override     protected void onMethodEnter() {         // 在方法入口插入日志:Log.d("LogInsertPlugin", "Enter method: " + methodName)         visitLdcInsn("LogInsertPlugin");         visitLdcInsn("Enter method: " + methodName);         visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);         visitInsn(POP); // 丢弃返回值(Log.d返回int)         super.onMethodEnter();     }      @Override     protected void onMethodExit(int opcode) {         // 在方法出口插入日志:Log.d("LogInsertPlugin", "Exit method: " + methodName)         visitLdcInsn("LogInsertPlugin");         visitLdcInsn("Exit method: " + methodName);         visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);         visitInsn(POP);         super.onMethodExit(opcode);     } 

然后就可以把这个插件发布到远程仓库了

2.2.3 使用插件

在需要使用的工程的build.gradle目录下,添加对应的依赖

buildscript {     repositories {         maven {             //前面插件的发布地址         }     }     dependencies {     	//          classpath 'com.example:log-insert-plugin:1.0.0'     } }  // 应用前面做好的插件 apply plugin: 'com.example.LogInsertPlugin' 

2.3 AspectJ(AOP)

使用Transform的步骤:

  • 添加依赖
  • 定义切面:使用Aspect注解标记切面类,通过Pointcut 指定目标方法
  • 织入逻辑:在BeforeAround 通知中插入日志代码
  • 集成:通过AspectJ插件实现编译期织入;

2.3.1 添加依赖

在工程的build.gradle添加AspectJ插件和依赖

buildscript {     dependencies {         classpath 'org.aspectj:aspectjtools:1.9.7'     } } // 插件 apply plugin: 'aspectj'  dependencies {     implementation 'org.aspectj:aspectjrt:1.9.7'     aspectpath 'org.aspectj:aspectjweaver:1.9.7' } 

2.3.2 定义切面

创建一个Aspect类,用于拦截方法调用并插入日志

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.JoinPoint;  @Aspect public class LogAspect { 	private static final String TAG = "LogAspect"; 	// ("execution(* com.demo..*(..))" ,匹配在com.demo包下的所有类的所有方法     // 定义一个切入点,这里表示匹配所有方法     @Pointcut("execution(* *.*(..))")     public void logMethods() {}     // 在方法执行前插入日志     @Before("logMethods()")     public void beforeMethod(JoinPoint joinPoint) {         String methodName = joinPoint.getSignature().getName();         Log.d(TAG, "Before method: " + methodName);     }      // 在方法执行后插入日志     @After("logMethods()")     public void afterMethod(JoinPoint joinPoint) {         String methodName = joinPoint.getSignature().getName();         Log.d(TAG, "After method: " + methodName);     } } 

3 参考

Android编译时动态插入代码原理与实践

发表评论

评论已关闭。

相关文章