Android 字节码插桩全流程解析

Android进阶宝典 – Handler应用于线上卡顿监控中,我简单介绍了一下关于ASM实现字节码插桩来实现方法耗时的监控,但是当时只是找了一个特定的class文件,针对某个特定的方法进行插桩,但是真正的开发中不可能这么做的,因为整个工程中会有成百上千的方法,而且存储的位置也各有不同,这个时候,我们就需要借助gradle插件来实现ASM字节码插桩。

1 准备工作

但凡涉及到gradle开发,我一般都是会在buildSrc文件夹下进行,还有没有伙伴不太了解buildSrc的,其实buildSrc是Android中默认的插件工程,在gradle编译的时候,会编译这个项目并配置到classpath下。这样的话在buildSrc中创建的插件,每个项目都可以引入。

在buildSrc中可以创建groovy目录(如果对groovy或者kotlin了解),也可以创建java目录,对于插件开发个人更便向使用groovy,因为更贴近gradle。

1.1 创建插件

创建插件,需要实现Plugin接口,在引入这个插件后,项目编译的时候,就会执行apply方法。

class ASMPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        def ext = project.extensions.getByType(AppExtension)
        if (ext != null){
            ext.registerTransform(new ASMTransform())
        }
    }
}

在apply方法中,可以执行自定义的Task,也可以执行自定义的Transform(其实也可以看做是一种特殊的Task),这里我们自定义了插桩相关的Transform。

1.2 创建Transform

什么是Transform呢?就是在class文件打包生成dex文件的过程中,对class字节码做处理,最终生成新的dex文件,那么有什么方式能够对字节码操作呢?ASM是一种方式,使用Javassist也可以织入字节码。

class ASMTransform extends Transform {

    @Override
    String getName() {
        return "ASMTransform"
    }

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

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

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        inputs.each { input ->
            input.directoryInputs.each { dic ->
                /**这里会拿到两个路径,分别是java代码编译后的javac/debug/classes,以及kotlin代码编译后的 tmp/kotlin-classes/debug */
                println("dic path == >${dic.file.path}")
                /**所有的class文件的根路径,我们已经拿到了,接下来就是分析这些文件夹下的class文件*/
                findAllClass(dic.file)
                /**这里一定不能忘记写*/
                def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(dic.file, dest)
            }
            input.jarInputs.each { jar ->
                /**这里也一定不能忘记写*/
                def dest = outputProvider.getContentLocation(jar.name,jar.contentTypes,jar.scopes,Format.JAR)
                FileUtils.copyFile(jar.file,dest)
            }
        }
    }

    /**
     * 查找class文件
     * @param file 可能是文件也可能是文件夹
     */
    private void findAllClass(File file) {
        if (file.isDirectory()) {
            file.listFiles().each {
                findAllClass(it)
            }
        } else {
            modifyClass(file)
        }
    }

    /**
     * 进行字节码插桩
     * @param file 需要插桩的字节码文件
     */
    private void modifyClass(File file) {
        println("最终的class文件 ==> ${file.absolutePath}")
        /**如果不是.class文件,抛弃*/
        if (!file.absolutePath.endsWith(".class")) {
            return
        }

        /**BuildConfig.class文件以及R文件都抛弃*/
        if (file.absolutePath.contains("BuildConfig.class") || file.absolutePath.contains("R")) {
            return
        }

        doASM(file)
    }

    /**
     * 进行ASM字节码插桩
     * @param file 需要插桩的class文件
     */
    private void doASM(File file) {
        def fis = new FileInputStream(file)
        def cr = new ClassReader(fis)
        def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
        cr.accept(new ASMClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG)
        /**重新覆盖*/
        def bytes = cw.toByteArray()
        def fos = new java.io.FileOutputStream(file.absolutePath)
        fos.write(bytes)
        fos.flush()
        fos.close()
    }
}

如果想要使用Transform,那么需要引入transform-api,其实在transform 1.5之后gradle就支持Transform了。

implementation 'com.android.tools.build:transform-api:1.5.0'

当执行Transform任务的时候,最终会执行到transform方法,在这个方法中可以获取TransformInput的输入,主要包括两种:文件夹和Jar包;对于Jar包,我们不需要处理,只需要拷贝到目标文件夹下即可。

对于文件夹我们是需要处理的,因为这里包含了我们要处理的.class文件,对于Java编译后的class文件是存在javac/debug/classes根文件夹下,对于kotlin编译后的class文件是存在temp/classes根文件下。

所以在整个编译的过程中,只要是.class文件都会执行doASM这个方法,在这个方法中就是我们在上节提到的对于字节码的插桩。

1.3 ASM字节码插桩

class ASMClassVisitor extends ClassVisitor {

    ASMClassVisitor(int api) {
        super(api)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        println("visitMethod==>$name")
        /**所有的方法都会在ASMMethodVisitor中插入字节码*/
        def method = super.visitMethod(access, name, descriptor, signature, exceptions)
        return new ASMMethodVisitor(api, method, access, name, descriptor)
    }

    ASMClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor)
    }

    @Override
    FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        return super.visitField(access, name, descriptor, signature, value)
    }

    @Override
    AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        return super.visitAnnotation(descriptor, visible)
    }
}
class ASMMethodVisitor extends AdviceAdapter {

    private def methodName
    /**
     * Constructs a new {@link AdviceAdapter}.
     *
     * @param api the ASM API version implemented by this visitor. Must be one of {@link
     *     Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
     * @param methodVisitor the method visitor to which this adapter delegates calls.
     * @param access the method's access flags (see {@link Opcodes}).
     * @param name the method's name.
     * @param descriptor the method's descriptor (see {@link Type Type}).
     */
    protected ASMMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor)
        this.methodName = name
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter()
        visitFieldInsn(GETSTATIC,
                "com/lay/learn/base_net/LoggUtils",
                "INSTANCE",
                "Lcom/lay/learn/base_net/LoggUtils;")
        visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "start", "()V", false)
    }

    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode)
        visitFieldInsn(GETSTATIC,
                "com/lay/learn/base_net/LoggUtils",
                "INSTANCE",
                "Lcom/lay/learn/base_net/LoggUtils;")
        visitLdcInsn(methodName)
        visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "end", "(Ljava/lang/String;)V",false)
    }
}

这里就不再细说了,贴上源码大家可以借鉴一下哈。

最终在编译的过程中,对所有的方法插入了我们自己的耗时计算逻辑,当运行之后

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

虽然我们没有显示地在MainActivity的onCreate中插入耗时检测代码,但是在控制台中我们可以看到,onCreate方法耗时180ms

2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗时==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗时==>180

1.4 插件配置

当我们完成一个插件之后,需要在META-INF文件夹下创建一个gradle-plugins文件夹,并在properties文件中声明插件全类名。

implementation-class=com.lay.asm.ASMPlugin

要注意插件id就是properties文件的名字。

这样只要某个工程中需要字节码插桩,只需要引入asm_plugin这个插件即可在编译的时候扫描整个工程。

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'asm_plugin'
}

附上buildSrc中的gradle配置文件

plugins{
    id 'groovy'
}

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation  'org.apache.commons:commons-io:1.3.2'
    implementation "com.android.tools.build:gradle:7.0.3"
    implementation 'com.android.tools.build:transform-api:1.5.0'
    implementation 'org.ow2.asm:asm:9.1'
    implementation 'org.ow2.asm:asm-util:9.1'
    implementation 'org.ow2.asm:asm-commons:9.1'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

最后需要说一点就是,在Transform任务执行时,一定要将文件夹或者jar包传递到下一级的Transform中,否则会导致apk打包时缺少文件导致apk无法运行

作者:Vector7
链接:https://juejin.cn/post/7182178552207376421

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了满足广大Android开发爱好者与从业者的学习需求,我们精心整理并上传了一份面而实用的Android项目资源包。这份资源包内容丰富,涵盖了从基础知识到实战应用的方位内容,旨在为开发者们提供一个便捷、高效的学习平台。 一、文件手册 资源包中的文件手册部分,详细记录了Android开发的核心知识点和常用技术。无论是初学者还是有一定经验的开发者,都能从中找到所需的学习资料。手册采用了简洁明了的排版方式,使得查阅更加方便快捷。同时,手册内容深入浅出,既适合新手入门,也能为老手提供有价值的参考。 二、项目实战与练习 为了让学习者能够将理论知识与实践相结合,我们特别准备了项目实战与练习部分。这部分内容包含了多个精心设计的Android项目案例,从需求分析、设计思路到实现过程,都有详细的讲解和代码示例。学习者可以通过实际操作,深入了解Android开发的整个流程,提升自己的实战能力。 此外,我们还提供了一系列练习题,旨在巩固所学知识,检验学习成果。这些练习题既有基础题,也有难度较高的挑战题,适合不同层次的学习者进行练习。 三、Android开发工具集 在Android开发过程中,选择合适的工具能够大大提高开发效率。因此,我们整理了常用的Android开发工具集,包括开发工具、测试工具、性能优化工具等。这些工具都是经过我们精心筛选和测试的,能够帮助开发者们更加高效地进行Android开发工作。 总的来说,这份Android项目资源包是一份不可多得的学习资料,无论你是初学者还是有一定经验的开发者,都能从中受益匪浅。我们希望通过这份资源包,为广大Android开发爱好者与从业者提供一个更加便捷、高效的学习平台,共同推动Android开发领域的发展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值