学习大神的思路,动手实现字节码插桩功能

640?wx_fmt=jpeg


/   今日科技快讯   /


8月19日华为公司19日晚发布媒体声明,称反对美国商务部将另外46家华为实体列入“实体名单”,呼吁美国政府停止对华为的不公正对待,将华为移出“实体名单”。8月19日晚间,美国商务部宣布将对华为的临时采购许可证延长90天,并决定将会把46家华为附属公司加入“实体名单”。


/   作者简介   /


本篇文章来自N0tExpectErr0r的投稿,文章用实例来展现如何在编译期实现字节码插装,希望对大家有所帮助!同时也感谢作者贡献的精彩文章。


N0tExpectErr0r的博客地址:

http://blog.n0texpecterr0r.cn/


/   起因   /


这两天摸鱼的时候,突然发现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的功能,我们就只需要在方法调用前记录时间,在方法调用后计算时间差即可。


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


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


Hugo地址:

https://github.com/JakeWharton/hugo


/   基本原理   /


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


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


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


640?wx_fmt=jpeg


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


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


/   实现思路   /


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


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


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


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


640?wx_fmt=jpeg


/   Show me your 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调用了transformJar及 transformDirectory方法。


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(08);
    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;
    }
    // ...其他代码
}


这里我们保存了methodVisitor及methodName,前者是为了后期通过它来对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);


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


通过javap查看字节码


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


640?wx_fmt=jpeg


可以很明显的看到,这里先通过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");
    }
}


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


通过ASM Bytecode插件查看


前面这种通过字节码查看的过程确实比较麻烦,因此我们还有另外的一种方法来简化这个步骤,有大神写了一个名为「ASM Bytecode outline」的 IDEA 插件,我们可以通过它直接查看对应的ASM代码。安装该插件后,在需要查看的代码上 点击右键-Show ByteCode即可查看对应的ASM代码,效果如下:


640?wx_fmt=jpeg


我们从中提炼出自己需要的代码即可。两种方法各有优劣,读者可以根据自己的需求使用不同的方式实现。通过前面的一系列步骤,这个ASM织入的核心功能我们就已经实现了,如果还需要获取函数的参数等扩展,只需要知道对应的字节码实现,剩下的都很容易实现,这里由于篇幅有限就不细讲了。


打包为 Gradle 插件


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


 
 

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");
    }
}


之后我们在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中成功打印了我们需要的信息:


640?wx_fmt=jpeg


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


640?wx_fmt=jpeg


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


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


640?wx_fmt=jpeg


我们开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);
    }
}


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


640?wx_fmt=jpeg


/   总结   /


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


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


推荐阅读:

真香系列,开箱即用的自定义Banner

动画代码太丑,用Kotlin DSL来拯救!

分享一个能让你的代码变得更整洁的技巧


欢迎关注我的公众号

学习技术或投稿


640.png?


640?wx_fmt=jpeg

长按上图,识别图中二维码即可关注


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值