目录
- 1、Gradle插件
- 2、Transform
- 3、ASM
- 4、应用-防止快速点击的插件
1、Gradle插件
1.1、Gradle插件是什么?
Gradle插件打包了可重用的构建逻辑,可以适用不同的项目和构建。
1.2、自定义Gradle插件的流程
- (1)、新建一个 Android Library项目,然后除了主目录删除主目录中的所有文件;
- (2)、main目录下建立groovy目录和 resources目录,groovy目录用于写插件逻辑, resources目录下用于声明自定义的插件;
- (3)、书写插件的方法就是,写一个类实现Plugin类,并实现其apply方法,在apply方法中完成插件逻辑;
- (4)、在resources目录下(建立/META-INF/gradle-plugins目录,并)建立一个(plugin.)properties的文件,在里面声明自定义的插件。这个properties文件的名称是我们应用插件时使用的名称。
1.3、Gradle插件应用流程
- (5)、使用uploadArchives将插件上传的maven库。
- (6)、依赖路径,使用apply plugin应用插件。
2、Transform API
2.1、Transform API是什么
Transform用于在编译打包的.class文件到.dex文件流程中,去转换.class文件。 目前 jarMerge、proguard、multi-dex、Instant-Run都已经换成 Transform 实现。
2.2、如何注册一个自定的Transform
public class SingleClickHunterPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension appExtension = project.getExtensions().getByType(AppExtension);
appExtension.registerTransform(new SingleClickHunterTransform(project), Collections.EMPTY_LIST);
}
}
在自定义插件的apply方法中,获取module对应的project的AppExtension,然后通过其registerTransform方法注册一个自定义的Transform。
注册之后,在编译流程中会通过TaskManager#createPostCompilationTasks为这个自定义的Transform生成一个对应的Task,(transformClassesWithSingleClickHunterTransformForDebug),在.class文件转换成.dex文件的流程中会执行这个Task,对所有的.class文件(可包括第三方库的.class)进行转换,转换的逻辑定义在Transform的transform方法中。
2.3、自定义一个Transform
public class CustomTransform extends Transform {
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
//当前是否是增量编译(由isIncremental() 方法的返回和当前编译是否有增量基础)
boolean isIncremental = transformInvocation.isIncremental();
//消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
//OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyFile(jarInput.getFile(), dest);
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.getFile(), dest);
}
}
}
@Override
public String getName() {
return "CustomTransform";
}
@Override
public boolean isIncremental() {
return true; //是否开启增量编译
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
}
在transform方法中,我们需要将每个jar包和class文件复制到dest路径,这个dest路径就是下一个Transform的输入数据。而在复制时,就可以将jar包和class文件的字节码做一些修改,再进行复制。
2.4、Transform两个过滤纬度
ContentType,数据类型,有CLASSES和RESOURCES两种。 其中的CLASSES包含了源项目中的.class文件和第三方库中的.class文件。 RESOURCES仅包含源项目中的.class文件。 对应getInputTypes() 方法。
Scope,表示要处理的.class文件的范围,主要有 PROJECT, SUB_PROJECTS,EXTERNAL_LIBRARIES等。 对应getScopes() 方法。
2.5、支持增量编译
Transform支持增量编译分为两步:
(1)重写Transform的接口方法:isIncremental(),返回true。
@Override
public boolean isIncremental() {
return true;
}
(2)判断当前编译对于Transform是否是增量编译: 如果不是增量编译,就按照前面的方式,依次处理所有的class文件; (比如说clean之后的第一次编译没有增量基础,即使Transform的isIncremental放回true,当前编译对Transform仍然不是增量编译,所有需要依次处理所有的class文件)
如果是增量编译,根据每个文件的Status,处理文件: 如果文件有改变,就按照前面的方式,去处理这个问题。
如果文件没有改变,就不需要进行处理,因为在输出目录已经有一个上次处理过的class文件了 (NOTCHANGED: 当前文件不需处理,甚至复制操作都不用; ADDED、CHANGED: 正常处理,输出给下一个任务; REMOVED: 移除outputProvider获取路径对应的文件。)
注意:当前编译对于Transform是否是增量编译受两个方面的影响: (1)isIncremental() 方法的返回值; (2)当前编译是否有增量基础;(clean之后的第一次编译没有增量基础,之后的编译有增量基础)
增量的时间缩短为全量的速度提升了3倍多,而且这个速度优化会随着工程的变大而更加显著。
2.6、支持并发编译
private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//异步并发处理jar/class
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveJar(srcJar, destJar);
return null;
});
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
return null;
});
//等待所有任务结束
waitableExecutor.waitForTasksWithQuickFail(true);
为什么要等待所有任务结束? 如果不等待,主线程就会进入下一个任务的处理,可能当前的任务的处理工作还没完成。
并发Transform和非并发Transform下,编译速度提高了80%。
3、ASM
ASM ,速度快、代码量小、功能强大,要写字节码、学习曲线高。 Javassist,学习简单,不用写字节码,比ASM慢,功能少。
3.1、ASM访问字节码流程
private void copy(String inputPath, String outputPath) {
FileInputStream is = new FileInputStream(inputPath);
ClassReader cr = new ClassReader(is);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cr.accept(cw, 0);
FileOutputStream fos = new FileOutputStream(outputPath);
fos.write(cw.toByteArray());
}
- (1)、ClassReader负责读取.class字节码;
- (2)、ClassReader将所有字节码传递ClassWriter(是一个ClassVisitor)中的(多个)visitxxx接口方法依次进行处理;
- (3)、ClassWriter访问某个方法时会将这个方法的所有字节码传递给MethodWriter(是一个MethodVisitor)处理。
默认ClassWriter会保存传递到它的所有字节码,可使用ClassWriter.toByteArray()方法获取经过ClassWriter的字节码。
3.2、以上流程代码证明:ClassReader.accept()。
public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
// 读取当前class的字节码信息
int accessFlags = this.readUnsignedShort(currentOffset);
String thisClass = this.readClass(currentOffset + 2, charBuffer);
String superClass = this.readClass(currentOffset + 4, charBuffer);
String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)];
//classVisitor就是刚才accept方法传进来的ClassWriter,每次visitXXX都负责将字节码的信息存储起来
classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
/**
略去很多visit逻辑
*/
//visit Attribute
while(attributes != null) {
Attribute nextAttribute = attributes.nextAttribute;
attributes.nextAttribute = null;
classVisitor.visitAttribute(attributes);
attributes = nextAttribute;
}
/**
略去很多visit逻辑
*/
classVisitor.visitEnd();
}
Gradle中的ClassWriter默认对传递给它的字节码不做任何处理,只做保存工作。
通过默认ClassWriter处理字节码的流程如下:
3.3、修改字节码
要修改字节码,需要自定义ClassWriter,在其访问类的相应方法时对其做相应操作(使用自定义的MetiodWriter),达到字节码插桩的目的。
3.4、什么事增量编译
我理解的增量编译: 1、基于Task的上次输出快照和这次输入快照对比,如果相同,则跳过相应任务; 2、基于Task本身是否支持增量更新。
3.4、增量编译实验
3.4.1、Transform 的isIncremental()返回true。
@Override
public boolean isIncremental() {
return true;
}
(1)、clean之后,第一次编译,即使Transform里面isIncremental()返回true,Transform开启了增量编译,此时对Transform来说仍然不是增量编译, transform方法中isIncremental = false;
(2)、不做任何改变直接进行第二次编译,Transform别标记为up-to-date,被跳过执行;
(3)、修改一个文件中代码,进行第三次编译,此时对Transform来说是增量编译,transform方法中isIncremental = true。
3.4.2、Transform 的isIncremental()返回false。
@Override
public boolean isIncremental() {
return false;
}
(1)、clean之后,第一次编译,此时对Transform来说不是增量编译, transform方法中isIncremental = false;
(2)、不做任何改变直接进行第二次编译,Transform别标记为up-to-date,被跳过执行;
(3)、修改一个文件中代码,进行第三次编译,此时对Transform来说不是增量编译,transform方法中isIncremental = false。
结论:1、一次编译对Transform来说是否是增量编译取决于两个方面: (1)、当前编译是否有增量基础; (2)、当前Transform是否开启增量编译。
结论:2、不管Transform是否开启增量编译,若TransformTask的当前输入快照和上次输出快照相同,则跳过当前TransformTask。
4、Gradle插件和Transform实战应用
按钮快速点击的问题在于:可能重复打开多个页面。
原理:
4.1、防止快速点击的原理
记录view两次点击的时间差,如果这个时间差小于我们定义的一个时间间隔,那么第二次点击就直接返回,不进行点击的逻辑处理。
4.2、如何全局解决项目中所有按钮的快速点击问题
第一种方法是手动全局添加,它的问题在于:
- (1)、按钮太多,工作量大,容易遗漏;
- (2)、无法给第三方sdk中的按钮添加此逻辑。
第二种方法是采用AOP的方式去添加,具体的过程是:
在打包过程中有一个阶段是class文件转换dex的阶段,所有class文件会经历多个Transform进行处理,我们可以自定义一个Transform得到所有的class文件,然后扫描判断这个类是否实现OnClickListener,如果实现就在其onclick方法中是用asm操作字节码插入上述防止快速点击的逻辑。最后将所有文件复制到输出目录就可以了。
这样做可以实现功能,但是发现处理速度较慢,修改5个类,这个Transform大概需要10s处理。
于是我做了两点优化:
- (1)、支持并发编译
- (2)、支持增量编译
做了这两点优化后,修改5个类,这个Transform大概在1.5s左右处理。提升了6倍多。
另外我发现我们app中有少数按钮是需要快速点击的,所有我又自定义了一个注解,在onclick方法上面加上这个注解就不会插入防止快速点击的逻辑。
实现原理也很简单,就是在字节码插桩的时候去判断onclick方法上是否有这样一个注解,如果有就不插入。
问题:
1,点击事件委派问题。导致按钮点击没有响应。如:
View.OnClickListener DelegateClickListener;
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
DelegateClickListener.onclick(v);
}
});
在一个view的点击事件中有两个OnClickListener处理这个事件,具体来说就是在第一个OnClickListener的onClick中调用了第二个OnClickListener的onclick方法,讲这个事件传递个了二个OnClickListener
出现这个问题的原因是,我们开始将点击时间和view绑定,那么必然存在第一个onClick方法可以响应点击逻辑,第二个onclick方法就直接返回了,因为代码的执行时间必然比我们定义的时间间隔短。
问题的根源是点击时间和view绑定,那么我们将点击时间和view解绑,然后将点击时间和OnclickListener关联起来,就是每个OnclickListener中有一个点击时间,这样,就不会相互影响了。
2,lambda表达式问题 最新的编译流程中,lambda代码转换成class后,不会脱糖,所以是找不到onclick方法的。
public class OtherTestViewHolder extends ViewHolder {
private int i = 100;
public OtherTestViewHolder(@NonNull View itemView) {
super(itemView);
itemView.setOnClickListener((v) -> {
Log.i("", "");
ToastHelper.toast(v, v, "1234");
});
}
}
//删除无效的不想阅读的代码
// access flags 0x1
public <init>(Landroid/view/View;)V
L2
LINENUMBER 19 L2
ALOAD 1
// 通过INVOKEDYNAMIC 将当前的的setOnClickListener 链接到lambda$new$0静态方法上去。
INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [
// arguments:
(Landroid/view/View;)V,
// handle kind 0x6 : INVOKESTATIC
com/wallstreetcn/sample/adapter/OtherTestViewHolder.lambda$new$0(Landroid/view/View;)V,
(Landroid/view/View;)V
]
// access flags 0x100A
private static synthetic lambda$new$0(Landroid/view/View;)V
L0
LINENUMBER 20 L0
LDC ""
LDC ""
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L1
LINENUMBER 21 L1
ALOAD 0
ALOAD 0
LDC "1234"
INVOKESTATIC com/wallstreetcn/sample/ToastHelper.toast (Ljava/lang/Object;Landroid/view/View;Ljava/lang/Object;)V
RETURN
L2
LOCALVARIABLE v Landroid/view/View; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
最新的编译流程中,Lambda表达式会被翻译成INVOKEDYNAMIC指令,(它的名称是onclick,描述符是OnClickListener),然后对应的onclick里面的逻辑会被包装成一个静态方法,在这个INVOKEDYNAMIC指令的参数中,会记录将这个onclick对应的静态方法的名字和描述,我们根据这个名称和描述就可以找对 onclick对应的方法。进而就可以对这个静态方法进行后续的字节码操作就可以了。
fun ClassNode.lambdaHelper(): MutableList<MethodNode> {
val lambdaMethodNodes = mutableListOf<MethodNode>()
methods?.forEach { method ->
method.instructions.iterator()?.forEach {
// 先从 method.instructions中找到`InvokeDynamicInsnNode`
if (it is InvokeDynamicInsnNode) {
// 判断是不是我想要修改的类 举例View\$OnClickListener
if (it.name == "onClick" && it.desc.contains(")Landroid/view/View\$OnClickListener;")) {
Log.info("dynamicName:${it.name} dynamicDesc:${it.desc}")
//获取指令中的参数,name和desc
val args = it.bsmArgs
args.forEach { arg ->
// 根据其中的name和desc等找到其所对应的静态方法,之后加入list中
if (arg is Handle) {
val methodNode = findMethodByNameAndDesc(arg.name, arg.desc, arg.tag)
Log.info("findMethodByNameAndDesc argName:${arg.name} argDesc:${arg.desc} " +
"method:${method?.name} ")
if (methodNode != null) {
lambdaMethodNodes.add(methodNode)
}
}
}
}
}
}
}
//然后返回当前类所有要修改的lambda
lambdaMethodNodes.forEach {
Log.info("lambdaName:${it.name} lambdaDesc:${it.desc} lambdaAccess:${it.access}")
}
return lambdaMethodNodes
}
// 根据名字和描述以及操作类型找到对应的方法
fun ClassNode.findMethodByNameAndDesc(name: String, desc: String, access: Int): MethodNode? {
return methods?.firstOrNull {
it.name == name && it.desc == desc
}
}
AsmClassVisitorFactory流程
1. ClickBlockerPlugin.kt
插件主类
kotlin
复制编辑
package com.example.clickblocker
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* 插件入口类,用于注册 ASM 插桩逻辑
*/
class ClickBlockerPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 检查是否应用了 Android 插件(只处理应用模块)
project.plugins.withId("com.android.application") {
// 获取 Android 构建组件扩展
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
// 对所有构建变体执行 instrumentation 插桩逻辑
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
variant.instrumentation.apply {
// 注册我们自定义的 ASM 工厂类
transformClassesWith(ClickClassVisitorFactory::class.java, InstrumentationScope.ALL) {
// 此处可以传参数到工厂(目前未用)
}
// 设置帧重计算模式,确保插桩后字节码可执行
setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
}
}
}
}
自定义一个插件,在插件中注册AsmClassVisitorFactory的子类,用于asm修改字节码
✅ 2. ClickClassVisitorFactory.kt
—— ASM 工厂类
kotlin
复制编辑
package com.example.clickblocker
import com.android.build.api.instrumentation.*
import org.objectweb.asm.ClassVisitor
/**
* 用于创建具体的 ASM 字节码访问器 ClickClassVisitor
*/
abstract class ClickClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
// 返回我们自定义的字节码访问器
return ClickClassVisitor(nextClassVisitor)
}
/**
* 判断是否对当前 class 文件插桩。
* 这里只是一个简单示例,过滤包含 "OnClickListener" 的类。
* 实际可使用更精确判断,比如 classData.superClasses.contains(...)
*/
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className.contains("OnClickListener")
}
}
在自定义的AsmClassVisitorFactory内部返回一个ClassVisitor,将框架传递过来的ClassVisitor,传递进去
✅ 3. ClickClassVisitor.kt
—— 字节码插桩逻辑
kotlin
复制编辑
package com.example.clickblocker
import org.objectweb.asm.*
import org.objectweb.asm.commons.AdviceAdapter
/**
* 访问 class 文件中的方法,当检测到 onClick() 方法时注入逻辑
*/
class ClickClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM9, cv) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
// 找到 onClick(View view) 方法
if (name == "onClick" && descriptor == "(Landroid/view/View;)V") {
// 使用 AdviceAdapter 简化插桩逻辑
return object : AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {
override fun onMethodEnter() {
// 插入以下逻辑:
// if (ClickUtils.isFastClick(view)) return;
// 将方法参数 View 压入操作数栈(第1个参数)
mv.visitVarInsn(ALOAD, 1)
// 调用工具类 ClickUtils.isFastClick(view)
mv.visitMethodInsn(
INVOKESTATIC,
"com/example/clickblocker/ClickUtils", // 替换为实际路径
"isFastClick",
"(Landroid/view/View;)Z",
false
)
// 如果返回 false,说明不是快速点击,跳过 return
val continueLabel = Label()
mv.visitJumpInsn(IFEQ, continueLabel)
// return;
mv.visitInsn(RETURN)
// 标记继续执行位置
mv.visitLabel(continueLabel)
}
}
}
return mv
}
}
在自定义的ClassVisitor通过传递过来的ClassVisitor.visitMethod获取MethodVisitor,进行字节码修改
框架传递过来的ClassVisitor实际上是一个classwriter
val mv = super.visitMethod(access, name, descriptor, signature, exceptions) super是传递给ClassVisitor的那个框架的ClassVisitor,实际上是一个classwriter
protected final int api;
protected ClassVisitor cv; // 父访问器,比如 ClassWriter
public ClassVisitor(int api) {
this(api, null);
}
public ClassVisitor(int api, ClassVisitor classVisitor) {
this.api = api;
this.cv = classVisitor;
}
/**
* 访问一个类的方法结构
*/
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions
) {
// 如果还有下一层的 ClassVisitor(如 ClassWriter),就把这个方法结构继续传下去
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
// 否则返回 null,表示不处理
return null;
}
}
✅ 4. ClickUtils.kt
—— 快速点击工具类
kotlin
复制编辑
package com.example.clickblocker
import android.view.View
/**
* 用于判断是否是快速点击
*/
object ClickUtils {
// 最小点击间隔:600ms
private const val MIN_CLICK_DELAY_TIME = 600L
// 保存每个 View 的上次点击时间(key: view.id)
private val viewClickMap = mutableMapOf<Int, Long>()
/**
* 判断给定的 View 是否属于快速点击
*/
@JvmStatic
fun isFastClick(view: View): Boolean {
val now = System.currentTimeMillis()
val id = view.id
val lastTime = viewClickMap[id] ?: 0L
return if (now - lastTime < MIN_CLICK_DELAY_TIME) {
true // 是快速点击
} else {
viewClickMap[id] = now
false // 正常点击
}
}
}
🧪 最终效果
任何实现了 View.OnClickListener
的类,其 onClick(View v)
方法将在进入时插入如下逻辑:
java
复制编辑
if (ClickUtils.isFastClick(view)) return;
即自动拦截快速点击,无需手动在业务代码中处理!
AMSClassVisitorFactory实际上是将原来transform修改字节码的流程进行了封装,并且支持了增量编译和并发编译
asm修改字节码流程
##读取 class 文件(ClassReader) val classReader = ClassReader(inputClassBytes) 创建 ClassWriter(用于输出) val classWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES) 将classWriter传递给自定义的ClassVisitor val classVisitor = object : ClassVisitor(Opcodes.ASM9, classWriter) { override fun visitMethod(...): MethodVisitor { 通过classWriter的visitMethod获得Methodvisitor,用于修改字节码 val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
return object : AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {
override fun onMethodEnter() {
mv.visitVarInsn(ALOAD, 1)
}
}
}
}
连接 ClassReader
与 ClassVisitor
进行扫描与写入
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
输出修改后的字节码
val modifiedBytes = classWriter.toByteArray()
在 ASM 插桩中使用 AsmClassVisitorFactory
(通常配合 Android Gradle Plugin 7.0+ 的 Instrumentation
API)可以实现更现代化的字节码修改方式,它天然支持增量编译和并发编译(并发 transform) ,但前提是你正确实现了相关接口。天然支持增量编译和并发编译
增量编译支持原理
- Gradle 会记录上一次构建中所有 class 文件的输入状态(hash、时间戳)
- 实现
IncrementalVisitor
后,Gradle 会调用你插件中的isInstrumentable()
来判断是否要处理当前类 - 你只需让插件保证「对 unchanged class 不做修改」即可支持增量构建
isInstrumentable()
的作用
这是 Gradle 编译器在使用 AsmClassVisitorFactory
插桩时的一个过滤器方法:
kotlin
复制编辑
override fun isInstrumentable(classData: ClassData): Boolean
作用是:告诉 Gradle:这个 class 文件是否要进行字节码插桩处理。
Gradle 是否启用增量编译逻辑,是根据你是否实现了
IncrementalVisitor
接口来判断的。
if (visitor is IncrementalVisitor) {
// Gradle 会根据文件的变化类型(ADDED, CHANGED, REMOVED)
处理class
} else {
// 没有实现 IncrementalVisitor,Gradle 默认执行“全量模式”
// 所有类都重新 visit,不管有没有变
}
情况一:你启用了增量编译(且插件支持)
- 如果 class 没有变动(包括源码没改、依赖没变),
- 你又实现了
IncrementalVisitor
接口, - 那么:Gradle 不会再次调用你的
visitMethod()
等方法
👉 不会重新插桩,构建速度更快。
情况二:你没启用增量编译,或插件实现不支持
- 插件未实现
IncrementalVisitor
- 或者你的插件对所有类都
isInstrumentable() == true
,没有细粒度控制
那么:第二次编译仍然会重新处理所有类
🧠 插件如何判断类是否“需要重新处理”?
Gradle 会比较 .class
文件:
- 上一次构建记录的 class hash / 时间戳
- 当前 class 文件的 hash
- 如果不一致,就标记为“变化”,再次处理
- 否则跳过(前提是插件实现支持)
✅ 如何让“处理过的类”下次不再处理?
你需要确保:
- 实现了
IncrementalVisitor
- 在
isInstrumentable()
中只对真正关心的类返回true
- 插桩逻辑是幂等的,不引起不必要的结构变化
- Gradle 构建缓存正常开启(默认是开的)
实际上对transform api的封装