Android ASM插桩探索实战详情

目录
  • 前言
  • ASM的作用是什么?
  • 如何使用ASM?
    • 基本使用方式
    • 自定义ClassVisitor
    • ASM ByteCode Viewer
    • 如何将ASM运用都我们的实际项目中来?
  • 引入工程
    • Android Gradle Plugin
    • 创建插件项目
    • 配置插件
    • 实现插件
    • 发布插件
    • 应用插件
    • Android Transform
  • 方法节流
  • 方法耗时日志
  • 如何调试
  • 发布线上的额外工作
    • 插件项目的maven仓库
    • 编译影响评估
  • Tips
  • 总结

前言

我们都知道,在Android编译过程中,Java代码会被编译成Class文件,Class文件再被打进Dex文件,虚拟机最终会加载Dex文件去执行。

插桩,就是干涉代码的编译过程,在编译期间生成新的代码或者修改已有的代码。

常用的EventBus、ARouter,内部就是使用了APT(Annotation Process Tool),在编译的最开始解析Java文件中的注解,并生成新的Java文件。

但如果有以下两个需求:

  • 给Activity的attach()方法加日志
  • 将第三方库中所有调用getDeviceId()的地方替换为我们自己的方法,使其符合隐私规范

这两个需求一个是需要修改Android SDK的Activity文件,一个是需要修改三方库中的某个方法。而我们集成它们的方式是通过Jar包/AAR,本质上也就是Class文件。这时候就需要我们能够在编译阶段去修改Class文件,这也就是ASM发挥作用的地方。

通过本文,你可以解决如下问题:

  • ASM的作用是什么?
  • 如何使用ASM?
  • 如何将ASM运用到我们的实际项目中来?

ASM的作用是什么?

在介绍ASM插桩之前,首先来回顾一下Java Class文件。在AS中,我们可以看到打开一个Class文件是这样的:

但其实这是IDE为了方便开发者查阅,特意解析渲染了CLASS文件。如果直接拖进编辑器查看这个文件的话,我们可以看到它其实是这样的:

上图是CLASS文件的16进制代码。一般人都看不懂这些代码的含义...但既然AS可以将这些代码解析成开发者可以看懂的样子,说明CLASS文件肯定是遵循某个格式规范的。所以,一个熟悉CLASS文件格式规范的开发者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。

ASM的开发者就是这么做的,并且提供一套完整的API帮助我们在不需要了解CLASS文件格式规范的情况下,可以解析并修改CLASS文件

如何使用ASM?

基本使用方式

下面我们就来使用一下ASM,看一下它能达到的效果。假设现在我们需要统计MainActivity所有方法的耗时,原先的MainActivity.Class文件是这样的:

用ASM修改过后的MainActivity.Class文件:

 具体的实现代码:

// 读取Class文件
String clazzFilePath = "/Users/xiaozhi/AndroidStudioProjects/ASMTest/app/build/intermediates/javac/debug/classes/com/xiaozhi/asmtest/MainActivity.class";
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);

// 写入Class文件
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(clazzFilePath);
fos.write(bytes);
fos.flush();
fos.close();

首先在第6行,通过ClassReader.accept(classVisitor, parsingOptions)读取Class文件。然后将修改完的字节码用FileOutputStream写回原文件,原先的Class代码也就被修改了。但这里我们看不到是怎么修改的,因为修改其实就发生在读取阶段,ClassReader负责读取解析Class文件,遇到相应节点后,调用ClassVisitor中的方法去修改相应的节点代码(4、5行)。

这里涉及到两个类,ClassWriter与MethodTimeConsumeClassVisitor,这两个类都继承于ClassVisitor。结合第9行我们可以猜测,ClassWriter肯定可以记录我们修改后的字节码。既然ClassWriter是用来记录的,而第6行ClassReader.accept(classVisitor, parsingOptions)读取Class文件又只能接收一个classVisitor,那我们怎么用另一个ClassVisitor去修改Class文件呢?

我们可以看到ClassVisitor有这么一个构造函数:

public ClassVisitor(final int api, final ClassVisitor classVisitor)

所以我们第5行的代码,实际上是用自定义的ClassVisitor-MethodTimeConsumeClassVisitor,代理了ClassWriter,在需要修改的Class节点复写方法进行修改就可以了。

另外我们额外了解一下构造函数中的几个参数。

// 接收Flag参数,用于设置方法的操作数栈的深度。COMPUTE_MAXS可以自动帮我们计算stackSize。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// 接收api与ClassVisitor。 Opcodes.ASM4~Opcodes.ASM9标识了ASM的版本信息
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);

// 接收ClassVisitor与parsingOptions参数。 parsingOptions用来决定解析Class的方式,SKIP_FRAMES代表跳过MethodVisitor.visitFrame方法
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);

自定义ClassVisitor

下面我们具体看一下怎么通过自定义ClassVisitor修改Class文件。

public class MethodTimeConsumeClassVisitor extends ClassVisitor {
    private String mOwner;

    public MethodTimeConsumeClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        mOwner = name;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new TimeConsumeMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
    }
}

我们可以看到第9行与第15行,分别是visit方法与visitMethod方法,对应的是访问Class文件头部与Class文件方法这两个节点。

类似的还有很多节点:

visit visitSource? visitOuterClass? ( visitAnnotation |
   visitAttribute )*
   ( visitInnerClass | visitField | visitMethod )*
   visitEnd

我们要统计MainActivity所有方法的耗时,就需要重写visitMethod方法。第15行的visitMethod返回了一个MethodVisitor,顾名思义就是用来遍历修改Method,跟ClassVisitor是一个道理只不过维度不同罢了。类似的还有AnnotationVisitor与FiledVisitor,它们分别在visitAnnotation和visitField方法中返回,用来访问修改注解与字段。

然后我们来看这个MethodVisitor是怎么修改方法的:

static class TimeConsumeMethodVisitor extends AdviceAdapter {
    private final String methodName;
    private final int access;
    private final String descriptor;
    private final String owner;

    private static final String METHOD_TIME_CONSUME_LOG_TAG = "METHOD_TIME_CONSUME_ASM_HOOK";
    private static final String METHOD_TIME_CONSUME_LOG = "method time consume:";

    protected TimeConsumeMethodVisitor(String owner, int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
        this.owner = owner;
        this.methodName = name;
        this.access = access;
        this.descriptor = descriptor;
    }

    @Override
    protected void onMethodEnter() {
        System.out.println("TimeConsumeMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);

        visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        visitVarInsn(LSTORE, 1);

        super.onMethodEnter();
    }

    @Override
    protected void onMethodExit(int opcode) {
        System.out.println("TimeConsumeMethodVisitor onMethodExit. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);

        visitLdcInsn(METHOD_TIME_CONSUME_LOG_TAG);
        visitTypeInsn(NEW, "java/lang/StringBuilder");
        visitInsn(DUP);
        visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        visitLdcInsn(METHOD_TIME_CONSUME_LOG);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        visitVarInsn(LLOAD, 1);
        visitInsn(LSUB);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        visitLdcInsn("ms" + ", clazz:" + owner + ", method:" + methodName);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        visitMethodInsn(INVOKESTATIC, "com/taobao/yyds/common/utils/Logger", "d", "(Ljava/lang/String;Ljava/lang/String;)V", false);
        super.onMethodExit(opcode);
    }
}

在第19行onMethodEnter()方法中,我们实现了

long startTime = System.currentTimeMillis();

在第29行onMethodExit()方法中,我们实现了

Logger.d("METHOD_TIME_CONSUME_ASM_HOOK", "method time consume:" + (System.currentTimeMillis() - var1) + "ms, clazz:com/xiaozhi/asmtest/MainActivity, method:");

至于其中所用的API,我们可以通过函数名大致推断出什么意思,感兴趣的话可以去学习这些API的使用。但因为刚入门,我这里使用了一个偷懒的方式:ASM ByteCode Viewer。

ASM ByteCode Viewer

ASM ByteCode Viewer是专门用于ASM插桩的AS插件。

安装该插件后,我们可以很方便地查看一个Class文件怎么用ASM的API去写出来:

如果我想很快速地知道如何通过ASM API去给方法加耗时日志,只需要先在本地Java文件中写好这一段逻辑,然后通过插件查看对应的API是怎么样的就可以了。但建议还是要多了解下这些API,因为我们写的Java代码可能不是通用的,在其它Java文件中不一定就能顺利地编译成功,因此往往会有需要进行适配的地方,排查的过程中就需要我们了解API才行了。

到这里,ASM的基本使用就已经讲好了。如果感兴趣可以参考官方文档asm.ow2.io/asm4-guide.…去实践。

如何将ASM运用都我们的实际项目中来?

上一节我们已经知道如何用ASM对一个Class文件进行修改,那么怎么运用到我们的项目中来呢?Android打包过程中会将Class文件打包成Dex文件,在这个阶段我们可以借助AGP(Android Gradle Plugin)与Android Transform来遍历访问到所有需要的Class文件,再通过ASM去修改。

引入工程

Android Gradle Plugin

自定义插件一共分为5个步骤:

  • 创建插件项目
  • 配置插件
  • 实现插件
  • 发布插件
  • 应用插件

创建插件项目

跟其它子模块一样,我们需要创建一个插件模块,然后在根目录的settings.gradle中引入该模块。

配置插件

首先,插件模块的文件目录需要严格遵守以下目录结构(因为我们选择用groovy实现插件,所以要用groovy文件夹):

main
├── groovy
├── resources
    ├── META-INF
        ├── gradle-plugins
            ├── *.properties

上图中的配置文件需要特别注意,该配置文件代表着插件id->插件实现类的映射。

其它项目应用插件时所用的插件id,就是配置文件的文件前缀com.yyds.asm.plugin。而实际的实现类就是com.xiaozhi.plugin.ASMPlugin。

另外,我们需要在build.gradle中配置如下内容:

plugins {
    id 'groovy'
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
}

sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }

        resources {
            srcDir 'src/main/resources'
        }
    }
}

实现插件

接下来就是实现我们自定义的插件了,我们可以在ASMPlugin中写我们需要执行的逻辑:

class ASMPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("ASMPlugin apply")
    }
}

发布插件

插件项目写完后,我们需要将其发布到maven仓库中去(这里可以选择先将其发布到本地maven仓库),从而让其它模块可以方便地进行依赖。

我们需要在build.gradle中添加以下代码:

uploadArchives {
    repositories {
        mavenDeployer {
            //设置插件的GAV参数
            pom.groupId = 'com.xiaozhi.plugin.asm'
            pom.artifactId = 'asmArt'
            pom.version = '1.0.1'
            //文件发布到下面目录
            repository(url: uri('../maven_repo'))
        }
    }
}

sync后,我们就可以在gradle tasks中看到上传插件的task:

执行task,插件就会发布到本地的maven仓库了,我们可以在本地的maven_repo文件夹中找到。

应用插件

现在,我们的项目就可以很方便地依赖这个插件了。

只用做两个步骤:

  1. 在工程根目录build.gralde添加maven仓库与插件依赖:
buildscript {
    repositories {
        ···
        maven { url uri('./maven_repo') }
    	···
    }
    dependencies {
        ···
        classpath "com.xiaozhi.plugin.asm:asmArt:1.0.1"
        ···
    }
}
  • 在想要依赖插件的项目的build.gradle中应用插件:
plugins {
    id 'com.xiaozhi.plugin'
}

可以看到,这个id就是对应的插件项目中配置文件的前缀名

Android Transform

现在假设app模块已经应用了我们的ASM插件,那么还需要使用Transform才能访问到app模块在编译过程中产生/依赖的所有Class文件。

自定义一个Transform:

public class ASMTransform extends Transform {

    // transfrom名称
    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    // 输入源,class文件
    @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 {

    }
}

我们主要看第29行transform()方法,在这里我们就能访问到app模块的所有Class文件。

实现如下:

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    if (!transformInvocation.isIncremental()) {
        //不是增量编译删除所有的outputProvider
        transformInvocation.getOutputProvider().deleteAll();
    }
    // 获取输入源
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    inputs.forEach(transformInput -> {
        Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
        Collection<JarInput> jarInputs = transformInput.getJarInputs();
        directoryInputs.forEach(new Consumer<DirectoryInput>() {
            @Override
            public void accept(DirectoryInput directoryInput) {
                try {
                    // 处理输入源
                    handleDirectoryInput(directoryInput);
                } catch (IOException e) {
                    System.out.println("handleDirectoryInput error:" + e.toString());
                }
            }
        });
        for (DirectoryInput directoryInput : directoryInputs) {
            // 获取output目录
            File dest = transformInvocation.getOutputProvider().getContentLocation(
                    directoryInput.getName(),
                    directoryInput.getContentTypes(),
                    directoryInput.getScopes(),
                    Format.DIRECTORY);
            //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
            try {
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            } catch (IOException e) {
                System.out.println("output copy error:" + e.toString());
            }
        }

        for (JarInput jarInput : jarInputs) {
            // 获取output目录
            File dest = transformInvocation.getOutputProvider().getContentLocation(
                    jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
            try {
                FileUtils.copyFile(jarInput.getFile(), dest);
            } catch (IOException e) {
                System.out.println("output copy error:" + e.toString());
            }
        }
    });
}

这里的逻辑主要是三端:

  • 获取输入源

第9行获取到的TransformInput中可以访问到所有的DirectoryInput和JarInput,分别代表着我们项目中的Class文件与依赖的JAR包/AAR包中的Class文件。DirectoryInput和JarInput都继承于QualifiedContent,调用getFile()方法就可以拿到Class文件的所有信息了。

  • 处理输入源

获取Class文件后,其实我们就可以用ASM去修改它了。相当于把我们之前ASM修改Class文件的代码复制过来就可以了,这一部分留到下一节中讲。

  • 将输入源文件拷贝到目标文件中

处理完之后,我们还要记得把输入源文件拷贝到输出路径中去,否则下一个transform可能就要失败了,因为它找不到输入源了。第27行transformInvocation.getOutputProvider().getContentLocation()可以确保我们获取到最终的输出路径。

现在Transform写好了,但我们还没有应用。应用很简单,只需要在插件中注册一下就好了:

class ASMPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def android = project.getExtensions().findByType(AppExtension)
        android.registerTransform(new ASMTransform())
    }
}

然后,重新发布插件到maven仓库,sync一下,我们就可以在app模块的gradle tasks中看到我们刚写好的transform了:

至此,所有链路都已经走通了,我们知道ASM如何修改Class文件,并可以利用AGP与Transfrom应用到我们的工程中来。下面我们就用这条链路来实现一下方法节流。

方法节流

Android中最常见的方法节流就是防重复点击。假设当用户在首页2s内频繁点击了商品或者误触了商品时,我们期望只打开一次商详页面,这时候就需要对点击事件做节流。

首先我们需要定义一个注解,并声明到点击事件上,同时支持设置节流时长duration:

private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        @MethodThrottle(duration = 3000)
        public void onClick(View view) {

        }
}

接下来就是ASM的舞台了。回顾我们上一节中处理输入源相关的代码

Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
directoryInputs.forEach(new Consumer<DirectoryInput>() {
    @Override
    public void accept(DirectoryInput directoryInput) {
        try {
            // 处理输入源
            handleDirectoryInput(directoryInput);
        } catch (IOException e) {
            System.out.println("handleDirectoryInput error:" + e.toString());
        }
    }
});

在第7行handleDirectoryInput()方法中,我们利用ASM修改Class文件:

/**
 * 处理文件目录下的class文件
 */
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
    List<File> files = new ArrayList<>();
    //列出目录所有文件(包含子文件夹,子文件夹内文件)
    listFiles(files, directoryInput.getFile());
    for (File file: files) {
        ClassReader classReader = new ClassReader(new FileInputStream(file.getAbsolutePath()));
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        MethodThrottleClassVisitor methodThrottleClassVisitor = new MethodThrottleClassVisitor(Opcodes.ASM5, classWriter);
        classReader.accept(methodThrottleClassVisitor, ClassReader.SKIP_FRAMES);

        byte[] code = classWriter.toByteArray();
        FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
        fos.write(code);
        fos.close();
    }
}

关键是MethodThrottleClassVisitor类,我们看它主要是怎么实现的:

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
    throttleMethodVisitor = new ThrottleMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
    return throttleMethodVisitor;
}

visitMethod()方法返回一个自定义的MethodVisitor。ThrottleMethodVisitor在访问每个方法时,若发现方法声明了@MethodThrottle注解,就会插入节流代码:

@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    System.out.println("ThrottleMethodVisitor visitAnnotation. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", annotationDesc:" + desc);
    throttle = "Lcom/taobao/yyds/common/annotation/MethodThrottle;".equals(desc);
    ···
}
@Override
protected void onMethodEnter() {
    System.out.println("ThrottleMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", throttle:" + throttle);
    if (throttle) {
        // 插入方法节流代码
    }
}

具体的代码就不放上去了,通过ASM ByteCode Viewer可以很方便地生成。这样我们在平时代码中要做方法节流时,只需要给方法声明一个注解就可以了。

方法耗时日志

类似的,我们可以用这个链路实现很多AOP逻辑。给方法加耗时日志这一点,在上面的ASM基本使用那一节中其实已经讲过了。

运用到工程中来,其实就跟方法节流一样,自定义一个注解,然后在Transform中加一点处理输入流的逻辑就好了:

/**
 * 处理文件目录下的class文件
 */
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
    List<File> files = new ArrayList<>();
    //列出目录所有文件(包含子文件夹,子文件夹内文件)
    listFiles(files, directoryInput.getFile());
    for (File file: files) {
        // 方法节流
        methodThrottleASM(file);

        // 方法耗时
        methodTimeConsumeASM(file);
    }
}

如何调试

另外有一点我个人觉得还是挺重要的,那就是Gradle插件该如何调试。即使有ASM ByteCode Viewer插件的帮助,我们在插件中写的ASM代码也不可能一键完成,很大概率会碰到各种各样的编译错误问题。本地打日志又比较麻烦,所以调试手段是必不可少的。篇幅受限,这里就不额外写了,可以参考Android Studio调试Gradle插件详情

发布线上的额外工作

虽然我们已经集成了ASM插件模块,但并不意味着这样就能上线了。至少还需要完成以下的工作才行。

插件项目的maven仓库

因为在调试阶段,插件的maven仓库是用的本地的maven仓库。但如果要集成进CI,肯定是需要线上的maven仓库的,所以到时候需要申请上传到某个maven仓中才行。

编译影响评估

第一点是必须保证CI打包时没问题。第二点就是看这样做是否会影响到编译时长,毕竟Transform是在编译阶段加了一个Task。如果对编译时长有比较大的影响,还需要额外做一些编译优化的工作。可以用./gradlew --profile --rerun-tasks assembleDebug命令查看各环节的编译耗时。

Tips

在做这个Demo的过程中,因为自己也是第一次接触,遇到了不少坑,拿几点贴一下:

  • 每次plugin改动都要重新发布一下,否则plugin中transform、asm的逻辑都不会更新,因为拿的是maven仓中的。
  • transform每次debug前都要clean一下,否则debug不进去。
  • 即使transform没修改任何东西,也需要将源文件jar directory拷贝到目标文件。否则最后编译出的build/transforms文件夹中会少很多jar包,造成启动时找不到各种Class而崩溃。
  • classpath引入后,造成启动后某so崩溃。一直以为是插件写的有什么问题,排查很久后也没找到原因。最后拿崩溃栈去找so库接口人,升级so版本后问题就解决了。

总结

通过本篇文章,我们了解到ASM的作用,学会ASM基本API的使用,进而利用AGP与Transform将ASM运用到实际项目中来。实战中,ASM可以实现常见的AOP逻辑,如方法节流与方法耗时日志。不仅如此,当我们以后为需要修改三方库代码而发愁时,或许可以想想,ASM能帮助我们搞定吗?

到此这篇关于Android ASM插桩探索实战详情的文章就介绍到这了,更多相关Android ASM插桩探内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android下hook点击事件的示例

    Hook是一种思想,也就是将原来的事件,替换到我们自己的事件,方便我们做一些切入处理.目的是不修改原来的代码,同时也避免遗漏的N多类里面处理. 最近需要在现有的app中设置统计埋点.去业务代码里埋的话似乎耦合度太高.所以决定使用hook的方法对事件进行埋点处理. 这里先记一下对点击事件hook的基本流程. 1.先建一个代理类实现View.OnClickListener,用来做点击后的后续处理. import android.view.View; /** * 实现点击监听 */ public cl

  • Android性能优化之plt hook与native线程监控详解

    目录 背景 native 线程创建 PLT PLT Hook xhook bhook plt hook总结 背景 我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有

  • Android手机屏幕同步工具asm.jar

    有时候可能需要将手机上的一些操作投影出来,比如一些App Demo的展示等.其实,有专门的硬件设备能干这件事儿,但没必要专门为展示个Demo去花钱买硬件设备.正好,对于Android系统的手机,有一个开源的jar包能干这事儿:Android Screen Monitor(asm.jar) 步骤: 一 . 下载附件 asm.jar 官网 https://code.google.com/p/android-screen-monitor/ 二. 放到任意目录下, 这里放到D盘根目录; 将asm.jar

  • Android开发AsmClassVisitorFactory使用详解

    目录 前言 AsmClassVisitorFactory 新的Extension 实战 ClassVisitor 实际代码分析 个人观点 前言 之前就和大家介绍过AGP(Android Gradle Plugin) 7.0.0版本之后Transform 已经过期即将废弃的事情.而且也简单的介绍了替换的方式是Transform Action,经过我这一阵子的学习和调研,发现只能说答对了一半吧.下面介绍个新东西AsmClassVisitorFactory. com.android.build.api

  • android的got表HOOK实现代码

    概述 对于android的so文件的hook根据ELF文件特性分为:Got表hook.Sym表hook和inline hook等. 全局符号表(GOT表)hook,它是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数. Androd so注入和函数Hook(基于got表)的步骤: 1.ptrace附加目标pid进程; 2.在目标pid进程中,查找内存空间(用于存放被注入的so文件的路径和so中被调用的函

  • ASM的tree api对匿名线程的hook操作详解

    目录 背景 ASM介绍 class文件 fields methods InsnList Signature 实战部分 解决“匿名”Thread 最后 背景 看完本章,你将会学习到用ASM的tree api进行对匿名线程的hook操作,同时也能够了解到asm相关的操作和背景知识介绍!对于ASM插桩来说,可能很多人都不陌生了,但是大多数可能都停留在core api上,对于现在市面上的一些插桩库,其实很多都用tree api进行编写了,因为tree api的简单与明了的特性,也越来越成为许多开源库的选

  • Android ASM插桩探索实战详情

    目录 前言 ASM的作用是什么? 如何使用ASM? 基本使用方式 自定义ClassVisitor ASM ByteCode Viewer 如何将ASM运用都我们的实际项目中来? 引入工程 Android Gradle Plugin 创建插件项目 配置插件 实现插件 发布插件 应用插件 Android Transform 方法节流 方法耗时日志 如何调试 发布线上的额外工作 插件项目的maven仓库 编译影响评估 Tips 总结 前言 我们都知道,在Android编译过程中,Java代码会被编译成

  • SpringBoot自定义maven-plugin插件整合asm代码插桩

    目录 背景 项目配置 编译拦截 ASM插桩 项目使用 背景 公司开发框架增加了web系统license授权证书校验模块,实行一台机器一个授权证书,初步方案是增加拦截器针对全局请求进行拦截校验,评估后认为校验方式单一,应该增加重要工具类,业务service实现中每个方法的进行校验,因为涉及代码量较大硬编码工作困难,故选择通过自定义maven插件在编译期间进行动态代码插桩操作 项目配置 新建maven项目设置打包方式 <packaging>maven-plugin</packaging>

  • Android进阶从字节码插桩技术了解美团热修复实例详解

    目录 引言 1 插件发布 2 Javassist 2.1 准备工作 2.2 Transform 2.3 transform函数注入代码 2.3.1 Jar包处理 2.3.2 字节码处理 2.4 Javassist织入代码 2.4.1 ClassPool 2.4.2 CtClass 引言 热修复技术如今已经不是一个新颖的技术,很多公司都在用,而且像阿里.腾讯等互联网巨头都有自己的热修复框架,像阿里的AndFix采用的是hook native底层修改代码指令集的方式:腾讯的Tinker采用类加载的方

  • 浅谈Android ASM自动埋点方案实践

    这段时间想到一个有趣的功能,就是在Android的代码编译期间进行一些骚操作,来达到一些日常情境下难以实现的功能,比如监听应用中的所有onClick点击时间,或者监听某些方法的运行耗时,如果在代码中一个方法一个方法修改会很蛋疼,所以想通过Gradle插件来实现在应用的编译期间进行代码插入的功能. 1.AOP的概念 其实这已经涉及到AOP(Aspect Oriented Programming),即面向切面编程,在编译期间对代码进行动态管理,以达到统一维护的目的. AOP切面 举个栗子,Andro

  • Android利用Hero实现列表与详情页无缝切换动画

    目录 前言 思路 列表元素 详情页面 源码 总结 前言 介绍了几篇 Hero 动画,我们来一个 Hero 动画应用案例.在一些应用中,列表的元素和详情的内容是一致的,这个时候利用 Hero 动画切换到详情会感觉无缝过渡,用户体验会更好.例如本篇我们要实现下面的效果: Hero 应用:列表与详情切换 思路 上面的效果是列表和详情共用了头像和头像的背景色.二者的组合是一个 Stack 组件,因此可以使用 Hero 组件完成.然后是 Hero 组件的移动,我们先做了水平移动,再做垂直方向移动,这样过渡

  • Android串口通讯SerialPort的使用详情

    目录 1.什么是串口? 2.添加依赖 3.编写串口处理类 4.使用串口 5.总结 1.什么是串口? 在不会使用串口通讯之前,暂且可以把它理解为“一个可通讯的口”:使用篇不深入探讨理论及原理.能理解串口如何使用之后,可以查看浅谈Android串口通讯SerialPort原理 2.添加依赖 1.)在 module 中的 build.gradle 中的 dependencies 中添加以下依赖: dependencies { //串口 implementation 'com.github.lichee

  • Android Retrofit原理深入探索

    目录 序章 Retrofit构建过程 创建网络请求接口实例过程 执行请求过程 总结 序章 首先引入依赖 implementation 'com.squareup.retrofit2:retrofit:2.9.0' 在原理之前,我们先来回忆一下Retrofit的基本使用 1.定义接口 interface MyService { @GET("gallery/{imageType}/response") fun getImages(@Path("imageType") i

  • Android仿京东、天猫商品详情页

    前言 前面在介绍控件TabLayout控件和CoordinatorLayout使用的时候说了下实现京东.天猫详情页面的效果,今天要说的是优化版,是我们线上实现的效果,首先看一张效果: 项目结构分析 首先我们来分析一下要实现上面的效果,我们需要怎么做.顶部是一个可以滑动切换Tab,可以用ViewPager+Fragment实现,也可以使用系统的TabLayout控件实现:而下面的 View是一个可以滑动拖动效果的View,可以采用网上一个叫做DragLayout的控件,我这里是自己实现了一个,主要

  • Android通过自定义控件实现360软件详情页效果

    一.概述 最近有不少朋友私聊问应用宝.360软件助手之类的软件详情页怎么做,刚好,最近有时间就模仿360软件助手详情页给大家做个Demo,供大家参考.嗯,关于实现呢,我写了两种方式: 1.ScrollView内嵌软件介绍+ViewPager+ViewPager中是ScrollView,这种方式呢,纯原生,没有涉及到自定义控件,但是这样嵌套呢,涉及到测量以及事件的冲突处理,大家可以自己尝试去做一下,想像起来蛮容易的,做起来其实还是挺费劲的,代码我会给出,核心代码不多,大家自行参考.本文将重点分析第

  • Android模仿知乎的回答详情页的动画效果

    废话不多说,咱们第一篇文章就是模仿"知乎"的回答详情页的动画效果,先上个原版的效果图,咱们就是要做出这个效果 在实现之前,我们先根据上面的动画效果,研究下需求,因为gif帧数有限,所以不是很连贯,推荐你直接下载一个知乎,找到这个界面自己玩玩 ☞当文章往上移动到一定位置之后,最上面的标题栏Bar和问题布局Title是会隐藏的,回答者Author布局不会隐藏 ☞当文章往下移动移动到一定位置之后,原先隐藏的标题栏Bar和问题布局Title会下降显示 ☞当文章往上移动的时候,下部隐藏的Tool

随机推荐