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 参考

相关推荐
JhonKI1 小时前
【MySQL】存储引擎 - CSV详解
android·数据库·mysql
开开心心_Every1 小时前
手机隐私数据彻底删除工具:回收或弃用手机前防数据恢复
android·windows·python·搜索引擎·智能手机·pdf·音视频
大G哥2 小时前
Kotlin Lambda语法错误修复
android·java·开发语言·kotlin
鸿蒙布道师5 小时前
鸿蒙NEXT开发动画案例2
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
androidwork5 小时前
Kotlin Android工程Mock数据方法总结
android·开发语言·kotlin
xiangxiongfly9157 小时前
Android setContentView()源码分析
android·setcontentview
人间有清欢9 小时前
Android开发补充内容
android·okhttp·rxjava·retrofit·hilt·jetpack compose
人间有清欢9 小时前
Android开发报错解决
android
每次的天空11 小时前
Android学习总结之kotlin协程面试篇
android·学习·kotlin
每次的天空13 小时前
Android学习总结之Binder篇
android·学习·binder