Android编译前修改代码的最优Gradle方案咨询
Android编译前修改代码且不改动本地原代码的最优方案
需求背景
要开发一款类似ProGuard的自定义Gradle插件,核心目标是仅修改编译阶段使用的代码,完全保留本地原代码,已掌握反射、自定义Gradle插件、自定义注解相关技术。
现有初步方案分析
当前采用「备份-修改-恢复」的流程:
- 执行
backupTask备份src/main/java目录 - 修改原代码
- 构建完成后恢复备份的原代码
对应的示例代码:
task backupCode(type:Copy){ def src = "src/main/java" def dst = "src/main/backup" from src into dst filter {String line -> line.replace('Hello',"Bello") } dependsOn 'deleteModified' } task restoreCode(type:Copy){ def src = "src/main/backup" def dst = "src/main/java" from src into dst } task deleteModified(type:Delete){ delete "src/main/modified" } preBuild{ dependsOn 'generateModifiedCode' doLast { //restore restoreCode() } }
但这个方案存在明显缺陷:
- 直接操作本地源码目录,若构建中断(手动取消、编译报错)易导致原代码未恢复,引发代码丢失风险
- 备份/恢复过程产生冗余文件,增加项目体积
- 基于文本替换的修改逻辑,面对复杂语法(嵌套函数、多行代码)时极易出错
最优实现方案
方案1:Gradle SourceSet + AST解析 隔离源码
核心思路
复制原源码到临时编译目录,在临时目录中通过AST解析修改代码,再将临时目录加入Gradle的SourceSet作为编译源,构建完成后临时目录随clean任务自动清理,完全隔离本地源码与编译用源码。
核心配置示例
android { sourceSets { main { java { // 添加临时目录为编译源,优先级高于原源码目录(按需调整) srcDirs += "build/generated/sources/customPlugin/java" } } } } // 自定义任务:处理注解并生成修改后的代码到临时目录 task generateModifiedCode(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath // 自定义代码修改工具的主类,负责读取原源码、解析@WrapFunction注解、生成带try-catch的代码 mainClass = "com.your.plugin.CodeModifier" // 传入原源码目录和临时输出目录参数 args = ["src/main/java", "build/generated/sources/customPlugin/java"] } // 让编译前任务依赖代码生成任务 preBuild.dependsOn generateModifiedCode
方案2:Gradle Transform + ASM 字节码增强
核心思路
在编译流程的class文件生成后、打包前,通过自定义Gradle Transform介入,使用ASM框架直接修改字节码,无需触碰本地源码,对开发者完全无感知。
核心代码示例
自定义Transform类
class CustomWrapTransform extends Transform { @Override String getName() { return "customWrapTransform" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException { invocation.inputs.each { input -> // 处理jar包中的class文件 input.jarInputs.each { jarInput -> def destJar = invocation.outputProvider.getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR ) modifyJar(jarInput.file, destJar) } // 处理本地目录中的class文件 input.directoryInputs.each { dirInput -> def destDir = invocation.outputProvider.getContentLocation( dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY ) modifyDirectory(dirInput.file) FileUtils.copyDirectory(dirInput.file, destDir) } } } private void modifyDirectory(File dir) { if (!dir.isDirectory()) return dir.eachFileRecurse { file -> if (file.name.endsWith(".class")) { modifyClassFile(file) } } } private void modifyClassFile(File file) { ClassReader cr = new ClassReader(new FileInputStream(file)) ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS) ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) { @Override MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions) // 检查方法是否带有@WrapFunction注解 AnnotationVisitor av = mv.visitAnnotation("Lcom/your/annotation/WrapFunction;", false) if (av != null) { // 返回自定义MethodVisitor,插入try-catch逻辑 return new TryCatchInjectVisitor(Opcodes.ASM9, mv, access, name, descriptor) } return mv } } cr.accept(cv, ClassReader.SKIP_DEBUG) FileOutputStream fos = new FileOutputStream(file) fos.write(cw.toByteArray()) fos.close() } private void modifyJar(File srcJar, File destJar) { // 处理jar包的逻辑,核心也是读取class文件后调用modifyClassFile修改 JarFile jarFile = new JarFile(srcJar) JarOutputStream jos = new JarOutputStream(new FileOutputStream(destJar)) Enumeration<JarEntry> entries = jarFile.entries() while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement() jos.putNextEntry(new JarEntry(entry.name)) if (entry.name.endsWith(".class")) { byte[] classBytes = jarFile.getInputStream(entry).bytes ClassReader cr = new ClassReader(classBytes) ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS) ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) { // 同modifyClassFile中的注解检查逻辑 } cr.accept(cv, ClassReader.SKIP_DEBUG) jos.write(cw.toByteArray()) } else { jos.write(jarFile.getInputStream(entry).bytes) } jos.closeEntry() } jarFile.close() jos.close() } }
插入try-catch的MethodVisitor实现
public class TryCatchInjectVisitor extends MethodVisitor { private final Label tryStart = new Label(); private final Label tryEnd = new Label(); private final Label catchStart = new Label(); public TryCatchInjectVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { super(api, methodVisitor); } @Override public void visitCode() { // 标记try块开始 mv.visitLabel(tryStart); super.visitCode(); } @Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { // 标记try块结束,进入catch块 mv.visitLabel(tryEnd); mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "Ljava/lang/Exception;"); mv.visitLabel(catchStart); // 存储异常对象到局部变量 mv.visitVarInsn(Opcodes.ASTORE, 1); // 可添加自定义异常处理逻辑(如日志打印) mv.visitInsn(Opcodes.POP); } super.visitInsn(opcode); } }
针对@WrapFunction注解场景的实现建议
场景需求
为带有@WrapFunction注解的函数自动添加try-catch块,编译时生效但不改动本地原代码。
原代码示例:
@WrapFunction override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.e("TAG","Nello World") }
编译后生效的逻辑:
@WrapFunction override fun onCreate(savedInstanceState: Bundle?) { try{ super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.e("TAG","Nello World") }catch (ex:Exception){ // 自定义异常处理逻辑 } }
方案选择
- 若需修改源码级逻辑(如添加代码块、调整变量逻辑),优先用SourceSet + AST解析方案,可读性和可维护性更强
- 若仅需方法增强(如添加try-catch、日志埋点),优先用Gradle Transform + ASM方案,性能更高,无需处理复杂源码语法
内容的提问来源于stack exchange,提问作者Farman Ullah Marwat
相关产品推荐
相关产品推荐

