11. Android <卡顿十一>深入ASM与Transform进行插桩,手写微信Matrix插件,打造自己的Matrix工具(卡顿进阶)

ASM,Transfer,自定义插件,要先对Gradle知识,它的生命周期有所了解才行!

可以先看我之前的Arounter自定义插件,还有微信Matrix源码插桩

5. Android 组件化五(演变升级) 从蜗牛编译到秒级构建:组件化如何让工程效率飙升300%你是否还在为修改一行 - 掘金

10. Android <卡顿十>高度封装Matrix卡顿, 修改Matrix源码和发布自己的插件在集成Matrix的时 - 掘金

1.微信Matrix插桩原理,源码分析

1.1 插件的结构

在源码中找到matrix-gradle-plugin这个模块,找到插件的入口。 resources/META-INF/gradle-plugins/com.tencent.matrix-plugin.properties

ini 复制代码
implementation-class=com.tencent.matrix.plugin.MatrixPlugin

Matrix-android 里面的matrix-gradle-plugin matrix-gradle-plugin 的源码进行一个深入的分析。这个插件是 Matrix-Android 实现"编译期插桩"的核心,它负责在 Android 应用的编译过程中修改字节码。

插件的核心内容在于TraceMethodAdapter中的三项操作:

在编译期间,除被排除掉的方法外,大量方法的入口和出口处被插入了AppMethodBeat的方法,意在能通过这两个方法计算出方法执行的耗时,于是,每一个方法执行的耗时情况就清晰的展现在我们开发者眼前,借助这些数据才能更好的发现卡顿问题的原因。

1.1.1 插件应用 (MatrixPlugin.apply)

当你在项目的 build.gradle 中写下 apply plugin: 'com.tencent.matrix-plugin' 时,MatrixPlugin.apply(project) 方法被调用。

主要工作:

  1. 检查环境:确认当前插件是否被应用在 Android Application 或 Library 项目中。
  2. 创建配置扩展 :通过 project.extensions.create('matrix', MatrixExtension),这使得你可以在 build.gradle 中使用 matrix { ... } 来配置插件。
  3. 注册自定义 Transform :这是最关键的一步。通过 project.android.registerTransform(...) 方法,将 MatrixTraceTransform 注册到 Android 构建流程中。此时,插件就成功"挂载"到了构建流程上。
  4. 注册其他任务:可能会注册一些用于处理资源、清单文件等的辅助任务。
1.1.2 配置扩展 (MatrixExtension)

这个类定义了你熟悉的配置结构:

ini 复制代码
matrix {
    trace {
        enable = true
        baseMethodMapFile = "${project.buildDir}/matrix_output/Debug.methodmap"
        blackListFile = "${project.projectDir}/matrixtrace/blackMethodList.txt"
        // ... 其他配置
    }
    removeUnusedResources {
        enable = true
        // ... 其他配置
    }
    // ... 其他功能配置
}

MatrixTraceTransform 在执行时会读取这些配置来决定插桩的行为。

1.1.3 字节码转换核心 (MatrixTraceTransform)

MatrixTraceTransform 继承自 com.android.build.api.transform.Transform,它重写了几个关键方法来实现字节码处理。

transform(transformInvocation: TransformInvocation) 方法流程:

这是 Transform 的核心入口。它的处理逻辑可以概括为以下几步:

  1. 检查是否启用/是否需要执行 :读取 MatrixExtension 中的配置,检查插桩功能是否被启用。如果未启用,则直接 return,跳过所有处理,避免不必要的性能开销。

  2. 收集输入文件

    • 遍历所有输入transformInvocation.inputs 包含了所有的输入内容,分为 目录输入 (DirectoryInput) 和 JAR 输入 (JarInput)。
    • 目录输入 :通常是项目的源码编译后的 .class 文件目录。
    • JAR 输入 :项目所依赖的本地或远程库(.jar.aar 中的 .class 文件)。
  3. 处理增量编译

    • 检查 transformInvocation.isIncremental()。如果是增量编译,则只处理发生变化(Status.ADDED, Status.CHANGED)的文件,而跳过状态为 Status.REMOVEDStatus.NOTCHANGED 的文件。这是优化构建速度的关键。
  4. 遍历和处理每个 Class 文件

    • 这是一个双重循环:外层循环遍历每个 Input,内层循环遍历每个 Input 中的每个 .class 文件。

    • 关键决策 :对于遇到的每一个 .class 文件,需要根据配置决定是否要处理它。

      • 是否在包名白名单内? (如 com.sample.app)
      • 是否在黑名单内? (如 android.support.*, com.tencent.matrix.*)
      • 是否要忽略的类? (如 R文件、BuildConfig文件、Lambda表达式等)
    • 这个过滤逻辑通常封装在 MatrixTrace 之类的类中。

  5. 字节码修改 (ASM 操作)

    • 对于需要插桩的 .class 文件,使用 FileInputStream 读取其字节数据。

    • 创建一个 ClassReader 来解析字节码。

    • 创建一个 ClassWriter (通常使用 ClassWriter.COMPUTE_FRAMES 模式,让 ASM 自动计算栈帧,更安全)。

    • 创建一个自定义的 ClassVisitor (例如 TraceClassAdapter),并包裹 ClassWriter。这个自定义的 ClassVisitor 是 ASM 操作的核心。

    • 调用 ClassReader.accept(ClassVisitor, parsingOptions),开始访问解析类的所有部分(注解、字段、方法等)。

    • 方法级访问 :当访问到方法时,自定义的 ClassVisitor 会创建一个自定义的 MethodVisitor (例如 TraceMethodAdapter)。在这个 MethodVisitor 中:

      • visitCode() 方法中(代表方法体开始),插入记录方法开始时间的代码(如调用 AppMethodBeat.i(methodId))。
      • visitInsn(opcode) 方法中,当遇到 RETURN, IRETURN, ATHROW 等方法返回指令时,插入记录方法结束时间的代码(如调用 AppMethodBeat.o(methodId))。
      • 这些"桩代码"是通过 ASM 的 visitMethodInsn(), visitLdcInsn() 等指令一步步"编织"进去的。
    • 访问结束后,ClassWriter.toByteArray() 会返回修改后的新字节码。

  6. 输出文件

    • 将修改后的新字节码(byte[])写入到 transformInvocation.outputProvider 指定的目标位置。这个位置后的后续编译步骤(如 dex 打包)会使用这些已经被修改过的文件。

1.1.4. 关键技术与设计模式

  1. Transform API :Android Gradle Plugin 提供的标准扩展点,允许第三方插件在 .class -> .dex 的过程中修改字节码。

  2. ASM :一个轻量级但功能强大的 Java 字节码操作和分析框架。它提供了基于 Visitor 模式的 API (ClassVisitor, MethodVisitor),使得遍历和修改字节码变得非常清晰。

  3. 增量编译:为了性能考量,插件必须很好地支持增量编译,避免每次构建都处理所有文件。

  4. 职责分离

    • MatrixPlugin 负责插件生命周期和注册。
    • MatrixExtension 负责配置管理。
    • MatrixTraceTransform 负责构建流程集成和文件IO。
    • MatrixTrace (或其他类似类) 负责核心的插桩逻辑和 ASM 操作。

1.2 Matrix 获取方法堆栈的完整流程

整个过程分为编译期运行期两个阶段,完美衔接:

阶段一:编译期 (Preparation)
  1. 插桩matrix-gradle-plugin 使用 ASM 在所有方法入口插入 AppMethodBeat.i(methodId),在所有方法出口(包括正常 return 和异常 throw)插入 AppMethodBeat.o(methodId)
  2. 生成映射表 :在插桩的同时,插件会收集所有被插桩方法的签名信息 (如 com/tencent/sample/MyActivity->onCreate(Landroid/os/Bundle;)V)并为它们分配一个唯一的数字 ID (如 10001)。
  3. 输出文件 :插件将这个 方法ID -> 方法签名 的映射关系生成一个 JSON 文件(如 methodmap.json),并将其放在 Assets 目录或固定路径下,随 APK 一起发布。
Method ID Method Signature
10001 com/tencent/sample/MyActivity->onCreate(Landroid/os/Bundle;)V
10002 com/tencent/sample/MyActivity->onClick(Landroid/view/View;)V
... ...
阶段二:运行期 (Execution & Analysis)
  1. 数据采集

    • 当应用运行时,被插桩的方法开始执行。
    • 方法开始时,执行 AppMethodBeat.i(10001),记录:[时间戳t1, 事件: Enter, ID:10001]
    • 方法结束时,执行 AppMethodBeat.o(10001),记录:[时间戳t2, 事件: Exit, ID:10001]
    • AppMethodBeat 内部使用一个巨大的环形数组long[])来按顺序存储这些记录。每条记录用一个 long 值表示,其中高位代表时间戳,低位代表方法ID和事件类型(进入/退出)。这种设计极其高效,避免了对象创建和GC。
  2. 检测卡顿/慢方法

    • Matrix 的 监控主线程的loop,然后超过700ms,去检查 AppMethodBeat 中记录的数据。
    • 它通过分析连续的事件,可以计算出每个方法的执行耗时(t2 - t1)。
    • 如果发现某个方法的执行时间超过了预设的阈值(如 500ms 用于卡顿,100ms 用于慢方法),就会被判定为有问题的方法。
  3. 还原堆栈(关键步骤!)

    • 当发现一个耗时方法时,Monitor 线程只知道它的 methodId (例如 10001)和开始/结束时间t1, t2)。

    • 为了得到可读的堆栈,它需要:

      a. 查找映射表 :根据 methodId=10001,去内存中加载的 methodmap.json 数据里查找,得到完整的方法签名 com/tencent/sample/MyActivity->onCreate(...)

      b. 还原调用链 :光有一个方法名还不够,我们需要知道是谁调用了它。AppMethodBeat 的环形数组里按时间顺序存储了所有方法的进出记录。分析器可以根据 t1t2 这个时间范围,回溯 出在这段时间内所有"进入但尚未退出"的方法。

      • 这其实就是当时完整的调用栈

      • 例如,在 t1 时刻,如果栈内还有 [10000, 10002],那么最终的调用栈就是:

        scss 复制代码
        10002 (调用者B)
        10000 (调用者A)
        10001 (当前耗时方法) <- 我们找到的这个

      c. 翻译堆栈 :最后,将回溯得到的这一系列 methodId (如 [10002, 10000, 10001]) 全部通过映射表 翻译成对应的方法签名,就得到了一个完全可读的、包含调用关系的方法堆栈

less 复制代码
// 最终上报的堆栈信息可能是这样的:
at com.tencent.sample.CallerA.doSomething(CallerA.java:100)  // methodId 10000
at com.tencent.sample.CallerB.invokeTask(CallerB.java:200)   // methodId 10002
at com.tencent.sample.MyActivity.onCreate(MyActivity.java:50) // methodId 10001 (耗时700ms)

AppMethodBeat 的角色

在正确的流程中,AppMethodBeat 的角色依然不变:

  • 它仍然是一个被动的、高性能的数据记录器。它默默地、持续地记录所有被插桩方法的执行痕迹,形成一个"方法执行时间线"。
  • 它不负责主动判断卡顿 。判断卡顿是 LooperMonitor 的职责。
  • 它是一个"黑匣子" 。当"事故"(卡顿)发生时,调查员(LooperMonitor 的回调分析逻辑)会根据"事故发生时间"去读取黑匣子里的数据,还原事故现场(调用栈)。

EvilMethodTracer

插桩相关的:

traceExtension和removeUnusedResourcesExtension对应的正是build.gradle中的配置

2.要显示的效果

每个方法前面和后面添加时间搓,结束的时候计算时间差值

需要插入的内容:主要就是2行

long startTime = System.currentTimeMillis()// --------> AppMethodBeat.i(methodId)

long endTime = System.currentTimeMillis();// -------->AppMethodBeat.o(methodId)

csharp 复制代码
 /**
     * 执行方法并统计执行时间
     * @param methodName 方法名称(用于标识和输出)
     * @param methodToExecute 要执行的方法(使用Lambda表达式或方法引用)
     */
    public static void executeWithTiming(String methodName, Runnable methodToExecute) {
        // 记录开始时间
        long startTime = System.currentTimeMillis(); // 需要插入的方法
        System.out.println("[" + methodName + "] 开始执行,时间戳: " + startTime);// 需要插入的方法
        
        try {
            // 执行目标方法
            methodToExecute.run();
        } finally {
            // 记录结束时间并计算耗时
            long endTime = System.currentTimeMillis();// 需要插入的方法
            long duration = endTime - startTime;// 需要插入的方法
            
            System.out.println("[" + methodName + "] 结束执行,时间戳: " + endTime);// 需要插入的方法
            System.out.println("[" + methodName + "] 执行耗时: " + duration + "ms");
        }
    }

3.手写Matrix插件

scss 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                           TimePlugin ASM 架构图                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────┐    注册       ┌─────────────────────────────────────┐  │
│  │   TimePlugin    │ ────────────► │        TimeTransform                │  │
│  │ (Gradle Plugin) │               │      (Android Transform)            │  │
│  └─────────────────┘               └─────────────────────────────────────┘  │
│         │                                      │                            │
│         │应用插件                              │处理Class文件               │
│         ▼                                      ▼                            │
│  ┌─────────────────┐               ┌─────────────────────────────────────┐  │
│  │  Android Project│               │        TimeClassVisitor              │  │
│  │ (使用插件项目)   │               │      (ASM Class Visitor)            │  │
│  └─────────────────┘               └─────────────────────────────────────┘  │
│                                                      │                      │
│                                                      │访问方法              │
│                                                      ▼                      │
│                                       ┌─────────────────────────────────────┐│
│                                       │        TimeMethodVisitor            ││
│                                       │      (ASM Method Visitor)           ││
│                                       └─────────────────────────────────────┘│
│                                                      │                      │
│                                                      │插入字节码            │
│                                                      ▼                      │
│                                       ┌─────────────────────────────────────┐│
│                                       │          MethodTimer                ││
│                                       │    (运行时时间统计工具类)           ││
│                                       └─────────────────────────────────────┘│
│                                                      │                      │
│                                                      │输出日志              │
│                                                      ▼                      │
│                                       ┌─────────────────────────────────────┐│
│                                       │          Android Log                ││
│                                       │        (系统日志输出)               ││
│                                       └─────────────────────────────────────┘│
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Android ASM 插桩插件开发与调试指南

针对您的问题,我整理了完整的解决方案,包括插件开发、调试技巧和常见错误处理。

3.1. 创建 ASM 插件模块

插件 build.gradle 配置

groovy 复制代码
plugins {
    id 'java-library'
    id 'maven-publish'
    id 'java-gradle-plugin' // 必须添加这个插件
}

group = 'com.example.asmplugin'  // 合法的 groupId
version = '1.0.0'

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation 'org.ow2.asm:asm:9.4' // 使用最新稳定版 ASM
    implementation 'org.ow2.asm:asm-commons:9.4'
    implementation 'com.android.tools.build:gradle:7.4.2' // 与项目 Gradle 版本一致
}

java {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

gradlePlugin {
    plugins {
        timeMethodPlugin {
            id = 'com.example.time-plugin'
            implementationClass = 'com.example.moduleasm.TimePlugin'
        }
    }
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId = 'com.example.asmplugin'
            artifactId = 'asm-plugin'
            version = '1.0.0'
            from components.java
        }
    }
    repositories {
        mavenLocal()
    }
}

3.2. 插件核心代码实现(特别要注意不同的版本,API都 不一样)

Plugin

3.2.1 TimePlugin.java

  • 职责: 插件入口,注册 Transform 到 Android 构建流程

  • 关键功能:

    • 应用插件时初始化
    • 创建排除列表(过滤系统类和不需插桩的类)
    • 注册 TimeTransform
    • 配置 MultiDex 支持
java 复制代码
package com.example.moduleasm;

import com.android.build.gradle.AppExtension;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class TimePlugin implements Plugin<Project> {
    private final Logger logger = Logging.getLogger(TimePlugin.class);

    @Override
    public void apply(Project project) {
        logger.lifecycle("⏳ [TimePlugin] 初始化开始");

        // 确保是 Android 项目
        if (!project.getPlugins().hasPlugin("com.android.application")) {
            throw new IllegalStateException("必须应用 'com.android.application' 插件");
        }

        // 创建排除列表
        Set<String> excludeList = createExcludeList();
        logger.lifecycle("排除列表: " + excludeList);

        // 注册 Transform
        AppExtension android = project.getExtensions().getByType(AppExtension.class);
        android.registerTransform(new TimeTransform(project, excludeList));

        logger.lifecycle("✅ [TimePlugin] 初始化完成");
    }

    private Set<String> createExcludeList() {
        return new HashSet<>(Arrays.asList(
            "android/", "androidx/", "kotlin/", "com/google/", 
            "javax/", "org/", "com/example/BuildConfig", 
            "com/example/R", "androidx/multidex/", "java/"
        ));
    }
}

3.2.2 TimeTransform.java

Transform

  • 职责: 处理编译过程中的 Class 文件转换

  • 关键功能:

    • 实现 Android Transform API
    • 遍历所有输入的 Class 和 Jar 文件
    • 调用 ASM Visitor 处理每个 Class 文件
    • 处理增量编译(如果支持)
java 复制代码
package com.example.moduleasm;

import com.android.build.api.transform.*;
import com.android.build.gradle.internal.pipeline.TransformManager;
import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;

public class TimeTransform extends Transform {
    private final Project project;
    private final Set<String> excludeList;
    private final Logger logger = Logging.getLogger(TimeTransform.class);

    public TimeTransform(Project project, Set<String> excludeList) {
        this.project = project;
        this.excludeList = excludeList;
    }

    @Override
    public String getName() {
        return "TimeTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation invocation) throws IOException {
        logger.lifecycle("🛠️ [TimeTransform] 开始处理...");
        
        for (TransformInput input : invocation.getInputs()) {
            for (DirectoryInput dirInput : input.getDirectoryInputs()) {
                processDirectory(dirInput.getFile(), invocation.getOutputProvider());
            }
            
            for (JarInput jarInput : input.getJarInputs()) {
                processJar(jarInput.getFile(), invocation.getOutputProvider());
            }
        }
        
        logger.lifecycle("✅ [TimeTransform] 处理完成");
    }

    private void processDirectory(File inputDir, TransformOutputProvider outputProvider) throws IOException {
        File outputDir = outputProvider.getContentLocation(
            inputDir.getName(), getInputTypes(), getScopes(), Format.DIRECTORY
        );
        
        FileUtils.copyDirectory(inputDir, outputDir);
        
        Collection<File> classFiles = FileUtils.listFiles(outputDir, new String[]{"class"}, true);
        logger.lifecycle("处理目录: {}, 找到 {} 个 class 文件", inputDir, classFiles.size());
        
        for (File file : classFiles) {
            if (shouldSkip(file)) {
                logger.debug("跳过排除文件: {}", file.getAbsolutePath());
                continue;
            }
            
            try {
                byte[] bytes = FileUtils.readFileToByteArray(file);
                byte[] modified = modifyClass(bytes);
                FileUtils.writeByteArrayToFile(file, modified);
                logger.debug("已处理: {}", file.getName());
            } catch (Exception e) {
                logger.error("处理文件失败: " + file.getAbsolutePath(), e);
            }
        }
    }

    private void processJar(File inputJar, TransformOutputProvider outputProvider) throws IOException {
        File outputJar = outputProvider.getContentLocation(
            inputJar.getName(), getInputTypes(), getScopes(), Format.JAR
        );
        FileUtils.copyFile(inputJar, outputJar);
    }

    private boolean shouldSkip(File classFile) {
        String path = classFile.getAbsolutePath().replace(File.separatorChar, '/');
        
        for (String exclude : excludeList) {
            if (path.contains(exclude)) {
                return true;
            }
        }
        
        return path.endsWith("/R.class") || 
               path.endsWith("/BuildConfig.class") ||
               path.contains("androidx/multidex/");
    }

    private byte[] modifyClass(byte[] original) {
        try {
            ClassReader reader = new ClassReader(original);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            TimeClassVisitor visitor = new TimeClassVisitor(writer);
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        } catch (Exception e) {
            logger.error("修改类失败", e);
            return original;
        }
    }
}

3.2.3 TimeClassVisitor.java

  • 职责: 访问类结构,筛选需要插桩的方法

  • 关键功能:

    • 实现 ASM ClassVisitor
    • 过滤不需要插桩的类(系统类、特定包等)
    • 识别并跳过特定方法(构造函数、静态块等)
    • 创建 TimeMethodVisitor 处理每个方法
java 复制代码
package com.example.moduleasm;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class TimeClassVisitor extends ClassVisitor {
    private String className;
    private final Logger logger = Logging.getLogger(TimeClassVisitor.class);
    
    // 需要完全跳过的类名单
    private static final Set<String> FULLY_EXCLUDED_CLASSES = new HashSet<>(Arrays.asList(
        "android/", "androidx/", "java/", "javax/", "kotlin/", 
        "org/", "com/google/", "com/example/BuildConfig"
    ));

    public TimeClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM9, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, 
                     String signature, String superName, String[] interfaces) {
        this.className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, 
                                    String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        
        if (shouldFullySkipClass(className) || shouldSkipMethod(access, name, descriptor)) {
            return mv;
        }
        
        logger.lifecycle("处理方法: {}.{}", className, name);
        return new TimeMethodVisitor(mv, className, name, descriptor, access);
    }

    private boolean shouldFullySkipClass(String className) {
        for (String excluded : FULLY_EXCLUDED_CLASSES) {
            if (className.startsWith(excluded)) {
                return true;
            }
        }
        return false;
    }

    private boolean shouldSkipMethod(int access, String methodName, String descriptor) {
        // 跳过构造方法和静态初始化块
        if (methodName.equals("<init>") || methodName.equals("<clinit>")) {
            return true;
        }
        
        // 跳过 native/abstract/synthetic 方法
        if ((access & Opcodes.ACC_NATIVE) != 0 || 
            (access & Opcodes.ACC_ABSTRACT) != 0 || 
            (access & Opcodes.ACC_SYNTHETIC) != 0) {
            return true;
        }
        
        return false;
    }
}

3.2.4 TimeMethodVisitor.java

  • 职责: 在方法前后插入时间统计代码

  • 关键功能:

    • 继承自 AdviceAdapter
    • 在方法入口处插入开始时间记录
    • 在方法出口处插入结束时间记录和耗时计算
    • 处理局部变量,避免冲突
    • 生成调用 MethodTimer 的字节码
java 复制代码
package com.example.moduleasm;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;

public class TimeMethodVisitor extends AdviceAdapter {
    private final String methodName;
    private final String className;
    private int startTimeVarIndex;
    private final Logger logger = Logging.getLogger(TimeMethodVisitor.class);

    public TimeMethodVisitor(MethodVisitor mv, String className, 
                           String methodName, String descriptor, int access) {
        super(Opcodes.ASM9, mv, access, methodName, descriptor);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    protected void onMethodEnter() {
        // 记录方法开始时间
        startTimeVarIndex = newLocal(Type.LONG_TYPE);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(LSTORE, startTimeVarIndex);
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (opcode != ATHROW) {
            // 记录方法结束时间
            int endTimeVarIndex = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, endTimeVarIndex);
            
            // 计算耗时
            int durationVarIndex = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LLOAD, endTimeVarIndex);
            mv.visitVarInsn(LLOAD, startTimeVarIndex);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationVarIndex);
            
            // 输出日志
            mv.visitLdcInsn("TimeTrace");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("Method [" + className.replace('/', '.') + "." + methodName + "] took ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationVarIndex);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
        }
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(Math.max(maxStack, 6), maxLocals);
    }
}

3.3. 发布插件到本地

在插件模块目录下运行:

bash 复制代码
./gradlew publishToMavenLocal

3.4. 在项目中使用插件

项目根目录 build.gradle

groovy 复制代码
buildscript {
    repositories {
        google()
        mavenCentral()
        mavenLocal() // 添加本地 Maven 仓库
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2'
        classpath 'com.example.asmplugin:asm-plugin:1.0.0' // 使用本地插件
    }
}

App 模块 build.gradle

groovy 复制代码
plugins {
    id 'com.android.application'
    id 'com.example.time-plugin' // 应用插件
}

android {
    // 正常 Android 配置
    compileSdk 33
    
    defaultConfig {
        applicationId "com.example.myapplication"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"
        
        // 启用 MultiDex
        multiDexEnabled true
    }
    
    buildTypes {
        debug {
            // 启用调试
            debuggable true
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.multidex:multidex:2.0.1'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

3.5. 调试技巧

主要3个办法: 添加日志,gradle的生命周期方法,反编译看字节码的插桩效果

3.5. 1. 实时调试(无需每次发布)

在项目的 settings.gradle 中添加:

groovy 复制代码
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
        mavenLocal()
    }
    
    // 包含插件模块进行实时开发
    includeBuild('../moduleAsm') // 修改为你的插件模块路径
}

这样修改插件代码后,无需重新发布,直接运行项目即可生效。

3.5.2. 查看插桩后的字节码,反编译(重点)

在transfer(),的里面有插桩的内容!在javac\debug\classes\xxxxx.class!

日志打印: 一个是在class里面的答应

一个是运行时候,在控制台,gradlew的日志打印

app/build/intermediates/transforms/TimeTransform/debug/ 目录下可以找到插桩后的 class 文件。

使用 JD-GUI 或其他反编译工具查看插桩效果。

3.5.3. 启用详细日志

运行构建时添加 --info--debug 参数:

bash 复制代码
./gradlew assembleDebug --info

3.5.4. 在插件中添加调试日志

java 复制代码
// 在 transform 方法中添加详细日志
@Override
public void transform(TransformInvocation invocation) throws IOException {
    logger.lifecycle("=== Transform开始执行 ===");
    logger.lifecycle("输入文件数量: " + invocation.getInputs().size());
    logger.lifecycle("输出路径: " + invocation.getOutputProvider());
    
    // 更多详细日志...
}

3.5.5. gradle生命周期排查(重点)

arduino 复制代码
gradle.taskGraph.beforeTask { Task task ->
    if (task.name.contains("Transform")) {
        println "Transform任务 detected: ${task.name}"
    }
}

3.5.6. 常见问题解决

问题 1: Invalid publication 'maven'

错误信息

csharp 复制代码
Invalid publication 'maven': groupId (My Application) is not a valid Maven identifier

解决方案: 使用合法的 Maven groupId,不能包含空格:

groovy 复制代码
group = 'com.example.asmplugin'  // 正确的格式

问题 2: ClassNotFoundException

错误信息

css 复制代码
Unable to instantiate application: java.lang.ClassNotFoundException

解决方案: 确保正确排除系统类和关键类:

java 复制代码
private Set<String> createExcludeList() {
    return new HashSet<>(Arrays.asList(
        "android/", "androidx/", "kotlin/", "com/google/", 
        "javax/", "org/", "com/example/BuildConfig", 
        "com/example/R", "androidx/multidex/", "java/"
    ));
}

问题 3: ASM 版本兼容性问题

错误信息

rust 复制代码
Cannot constrain type: INT for value: v9(i) by constraint: LONG

解决方案: 使用兼容的 ASM 版本,并确保所有 ASM 依赖版本一致:

groovy 复制代码
implementation 'org.ow2.asm:asm:9.4'
implementation 'org.ow2.asm:asm-commons:9.4'

3.5.7. 完整构建流程日志示例

正常构建输出应类似:

kotlin 复制代码
> Task :app:transformClassesWithTimeTransformForDebug
Transform任务 detected: transformClassesWithTimeTransformForDebug
🛠️ [TimeTransform] 开始处理...
=== Transform开始执行 ===
输入文件数量: 6
输出路径: com.android.build.gradle.internal.pipeline.TransformOutputProviderImpl@5086b32d
📁 Processing directory: F:\project\app\build\intermediates\javac\debug\classes
🔍 Found 13 class files to process
⚙️ Processing: MainActivity.class
✅ [TimeTransform] 处理完成

通过以上步骤和解决方案,您应该能够成功开发、调试和部署 ASM 插桩插件。

4 Android ASM 插桩插件开发总结

4.1 自定义插件的流程

开发流程概述

  1. 创建插件模块:新建 Android Library 模块,配置 build.gradle 添加必要依赖
  2. 实现 Plugin 接口:创建 TimePlugin 类实现 Gradle Plugin 接口
  3. 注册 Transform:在 Plugin 中注册自定义的 Transform 类
  4. 实现 Transform:创建 TimeTransform 类处理字节码转换
  5. 实现 ASM Visitors:创建 ClassVisitor 和 MethodVisitor 进行字节码操作
  6. 发布插件:配置发布到 Maven Local 或远程仓库
  7. 应用插件:在目标项目中应用插件

详细步骤

gradle 复制代码
// 1. 插件模块 build.gradle 配置
plugins {
    id 'java-library'
    id 'maven-publish'
    id 'java-gradle-plugin'
}

dependencies {
    implementation gradleApi()
    implementation 'org.ow2.asm:asm:9.4'
    implementation 'org.ow2.asm:asm-commons:9.4'
    implementation 'com.android.tools.build:gradle:7.4.2'
}

// 2. 发布配置
publishing {
    publications {
        maven(MavenPublication) {
            groupId = 'com.example.asmplugin'
            artifactId = 'asm-plugin'
            version = '1.0.0'
            from components.java
        }
    }
}

4.2 插入的方法内容与 ASM 工具使用

ASM 字节码操作原理

ASM 通过 Visitor 模式操作字节码,需要了解 Java 字节码结构和 JVM 指令集:

java 复制代码
// 方法插桩示例 - 在方法前后插入时间统计代码
@Override
protected void onMethodEnter() {
    // 插入: long startTime = System.currentTimeMillis();
    mv.visitMethodInsn(
        INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false
    );
    startTimeVarIndex = newLocal(Type.LONG_TYPE);
    mv.visitVarInsn(LSTORE, startTimeVarIndex);
}

@Override
protected void onMethodExit(int opcode) {
    // 插入耗时计算和日志输出代码
    if (opcode != ATHROW) {
        // 计算 duration = endTime - startTime
        // 使用 StringBuilder 构建日志信息
        // 调用 Log.d 输出日志
    }
}

ASM 工具使用建议

  1. 使用 ASM Bytecode Viewer:IntelliJ IDEA 插件,可查看字节码
  2. 编写测试用例:先对简单类进行插桩测试
  3. 逐步调试:从简单方法开始,逐步增加复杂度
  4. 参考官方文档:ASM 官网提供详细 API 文档和示例

4.3 Transform API 版本差异

不同 AGP 版本的 Transform API

AGP 版本 Transform API 状态 推荐替代方案
< 7.0 主要 API
7.0+ 已弃用 AsmClassVisitorFactory
8.0+ 完全移除 Instrumentation API

兼容性处理策略

java 复制代码
// 条件编译处理不同版本
dependencies {
    // 根据 AGP 版本选择不同的实现方式
    if (agpVersion.startsWith("7.") || agpVersion.startsWith("8.")) {
        implementation "com.android.tools.build:gradle:$agpVersion"
        // 使用新的 API
    } else {
        implementation "com.android.tools.build:gradle:$agpVersion"
        // 使用传统 Transform API
    }
}

新版 API 示例 (AsmClassVisitorFactory)

java 复制代码
// AGP 7.0+ 推荐方式
abstract class TimeAsmFactory implements AsmClassVisitorFactory<InstrumentationParameters.None> {
    @Override
    public ClassVisitor createClassVisitor(
        ClassContext classContext, ClassVisitor classVisitor) {
        return new TimeClassVisitor(classVisitor);
    }
    
    @Override
    public boolean isInstrumentableClass(ClassData classData) {
        // 过滤需要插桩的类
        return !classData.getClassName().startsWith("androidx/");
    }
}

4.4 插桩调试与问题排查

调试策略

  1. 启用详细日志 :运行构建时添加 --info--debug 参数
  2. 添加调试输出:在关键位置添加日志输出
java 复制代码
public void transform(TransformInvocation invocation) {
    logger.lifecycle("=== Transform开始执行 ===");
    logger.lifecycle("输入文件数量: " + invocation.getInputs().size());
    // 更多日志...
}
  1. 查看字节码:使用 JD-GUI 或 javap 查看插桩后的类文件
bash 复制代码
# 查看插桩后的字节码
javap -c build/intermediates/transforms/TimeTransform/debug/0/com/example/MainActivity.class
  1. Gradle 生命周期调试:添加任务监听器了解执行流程
gradle 复制代码
// 在插件 build.gradle 中添加
gradle.taskGraph.beforeTask { Task task ->
    if (task.name.contains("Transform")) {
        println "Transform任务 detected: ${task.name}"
    }
}

常见问题与解决方案

  1. ClassNotFoundException:检查排除列表,确保系统类被正确排除
  2. 字节码验证错误:确保 ASM 版本与 AGP 版本兼容
  3. 性能问题:优化排除列表,减少不必要的插桩
  4. 增量编译问题:正确实现 isIncremental() 方法

反编译查看插桩结果

  1. 找到插桩后的类文件:app/build/intermediates/transforms/TimeTransform/
  2. 使用 JD-GUI 或其他反编译工具查看
  3. 确认插桩代码正确插入:
java 复制代码
// 插桩后的方法示例
public void myMethod() {
    long startTime = System.currentTimeMillis();
    try {
        // 原始方法逻辑
    } finally {
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        Log.d("TimeTrace", "Method [com.example.MyClass.myMethod] took " + duration + "ms");
    }
}

总结

开发 Android ASM 插桩插件需要掌握以下关键点:

  1. Gradle 插件开发:了解插件结构和发布流程
  2. Transform API:掌握不同版本的区别和兼容性处理
  3. ASM 字节码操作:熟悉 Visitor 模式和字节码指令
  4. 调试技巧:使用日志、反编译和 Gradle 任务监控
  5. 兼容性考虑:正确处理系统类和第三方库的排除

通过遵循这些最佳实践,可以开发出稳定、高效的 ASM 插桩插件,为 Android 应用性能监控和优化提供有力工具。

5. 思考:如何关闭插桩的流程?

5.1. 通过 Gradle 配置开关控制

在插件中添加配置选项

typescript 复制代码
// TimePlugin.java 中添加配置扩展
public class TimePlugin implements Plugin<Project> {
    private TimeExtension timeExtension;
    
    @Override
    public void apply(Project project) {
        // 创建扩展
        timeExtension = project.getExtensions().create("timePlugin", TimeExtension.class);
        
        project.afterEvaluate(p -> {
            // 检查是否启用插件
            if (timeExtension.isEnabled()) {
                logger.lifecycle("⏳ [TimePlugin] 插件已启用");
                // 注册 Transform
                AppExtension android = project.getExtensions().getByType(AppExtension.class);
                android.registerTransform(new TimeTransform(project, createExcludeList()));
            } else {
                logger.lifecycle("⏳ [TimePlugin] 插件已禁用");
            }
        });
    }
}

// 配置扩展类
public class TimeExtension {
    private boolean enabled = true; // 默认启用
    
    public boolean isEnabled() {
        return enabled;
    }
    
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

在项目 build.gradle 中配置

ini 复制代码
// 在 app 模块的 build.gradle 中配置
timePlugin {
    enabled = false // 关闭插桩
}

// 或者根据构建类型配置
android {
    buildTypes {
        debug {
            // 调试版本启用
            timePlugin.enabled = true
        }
        release {
            // 发布版本禁用
            timePlugin.enabled = false
        }
    }
}

// 或者根据产品风味配置
android {
    productFlavors {
        dev {
            // 开发版本启用
            timePlugin.enabled = true
        }
        prod {
            // 生产版本禁用
            timePlugin.enabled = false
        }
    }
}

5. 2. 通过构建变体控制

使用变体特定的配置

arduino 复制代码
// 在构建变体中禁用插件
android {
    buildTypes {
        release {
            // 方法1: 通过扩展属性
            timePlugin.enabled = false
            
            // 方法2: 使用变体特定的插件应用
            if (project.plugins.hasPlugin('com.example.time-plugin')) {
                // 可以在这里移除插件,但更推荐使用配置开关
            }
        }
    }
    
    flavorDimensions "environment"
    productFlavors {
        dev {
            dimension "environment"
            timePlugin.enabled = true
        }
        prod {
            dimension "environment"
            timePlugin.enabled = false
        }
    }
}

项目案例:

自定义的插件地址: github.com/pengcaihua1...

参考博客: 官方的说明: android Matrix插桩原理 - 搜索 - 掘金

腾讯性能监控框架Matrix源码分析(十二)插桩 之MatrixTrace背景 对于APM项目,诸如像 Debug 日 - 掘金

浅谈Android Matrix使用原理前言 看了一下关于对Android性能监控框架Matrix的介绍九个模块的内容, - 掘金

相关推荐
小桥风满袖3 小时前
极简三分钟ES6 - 箭头函数
前端·javascript
bug_kada3 小时前
前端后端3步联调:Cookie认证实战,让登录功能完美上线!
前端·javascript
stringwu3 小时前
Flutter开发者必备:状态管理Bloc的实用详解
前端·flutter
青晚舟3 小时前
作为前端你必须要会的CICD
前端·ci/cd
hj5914_前端新手3 小时前
深入分析 —— JavaScript 深拷贝
前端·javascript
中微子3 小时前
虚拟列表完全指南:从零到一手写实现
前端
jqq6663 小时前
解析ElementPlus打包源码(二、buildFullBundle)
前端·javascript·vue.js
YaeZed3 小时前
TypeScript6(class类)
前端·typescript
织_网3 小时前
UniApp 页面通讯方案全解析:从 API 到状态管理的最佳实践
前端·javascript·uni-app