Android ASM插桩技术

简介

插桩,简单来说就是在编译期间插入代码。但是编译的时候不管是kt还是java都会被编译成字节码文件.class,那么插入的代码肯定不能是原版的java或者kt代码,而应该是字节码,这篇文章就简要介绍一下插桩技术。(建议顺着APT注解开发那篇文章继续看)

插桩技术

大体流程

插桩是在编译期执行的一个流程,那这个流程大概是怎样的呢?可以看下这张图
在这里插入图片描述
简单来说就是在编译期不断遍历当前编译模块的编译产物,找到满足条件的类和方法,在对应的方法里插入字节码

构建方式

使用asm是以插件的方式集成,这边就从头介绍一下插件的构建与ASM的使用
先搂一眼代码结构:

插件jar包

注解开发那篇文章有提到jar和aar 的创建区别,这里不在赘述,注意创建的是jar包。

gradle

gradle的必要配置包括

  1. 防止插件重复
tasks.withType<Copy>() {  
    filesMatching("**/*.properties"){  
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE  
    }  
}
  1. dependencies
dependencies{  
    implementation(gradleApi())  
    implementation("com.android.tools.build:gradle:7.2.2")  
    implementation("org.ow2.asm:asm:9.3")  
    implementation("org.ow2.asm:asm-commons:9.3")  
    implementation("org.ow2.asm:asm-analysis:9.3")  
    implementation("org.ow2.asm:asm-util:9.3")  
    implementation("org.ow2.asm:asm-tree:9.3")  
    implementation("commons-io:commons-io:2.8.0")  
    implementation("org.javassist:javassist:3.29.0-GA")  
}

其他的就是些上传maven的脚本

Transform

插件的必要类之一,决定了编译期有哪些类需要过滤

class BridgeTransform: Transform(), Plugin<Project> {  
  
    private var properties = mutableMapOf<String, Any?>()  
  
    override fun getName(): String {  
        return "KotlinBridgeTransform"  
    }  
  
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {  
        return TransformManager.CONTENT_CLASS  
    }  
  
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {  
        return mutableSetOf(QualifiedContent.Scope.PROJECT)  
    }  
  
    override fun isIncremental(): Boolean {  
        return false  
    }  
  
    override fun apply(target: Project) {  
        println(">>>>>> 1.1.1 this is a log just from DemoTransform")  
        properties.putAll(target.properties)  
        val appExtension = target.extensions.getByType(BaseExtension::class.java)  
        appExtension.registerTransform(this)  
    }  
  
    override fun transform(transformInvocation: TransformInvocation?) {  
        super.transform(transformInvocation)  
        val inputs = transformInvocation?.inputs  
        val outputProvider = transformInvocation?.outputProvider  
  
        if (!isIncremental) {  
            outputProvider?.deleteAll()  
        }  
  
        inputs?.forEach { it ->  
            it.directoryInputs.forEach {  
                if (it.file.isDirectory) {  
                    getAllFiles(it.file).forEach {  
                        println(">>>>>> file  check :${it.absolutePath}")  
                        val file = it  
                        val name = file.name  
                        if (name.endsWith(".class") && name != ("R.class")  
                            && !name.startsWith("R\$") && name != ("BuildConfig.class")  
                        ) {  
                            try {  
                                val classPath = file.absolutePath  
                                println(">>>>>> classPath :$classPath")  
  
                                val cr = ClassReader(file.readBytes())  
                                val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)  
                                val visitor = BridgeClassVisitor(cw)  
                                cr.accept(visitor, ClassReader.EXPAND_FRAMES)  
  
                                val bytes = cw.toByteArray()  
  
                                val fos = FileOutputStream(classPath)  
                                fos.write(bytes)  
                                fos.close()  
                            }catch (e: Throwable) {  
                                e.printStackTrace()  
                            }  
  
                        }  
                    }  
                }  
  
                try {  
                    val dest = outputProvider?.getContentLocation(  
                        it.name,  
                        it.contentTypes,  
                        it.scopes,  
                        Format.DIRECTORY  
                    )  
                    println("========== file ::: ${it.file.name}, dest::: $dest ==========, isDir ::: ${it.file.isDirectory}")  
                    FileUtils.copyDirectory(it.file, dest)  
                }catch (e: Throwable){  
                    e.printStackTrace()  
                }  
            }  
  
            //  !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!!  
            //使用androidx的项目一定也注意jar也需要处理,否则所有的jar都不会最终编译到apk中,千万注意  
            //导致出现ClassNotFoundException的崩溃信息,当然主要是因为找不到父类,因为父类AppCompatActivity在jar中  
            it.jarInputs.forEach {  
                val dest = outputProvider?.getContentLocation(  
                    it.name,  
                    it.contentTypes,  
                    it.scopes,  
                    Format.JAR  
                )  
                FileUtils.copyFile(it.file, dest)  
            }  
        }  
    }  
  
    private fun getAllFiles(folder: File): List<File> {  
        val fileList: MutableList<File> = ArrayList<File>()  
        val files: Array<out File>? = folder.listFiles()  
  
        if (files != null) {  
            println(">>>>>> files : files != null,  files.size = ${files.size}")  
            for (file in files) {  
                if (file.isDirectory()) {  
                    fileList.addAll(getAllFiles(file))  
                } else {  
                    fileList.add(file)  
                }  
            }  
        }else {  
            println(">>>>>> files : files = null")  
        }  
  
        return fileList  
    }  
}

大致模版就是这样,关注几个核心点点:

  1. name.endsWith(“.class”) && name != (“R.class”) && !name.startsWith(“R$”) && name != (“BuildConfig.class”)
    需要遍历插桩的文件,以.class结尾,不是R文件,不以R$打头,不是BuildConfig
    保证遍历的文件是正常要遍历的类文件
  2. BridgeClassVisitor这个是你的类访问器,访问方法时会用

ClassVisitor

类访问器,插件的必要类之一,决定了命中类时的操作和命中类后要过滤的方法

class MainClassVisitor (classVisitor: ClassVisitor, private val properties: Map<String, Any?>): ClassVisitor(Opcodes.ASM9, classVisitor) {  
  
    private var className: String? = null  
  
    override fun visit(  
        version: Int,  
        access: Int,  
        name: String?,  
        signature: String?,  
        superName: String?,  
        interfaces: Array<out String>?  
    ) {  
        super.visit(version, access, name, signature, superName, interfaces)  
        className = name  
    }  
  
    override fun visitMethod(  
        access: Int,  
        name: String?,  
        descriptor: String?,  
        signature: String?,  
        exceptions: Array<out String>?  
    ): MethodVisitor {  
  
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)  
        println("==== className ::: $className, name ::: $name")  
        if (className == properties["ENTER_CLASS_NAME"]){   // 全类名  
            if (name == properties["ENTER_METHOD_NAME"]){    // 方法名  
                println("====方法执行,${properties["ENTER_CLASS_NAME"]}.${properties["ENTER_METHOD_NAME"]}")  
                return MainMethodVisitor(methodVisitor)  
            }  
        }  
  
        return methodVisitor  
    }  
}

其中,visit方法表明命中类时的动作,visitMethod里的条件语句,表明了满足类名和方法名时需要走的方法访问器。

MethodVisitor

方法访问器,决定了命中方法后,插桩的具体逻辑

class MainMethodVisitor(methodVisitor: MethodVisitor): MethodVisitor(Opcodes.ASM9, methodVisitor) {  
  
    override fun visitCode() {  
        super.visitCode()  

    }  
}

这里有个关键的点,插桩的代码是字节码,常规的kt代码和java代码写这指定白扯,可是字节码要如何写呢?

AS安装ASM插件工具

到插件市场找ASM Bytecode Outline Rebooted插件,安装

java转字节码

首先,能够直接转字节码的代码只能是java,因此把你要插桩的代码写到一个java类中,然后右击这个文件,选择这个:
在这里插入图片描述
AS就会给出你这个java文件转成字节码后的东西。

注意:有些类由于引用关系可能会生成失败,保证要插桩的代码本身不复杂,找一些简单的java文件写一下代码进行转换。
然后把字节码粘贴到visitCode方法中,可能需要简单调整。

插件使用

这个jar包是要供外部以插件的形式使用,所以需要配置META-INF,在resources目录下创建META-INF.gradle-plugins目录,创建一个properties文件(例如hahaha.properties)
文件里写:implementation-class=transform的全类名
在打成二进制包后,使用方式如下:

  1. 根目录build.gradle下配置classpath指向这个jar包
  2. 需要插桩的module的build.gradle中引用插件,比如apply plugin:‘hahaha’
    这里的hahaha就对应了properties文件

编译结果检查

上述流程可以完成插桩,那怎么检查插桩是否成功呢?这就需要看一下.class文件了。
针对java,产物位于:
在这里插入图片描述
针对kt,产物位于:
在这里插入图片描述
如果class被反编译了,就可以直接看,如果没有被反编译,那就下一个GUI反编译工具,看一下结果,检查代码是否插桩成功。

总结

ok,到这里插桩技术基本就到这里了,如果是研究过arouter或者想研究Arouter的小伙伴,看完估计也能够对Arouter的底层实现原理会有更深的了解

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值