Android编译时动态插入代码的方案与实践

笔记哥 / 04-18 / 1点赞 / 0评论 / 116阅读
本文同步发布于公众号:移动开发那些事: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`的阶段,通过`ASM`或`javassit`直接修改字节码,插入日志指令;其实现的核心原理为 - 编译流程拦截:通过`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 定义注解 ```csharp 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`文件中添加这两个依赖 ```csharp implementation 'com.google.auto.service:auto-service:1.0.1' implementation 'com.squareup:javapoet:1.13.0' } ``` **注解处理器** ```csharp // 模块名: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 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`的注解标记 ```csharp 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`;通过`ClassVisitor`和`MethodVisitor`在目标方法中插入日志调用; - 优化:可通过`isIncremental` 方法减少重复处理(是否启动增量编译); #### 2.2.1 添加依赖 这里需要新建一个`Gradle`插件项目,在项目的`build.gradle`文件里添加必要的依赖 ```csharp 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仓库的逻辑,可参考公众号的另一篇文章 // 这里只是发布的本地的配置介绍 } ``` 关于如何将插件发布于`maven`仓库的介绍,可参考前面的文章如何高效发布Android AAR包到远程Maven仓库 #### 2.2.2 创建插件 首先在工程在工程`src/main/groovy`目录下创建一个`LogInsertPlugin.groovy ` ```csharp package com.example import org.gradle.api.Plugin import org.gradle.api.Project class LogInsertPlugin implements Plugin { @Override void apply(Project project) { def android = project.extensions.getByName('android') // 注册自定义的Transform类(后面会说明这个类的实现) android.registerTransform(new LogInsertTransform()) } } ``` 在工程`src/main/groovy`目录下创建一个`LogInsertTransform.groovy` ```csharp 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 getInputTypes() { // 指定Transform处理的输入类型,这里是类文件 return TransformManager.CONTENT_CLASS } @Override Set 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`目录下,添加对应的依赖 ```csharp 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` 指定目标方法 - 织入逻辑:在`Before`或`Around` 通知中插入日志代码 - 集成:通过`AspectJ`插件实现编译期织入; #### 2.3.1 添加依赖 在工程的`build.gradle`添加`AspectJ`插件和依赖 ```csharp 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`类,用于拦截方法调用并插入日志 ```csharp 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里面的编译器注解APT的使用及其原理](https://juejin.cn/post/7472306990158594075 "Android里面的编译器注解APT的使用及其原理") - [Transform+ASM牛刀小试](https://segmentfault.com/a/1190000041722223?sort=newest "Transform+ASM牛刀小试") - [看AspectJ在Android中的强势插入](https://blog.csdn.net/eclipsexys/article/details/54425414 "看AspectJ在Android中的强势插入")