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 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`的注解标记
```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 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`目录下,添加对应的依赖
```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中的强势插入")
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/2384
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利