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

你好,我是 N0tExpectErr0r,一名热爱技术的 Android 开发html

个人我的博客:blog.N0tExpectErr0r.cnjava

本文 Demo 地址:github.com/N0tExpectEr…android

本文已受权公众号『郭霖』发布:mp.weixin.qq.com/s/aBYA1mwUa…git

原由

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

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

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

就能够实如今 Logcat 中打印以下的日志:bash

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

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

但 Hugo 仅仅提供了打印方法执行相关信息的功能,所以就开始思考是否可以基于它的思路进行一些扩展,实如今方法调用先后执行指定逻辑的功能呢?app

若是能实现这样一个库,那对于 Hugo 的功能,咱们就只须要在方法调用前记录时间,在方法调用后计算时间差便可。maven

同时若是还须要一个统计应用中某个方法调用次数的功能,也只须要在方法调用时执行计数的逻辑便可。

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

基本原理

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

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

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

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

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

实现思路

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

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

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

这样的话,咱们只须要在方法的调用先后,经过 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:用于向其注册某个 TAG 对应的监听
  • notifyMethodEnter:用于通知对应 TAG 的监听该方法调用
  • notifyMethodExit:用于通知对应 TAG 的监听该方法退出

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

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:要处理编译后的字节码文件(jar 包或目录)
    • DefaultContentType.RESOURCES:要处理标准的 Java 资源
  • getScopes:Transform 的做用范围,是一个 Scope 的 Set,其中 Scope 有如下取值:
    • PROJECT:只处理当前项目
    • SUB_PROJECTS:只处理子项目
    • PROJECT_LOCAL_DEPS:只处理当前项目的本地依赖,例如 jar, aar
    • EXTERNAL_LIBRARIES:只处理外部的依赖库
    • PROVIDED_ONLY:只处理本地或远程以 provided 形式引入的依赖库
    • 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);
}
复制代码

能够看到,这里主要是如下几步:

  1. 经过 getContentLocation 方法获取到了输出路径,
  2. 构建了一个临时的输出 jar 文件
  3. 遍历原 jar 文件的 entry,将其中的 class 文件调用 modifyClass 进行修改,而后放入该临时 jar 文件
  4. 将该临时 jar 文件复制到输出路径。

这样就对 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 来查看其生成的字节码,从而了解到调用的字节码大概是一个怎样的顺序:

能够很明显的看到,这里先经过 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 织入的核心功能咱们就已经实现了,若是还须要获取函数的参数等扩展,只须要知道对应的字节码实现,剩下的都很容易实现,这里因为篇幅有限就不细讲了。

打包为 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 中成功打印了咱们须要的信息:

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

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

实现 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 中对其进行注册后,就能够在运行后看到效果了:

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

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

相关文章
相关标签/搜索