转载

和我一起用 ASM 实现编译期字节码织入

这两天摸鱼的时候,突然发现 Jake Wharton 大神写的 Hugo 非常有意思,通过这个库可以实现对方法调用的一些相关数据进行记录。比如它可以通过在方法前加上 DebugLog 注解使得该方法执行时在 Logcat 中打印这个方法的入参、耗时时间、返回值等等。

比如在代码中加入下面这样一个简单的注解:

@DebugLog
public String getName(String first, String last) {
  SystemClock.sleep(15);
  return first + " " + last;
}
复制代码

就可以实现在 Logcat 中打印如下的日志:

V/Example: ⇢ getName(first="Jake", last="Wharton")
V/Example: ⇠ getName [16ms] = "Jake Wharton"
复制代码

这个库的设计思路非常有趣,通过这样一种注解的形式可以很方便地打印调试信息,相比直接修改代码实现来说极大地降低了侵入性。经过查阅资料了解到 Hugo 是基于 AspectJ 所实现的,其核心原理就是编译期对字节码的插桩。刚好笔者前两天在项目中通过 ASM 字节码插桩实现了对 View 的点击事件的无痕埋点,因此突发奇想,想通过 ASM 实现一个类似功能的库。

但 Hugo 仅仅提供了打印方法执行相关信息的功能,因此就开始思考是否能够基于它的思路进行一些扩展,实现在方法调用前后执行指定逻辑的功能呢?

如果能实现这样一个库,那对于 Hugo 的功能,我们就只需要在方法调用前记录时间,在方法调用后计算时间差即可。

同时如果还需要一个统计应用中某个方法调用次数的功能,也只需要在方法调用时执行计数的逻辑即可。

这样的实现好处就在于便于扩展,对方法调用的前后进行了监听,而具体的执行逻辑可以由使用者来自己决定。如果对这个功能的实现感兴趣,就请跟着我继续看下去吧。

基本原理

首先,我们需要了解一下什么是 ASM,ASM 是一个 Java 字节码层面的代码分析及修改工具,它有一套非常易用的 API,通过它可以实现对现有 class 文件的操纵,从而实现动态生成类,或者基于现有的类进行功能扩展。

这时候可能有读者会问了,ASM 是操纵 class 文件的,但 Apk 里面的不都是 dex 文件么?这不就没办法应用到安卓中了么?

其实在 Android 的编译过程中,首先会将 java 文件编译为 class 文件,之后会将编译后的 class 文件打包为 dex 文件,我们可以利用 class 被打包为 dex 前的间隙,插入 ASM 相关的逻辑对 class 文件进行操纵。

和我一起用 ASM 实现编译期字节码织入

前面的思路很简单,但该如何才能做到在 class 文件被打包前执行我们 ASM 相关的代码呢?

Google 在 Gradle 1.5.0 后提供了一个叫 Transform 的 API,它的出现使得第三方的 Gradle Plugin 可以在打包 dex 之前对 class 文件进行进行一些操纵。我们本次就是要利用 Transform API 来实现这样一个 Gradle Plugin。

实现思路

有了前面提到的基本原理,让我们来思考一下具体的实现思路。

思路其实非常简单,这就是一种典型的观察者模式。我们的用户对某个方法的调用事件进行订阅,当方法被调用时,就会通知用户,从而执行指定的逻辑。

我们需要一个方法调用事件的调度中心,订阅者可以向该调度中心订阅某类型的方法的调用事件,每当带有指定注解的方法有调用事件产生时,都会通知该调度中心,然后由调度中心通知对应类型的订阅者。

这样的话,我们只需要在方法的调用前后,通过 ASM 织入通知调度中心的代码即可。

和我一起用 ASM 实现编译期字节码织入

Show me the code

有了思路,我们可以开始正式码代码了,这里我建立了一个叫 Elapse 的项目。(不要问为什么,就是因为好看)

准备工作

我们先进行一些准备工作——建立 ASM 插件的 module,清空自动生成的 gradle 代码,将 gradle 按如下方式编写:

apply plugin: 'groovy'

dependencies {
    implementation gradleApi()
    implementation localGroovy()

    implementation 'com.android.tools:gradle:3.1.2'
}

repositories {
    mavenCentral()
    jcenter()
    google()
}
复制代码

同时我们需要一个注解来标注需要被插桩的方法。我们采用了如下的一个编译期的注解,其含有一个 tag 参数用于表示该方法的 TAG,通过这个 TAG 我们可以实现针对不同方法的不同处理。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface TrackMethod {
    String tag();
}
复制代码

之后我们再创建一个 MethodEventManager ,用于注册及分发方法调用事件:

public class MethodEventManager {

    private static volatile MethodEventManager INSTANCE;
    private Map<String, List<MethodObserver>> mObserverMap = new HashMap<>();

    private MethodEventManager() {
    }

    public static MethodEventManager getInstance() {
        if (INSTANCE == null) {
            synchronized (MethodEventManager.class) {
                if (INSTANCE == null) {
                    INSTANCE = new MethodEventManager();
                }
            }
        }
        return INSTANCE;
    }

    public void registerMethodObserver(String tag, MethodObserver listener) {
        if (listener == null) {
            return;
        }

        List<MethodObserver> listeners = mObserverMap.get(tag);
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(listener);
        mObserverMap.put(tag, listeners);
    }

    public void notifyMethodEnter(String tag, String methodName) {
        List<MethodObserver> listeners = mObserverMap.get(tag);
        if (listeners == null) {
            return;
        }
        for (MethodObserver listener : listeners) {
            listener.onMethodEnter(tag, methodName);
        }
    }

    public void notifyMethodExit(String tag, String methodName) {
        List<MethodObserver> listeners = mObserverMap.get(tag);
        if (listeners == null) {
            return;
        }
        for (MethodObserver listener : listeners) {
            listener.onMethodExit(tag, methodName);
        }
    }
}
复制代码

这里代码不是很复杂,主要对外暴露了三个方法:

registerMethodObserver
notifyMethodEnter
notifyMethodExit

有了这样一个类,我们就只需要在代码编辑的时候向包含注解的方法的开始与结束处织入对应的代码就好,就像下面这样:

public void method(String param) {
	MethodEventManager.getInstance().notifyMethodEnter(tag, methodName);
	// 原来的代码
	MethodEventManager.getInstance().notifyMethodExit(tag, methodName);
}
复制代码

Transform 的编写

之后我们建立一个继承自 Transform 的类 ElapseTransform

public class ElapseTransform extends Transform {

    @Override
    public String getName() {
        return ElapseTransform.class.getSimpleName();
    }

    @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 transformInvocation)
            throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
    	// ...查找class文件并对其处理
    }
}
复制代码

这里需要我们实现四个方法,我们分别介绍一下:

  • getName :当前 Transform 的名称
  • getInputTypes :Transform 要处理的数据类型,是一个 ContentType 的 Set,其中 ContentType 有下列取值:
    DefaultContentType.CLASSES
    DefaultContentType.RESOURCES
    
  • getScopes :Transform 的作用范围,是一个 Scope 的 Set,其中 Scope 有以下取值:
    PROJECT
    SUB_PROJECTS
    PROJECT_LOCAL_DEPS
    EXTERNAL_LIBRARIES
    PROVIDED_ONLY
    TESTED_CODE
    
  • isIncremental :是否支持增量编译

这里我们指定的 TransformManager.CONTENT_CLASS 表示处理编译后的字节码文件,而 TransformManager.SCOPE_FULL_PROJECT 表示作用于整个项目,它们都是 TransformManager 预置好的 Set。

当调用该 Transform 时,会调用其 transform 方法,我们在里面就可以进行 class 文件的查找,然后对 class 文件进行处理:

@Override
public void transform(TransformInvocation transformInvocation)
        throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    // 获取输入(消费型输入,需要传递给下一个Transform)
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    for (TransformInput input : inputs) {
        // 遍历输入,分别遍历其中的jar以及directory
        for (JarInput jarInput : input.getJarInputs()) {
            // 对jar文件进行处理
            transformJar(jarInput);
        }
        for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
            // 对directory进行处理
            transformDirectory(directoryInput);
        }
    }
}
复制代码

这里我先通过 transformInvocation.getInputs 获取到了输入,这种输入是消费型输入,需要传递给下一个 Transform,其中包含了 jar 文件与 directory 文件。

然后对 inputs 进行遍历,分别获取其中的 jar 列表以及 directory 列表,再对其进行遍历,分别对 jar 文件及 directory 调用了 transformJartransformDirectory 方法。

class 文件的寻找

jar

对于 jar 文件来说,我们需要遍历其中的 JarEntry,寻找 class 文件,对 class 文件修改后写入一个新的临时 jar 文件,编辑完成后再将其复制到输出路径中。

private void transformJar(TransformInvocation invocation, JarInput input) throws IOException {
    File tempDir = invocation.getContext().getTemporaryDir();
    String destName = input.getFile().getName();
    String hexName = DigestUtils.md5Hex(input.getFile().getAbsolutePath()).substring(0, 8);
    if (destName.endsWith(".jar")) {
        destName = destName.substring(0, destName.length() - 4);
    }
    // 获取输出路径
    File dest = invocation.getOutputProvider()
            .getContentLocation(destName + "_" + hexName, input.getContentTypes(), input.getScopes(), Format.JAR);
    JarFile originJar = new JarFile(input.getFile());
    File outputJar = new File(tempDir, "temp_"+input.getFile().getName());
    JarOutputStream output = new JarOutputStream(new FileOutputStream(outputJar));
    // 遍历原jar文件寻找class文件
    Enumeration<JarEntry> enumeration = originJar.entries();
    while (enumeration.hasMoreElements()) {
        JarEntry originEntry = enumeration.nextElement();
        InputStream inputStream = originJar.getInputStream(originEntry);
        String entryName = originEntry.getName();
        if (entryName.endsWith(".class")) {
            JarEntry destEntry = new JarEntry(entryName);
            output.putNextEntry(destEntry);
            byte[] sourceBytes = IOUtils.toByteArray(inputStream);
            // 修改class文件内容
            byte[] modifiedBytes = modifyClass(sourceBytes);
            if (modifiedBytes == null) {
                modifiedBytes = sourceBytes;
            }
            output.write(modifiedBytes);
            output.closeEntry();
        }
    }
    output.close();
    originJar.close();
    // 复制修改后jar到输出路径
    FileUtils.copyFile(outputJar, dest);
}
复制代码

可以看到,这里主要是以下几步:

getContentLocation
modifyClass

这样就对 jar 文件中的所有 class 文件进行了修改。

directory

对于 directory 来说,我们对其中的文件进行了递归遍历,找到 class 文件则将其修改后放入 Map 中,最后将 Map 中的元素复制到了输出路径下。

private void transformDirectory(TransformInvocation invocation, DirectoryInput input) throws IOException {
    File tempDir = invocation.getContext().getTemporaryDir();
    // 获取输出路径
    File dest = invocation.getOutputProvider()
            .getContentLocation(input.getName(), input.getContentTypes(), input.getScopes(), Format.DIRECTORY);
    File dir = input.getFile();
    if (dir != null && dir.exists()) {
    	// 遍历目录寻找并处理class文件
        traverseDirectory(tempDir, dir);
        // 复制目录
        FileUtils.copyDirectory(input.getFile(), dest);
        for (Map.Entry<String, File> entry : modifyMap.entrySet()) {
            File target = new File(dest.getAbsolutePath() + entry.getKey());
            if (target.exists()) {
                target.delete();
            }
            // 复制class文件
            FileUtils.copyFile(entry.getValue(), target);
            entry.getValue().delete();
        }
    }
}

private void handleDirectory(File tempDir, File dir) throws IOException {
    for (File file : Objects.requireNonNull(dir.listFiles())) {
        if (file.isDirectory()) {
            // 若是目录,递归遍历
            traverseDirectory(tempDir, dir);
        } else if (file.getAbsolutePath().endsWith(".class")) {
            String className = path2ClassName(file.getAbsolutePath()
                    .replace(dir.getAbsolutePath() + File.separator, ""));
            byte[] sourceBytes = IOUtils.toByteArray(new FileInputStream(file));
            // 对class文件进行处理
            byte[] modifiedBytes = modifyClass(sourceBytes);
            File modified = new File(tempDir, className.replace(".", "") + ".class");
            if (modified.exists()) {
                modified.delete();
            }
            modified.createNewFile();
            new FileOutputStream(modified).write(modifiedBytes);
            String key = file.getAbsolutePath().replace(dir.getAbsolutePath(), "");
            modifyMap.put(key, modified);
        }
    }

复制代码

具体逻辑不是很复杂,主要就是找出 class 文件并调用 modifyClass 文件对其进行操作。如果对具体代码感兴趣的读者可以到 GitHub 查看源码。

通过 ASM 织入代码

下面就到了我们最关键的地方,需要我们通过 ASM 来对指定类进行修改了。真正对 class 进行处理的逻辑在 modifyClass 方法中。

private byte[] modifyClass(byte[] classBytes) {
    ClassReader classReader = new ClassReader(classBytes);
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    ClassVisitor classVisitor = new ElapseClassVisitor(classWriter);
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
    return classWriter.toByteArray();
}
复制代码

我们首先需要用到 ASM 中的 ClassReader ,通过它来解析一些我们 class 文件中所包含的信息。

之后我们需要一个 ClassWriter 类,通过它可以实现 class 文件中字节码的写入。

之后,我们自定义了一个 ElapseClassVisitor ,通过 ClassReader.accept 方法使用前面的自定义 ClassVisitor 对这个 class 文件进行『拜访』,在拜访的过程中,我们就可以插入一些逻辑从而实现对 class 文件的编辑。

其实 ClassWriter 也是 ClassVisitor 的实现类,我们只是通过 ElapseClassVisitor 代理了 ClassWriter 而已。

由于我们主要是要对方法进行织入代码,因此在该 ClassVisitor 中我们不需要做太多的事情,只需要在 visitMethod 方法调用也就是方法被调用的时候,返回我们自己实现的 ElapseMethodVisitor 从而实现对方法的织入即可:

这里实际上 ElapseMethodVisitor 并不是 MethodVisitor 的子类,而是 ASM 提供的一个继承自 MethodVisitor 的类 AdviceAdapter 的子类,通过它可以在方法的开始、结尾等地方插入自己需要的代码。

class ElapseMethodVisitor extends AdviceAdapter {
    private final MethodVisitor methodVisitor;
    private final String methodName;
	// ...

    public ElapseMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) {
        super(Opcodes.ASM6, methodVisitor, access, name, desc);
        this.methodVisitor = methodVisitor;
        this.methodName = name;
    }
    // ...其他代码
}
复制代码

这里我们保存了 methodVisitormethodName ,前者是为了后期通过它来对 class 文件进行织入代码,而后者是为了在后期将其传递给 MethodEventManager 从而进行通知。

注解处理

接下来,我们可以通过重写 visitAnnotation 方法来在访问方法的注解时进行处理,从而判断该方法是否需要织入,同时获取注解中的 tag。

private static final String ANNOTATION_TRACK_METHOD = "Lcom/n0texpecterr0r/elapse/TrackMethod;";
private boolean needInject;
private String tag;

@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);
    if (desc.equals(ANNOTATION_TRACK_METHOD)) {
        needInject = true;
        return new AnnotationVisitor(Opcodes.ASM6, annotationVisitor) {
            @Override
            public void visit(String name, Object value) {
                super.visit(name, value);
                if (name.equals("tag") && value instanceof String) {
                    tag = (String) value;
                }
            }
        };
    }
    return annotationVisitor;
}
复制代码

这里首先判断了注解的签名是否与我们需要的注解 TrackMethod 相同(具体签名规则这里不再介绍,可以自行百度,其实就是方法签名那一套,注意里面的分号)

若该注解是我们所需要的注解,则将 needInject 置为 true,同时从该注解中获取 tag 的值,

这样我们在后续就只需要判断是否 needInject 就能知道哪些方法需要被织入了。

代码的织入

接下来我们就可以正式开始织入工作了,我们可以通过重写 onMethodEnter 以及 onMethodExit 来监听方法的进入及退出:

@Override
protected void onMethodEnter() {
    super.onMethodEnter();
    handleMethodEnter();
}

@Override
protected void onMethodExit(int opcode) {
    super.onMethodExit(opcode);
    handleMethodExit();
}
复制代码

两段代码及其相似,只是最后调用的方法名不同,所以这里仅仅以 handleMethodEnter 举例。

在 ASM 中,通过 MethodWriter.visitMethodInsn 方法可以调用类似字节码的指令来调用方法。比如

visitMethodInsn(INVOKESTATIC, 类签名, 方法名, 方法签名);

这样的方式就可以调用一个类下的 static 方法。如果这个方法需要参数,我们可以通过 visitVarInsn 方法来调用如 ALOAD 等指令将变量入栈。整个过程其实是与字节码中的调用形式比较类似的。

如果只是调用一个 static 方法还好,但我们这里是需要调用一个单例类下的具体方法,如

MethodEventManager.getInstance().notifyMethodEnter(tag, methodName);

这样的代码恐怕除了对字节码很熟悉的人很难有人能直接想到它用字节码如何表示了。我们可以通过以下的两种方法来解决:

1. 通过 javap 查看字节码

因此我们可以写个单例的调用 Demo,之后通过 javap -v 来查看其生成的字节码,从而了解到调用的字节码大概是一个怎样的顺序:

和我一起用 ASM 实现编译期字节码织入

可以很明显的看到,这里先通过 INVOKESTATIC 调用了 getInstance 方法,然后通过 LDC 将两个字符串常量放置到了栈顶,最后通过 INVOKEVIRTUAL 调用 notify 方法进行最后的调用。

那我们可以模仿这个过程,调用 ASM 中的对应方法来完成类似的过程,于是写出了如下的代码,其中 visitLdcInsn 的效果类似于字节码中的 LDC。

private void handleMethodEnter() {
    if (needInject && tag != null) {
        methodVisitor.visitMethodInsn(INVOKESTATIC, METHOD_EVENT_MANAGER, 
                "getInstance", "()L"+METHOD_EVENT_MANAGER+";");
        methodVisitor.visitLdcInsn(tag);
        methodVisitor.visitLdcInsn(methodName);
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, METHOD_EVENT_MANAGER, 
                "notifyMethodEnter", "(Ljava/lang/String;Ljava/lang/String;)V");
    }
}
复制代码

这样,就可以织入我们想要的代码了。

2. 通过 ASM Bytecode 插件查看

前面这种通过字节码查看的过程确实比较麻烦,因此我们还有另外的一种方法来简化这个步骤,有大神写了一个名为 「ASM Bytecode outline」的 IDEA 插件,我们可以通过它直接查看对应的 ASM 代码。

安装该插件后,在需要查看的代码上 点击右键->Show ByteCode 即可查看对应的 ASM 代码,效果如下:

和我一起用 ASM 实现编译期字节码织入

我们从中提炼出自己需要的代码即可。

两种方法各有优劣,读者可以根据自己的需求使用不同的方式实现。

通过前面的一系列步骤,这个 ASM 织入的核心功能我们就已经实现了,如果还需要获取函数的参数等扩展,只需要知道对应的字节码实现,剩下的都很容易实现,这里由于篇幅有限就不细讲了。

打包为 Gradle 插件

接下来我们来进行最后的一步,将这个库打包为一个 Gradle Plugin,我们新建一个 ElapsePlugin 类,继承自 Plugin<Project> ,并在其中注册我们的 ElapseTransform

public class ElapsePlugin implements Plugin<Project> {
    @Override
    public void apply(@NotNull Project project) {
        AppExtension appExtension = project.getExtensions().findByType(AppExtension.class);
        assert appExtension != null;
        appExtension.registerTransform(new ElapseTransform(project));
    }
}
复制代码

之后我们在 build.gradle 中加入如下的 gradle 代码,描述我们 pom 的信息:

apply plugin: 'maven'

uploadArchives {
    repositories.mavenDeployer {
        repository(url: uri('../repo'))
        pom.groupId = 'com.n0texpecterr0r.build'
        pom.artifactId = 'elapse-asm'
        pom.version = '1.0.0'
    }
}
复制代码

最后我们在 src/main 下新建一个 resources/META-INF/gradle-plugins 文件夹,在该文件夹下建立 <插件名>.properties 文件。

在该文件中,按如下的方式填写:

implementation-class = <Plugin所在目录> ,比如我这里就是 implementation-class = com.n0texpecterr0r.elapseasm.ElapsePlugin

这样,我们就能够通过运行 uploadArchives 这个 Gradle 脚本来生成对应的 jar 包了。到此为止,我们的函数调用插桩的 Gradle Plugin 就开发完成了。

效果展示

我们可以在需要使用的项目中将其添加到 classpath 中:

repositories {
    //...
    maven {
        url uri("repo")
    }
}

dependencies {
    // ...
    classpath 'com.n0texpecterr0r.build:elapse-asm:1.0.0'
}
复制代码

之后在 app module 下将其 apply 进来:

apply plugin: 'com.n0texpecterr0r.elapse-asm'
复制代码

我们可以写一个 Demo 测试一下效果:

public class MainActivity extends AppCompatActivity {

    private static final String TAG_TEST = "test";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MethodEventManager.getInstance().registerMethodObserver(TAG_TEST, new MethodObserver() {
            @Override
            public void onMethodEnter(String tag, String methodName) {
                Log.d("MethodEvent", "method "+ methodName + " enter at time " + System.currentTimeMillis());
            }

            @Override
            public void onMethodExit(String tag, String methodName) {
                Log.d("MethodEvent", "method "+ methodName + " exit at time " + System.currentTimeMillis());
            }
        });
        test();
    }

    @TrackMethod(tag = TAG_TEST)
    public void test() {
        try {
            Thread.sleep(1200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

运行程序,可以发现,Logcat 中成功打印了我们需要的信息:

和我一起用 ASM 实现编译期字节码织入

也就是说,我们的代码被成功到字节码中了。让我们看看编译后生成的字节码,我们可以打开 elapse-demo/build/intermediates/transforms/ElapseTransform/debug/33/MainActivitiy.class

和我一起用 ASM 实现编译期字节码织入

看得出来,我们的代码被成功地插入了字节码中。

实现 Hugo

我们接下来通过它来尝试实现 Hugo 的打印方法耗时功能,可以新建一个 TimeObserver

public class TimeObserver implements MethodObserver {
    private static final String TAG_METHOD_TIME = "MethodCost";
    private Map<String, Long> enterTimeMap = new HashMap<>();
    @Override
    public void onMethodEnter(String tag, String methodName) {
        String key = generateKey(tag, methodName);
        Long time = System.currentTimeMillis();
        enterTimeMap.put(key, time);
    }
    @Override
    public void onMethodExit(String tag, String methodName) {
        String key = generateKey(tag, methodName);
        Long enterTime = enterTimeMap.get(key);
        if (enterTime == null) {
            throw new IllegalStateException("method exit without enter");
        }
        long cost = System.currentTimeMillis() - enterTime;
        Log.d(TAG_METHOD_TIME, "method " + methodName + " cost "
                + (double)cost/1000 + "s" + " in thread " + Thread.currentThread().getName());
		enterTimeMap.remove(key);
    }
    private String generateKey(String tag, String methodName) {
        return tag + methodName + Thread.currentThread().getName();
    }
}
复制代码

这里我们以 tag + methodName + currentThread.name 来作为 key,避免了多线程下的调用导致的干扰,在方法进入时记录下开始时间,退出时计算时间差即可得到方法的耗时时间。

我们在 Application 中对其进行注册后,就可以在运行后看到效果了:

和我一起用 ASM 实现编译期字节码织入

我们开 10 个线程,来分别运行 test ,我们可以看看效果:

private ExecutorService mExecutor = Executors.newFixedThreadPool(10);

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    for (int i = 0; i < 10; i++) {
        mExecutor.execute(this::test);
    }
}
复制代码

可以看到,仍然可以正常统计方法的调用时间:

和我一起用 ASM 实现编译期字节码织入

总结

通过 ASM + Transform API,我们可以很方便地在 class 被打包为 dex 文件之前对字节码进行编辑,从而在代码的任意位置插入我们需要的逻辑,本文只是一个小 Demo 的演示,从而让读者们能够了解到 ASM 的强大。通过 ASM 能够实现的功能其实更加丰富。目前在国内关于 ASM 的相关文章还比较匮乏,如果想要进一步了解 ASM 的功能,读者们可以到 这里 查看 ASM 的官方文档。

其实本文的 Demo 还有更多功能可以扩展,比如函数参数及返回值的信息的携带,对整个类的方法进行插桩等等,读者可以根据已有知识,尝试对这些功能进行扩展,由于篇幅有限这里就不再赘述了,本质上都是插入对应的字节码指令。

原文  https://juejin.im/post/5e153559e51d45413846f8e0
正文到此结束
Loading...