Android 编译插桩之--ASM入门

会当凌绝顶,一览众山小。
(杜甫《望岳》)

一、前言

刚开始ASM的学习就直接又被绊了一天,真的太难了,这道题我不会做,不会做~~
好了首先环境如下:Android Studio3.6.2,gradle3.6.2,kotlin1.3.71,androidx。如果用的不是androidx的话估计也不会出问题,但是用了androidx的话记得按照本文来编码,否则你会耽误很久的时间。本文基于上一篇文章 Android 编译插桩之–自定义Gradle插件 ,所有工程也跟上文中的一样。一切就绪我们准备开始。

二、目标和提示

这次我们的目标是在ASMDemo App启动后在MainActivity的onCreate()方法之前自动输出一段简单的日志信息。要达到这样的目的我们就需要使用ASM,ASM 是一个 Java 字节码操控的框架,也就是说我们可以直接操作.class文件。这样我们就可以在不侵入MainActivity类的情况下,直接达到目的。至于ASM的具体介绍,本文不再具体介绍,请各位移步Google。
为了实现目标我们首先需要知道几个简单的类:

2.1、ClassVisitor

首先我们是要处理单个.class文件,那肯定需要访问到这个.class文件的内容,ClassVisitor就是处理这些的,他可以拿到class文件的类名,父类名,接口,包含的方法,等等信息。

2.2、MethodVisitor

因为我们需要在方法执行前插入一些字节码,所以我们需要MethodVisitor来帮我们处理并插入字节码。

2.3、Transform

Transform是gradle构建的时候从class文件转换到dex文件期间处理class文件的一套方案,也就是说处理class的吧。上文的ClassVisitor可以是看做处理单个class文件,那这里的话Transform可以处理一系列的class文件:从查找到所有class文件,到交给ClassVisitor和MethodVisitor处理后,再到重新覆盖原来的class文件这么一个流程。

三、开始编程

根据上文的步骤我们顺序在ASMDemoPlugin工程的plugin模块中编写ClassVisitor、MethodVisitor、以及Transform。
首先这里我们没有选择groovy的编程方式,因为groovy写起来总感觉有一些不舒服,我们还是选用kotlin来编写所有脚本。
所以plugin插件的module看起来是这样的:main文件夹下分了groovy,java和kotlin来分别存储对应的代码,这里我们只需要使用kotlin的即可,下文代码都集中在下图所示的三个类中:
在这里插入图片描述
另外要想实现这样根据语言分文件夹的效果需要在插件module的build.gradle中配置一下sourceSets ,如下代码所示。除了这些,还添加了kotlin插件以及kotlin和gradle的依赖,因为开发Transform的需要。最后是插件仓库地址的配置信息:

apply plugin: 'kotlin'
apply plugin: 'groovy'
apply plugin: 'maven'

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

        java {
            srcDir "src/main/java"
        }

        kotlin {
            srcDir "src/main/kotlin"
        }

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

dependencies {
    implementation gradleApi()

    implementation 'com.android.tools.build:gradle:3.6.2'
}

uploadArchives {
    repositories {
        mavenDeployer {
            pom.groupId = 'com.cooloongwu.plugin'
            pom.artifactId = 'asm-plugin'
            pom.version = '1.1.4'
            //生成的文件地址
            repository(url: uri('F:/Repo'))
        }
    }
}

3.1、ClassVisitor

在ClassVisitor中我们拿到相应class的类名,比如这时候是MainActivity.class,那么类名就是““com/cooloongwu/asmdemo/MainActivity””,你可以自行打印尝试【注意这里的包名是ASMDemo工程的包名,而不是ASMDemoPlugin工程的包名,因为我们是要处理的是ASMDemo对吧】。匹配到类名后覆写visitMethod()方法,根据当前方法名是否匹配onCreate方法来将具体的插桩操作交给DemoMethodVisitor处理。

DemoClassVisitor类源码如下:

package com.cooloongwu.plugin1

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

class DemoClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, 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)

        if (className.equals("com/cooloongwu/asmdemo/MainActivity")) {
            if (name.equals("onCreate")) {
                return DemoMethodVisitor(methodVisitor)
            }
        }

        return methodVisitor
    }
}

3.2、MethodVisitor

经过上一步ClassVisitor的处理我们已经匹配到onCreate方法了,此时我们需要在DemoMethodVisitor类中进行插入字节码操作。如下所示,直接继承自MethodVisitor,并覆写visitCode()方法。其中的代码就是我们要插入的代码了,乍一看完全不是我们平常那种Log.e("TAG", "===== This is just a test message =====");的写法,而是复杂了很多。是的,这时候你就知道visitCode中的代码和我们上边的Log信息等价就好了,等这篇文章阅读完,咱们就可以去深入学习JVM字节码的相关信息了,现在不要想那么多,直接拿去用。

package com.cooloongwu.plugin1

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

class DemoMethodVisitor(methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM5, methodVisitor) {
    override fun visitCode() {
        super.visitCode()
        
        mv.visitLdcInsn("TAG")
        mv.visitLdcInsn("===== This is just a test message =====")
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "android/util/Log",
            "e",
            "(Ljava/lang/String;Ljava/lang/String;)I",
            false
        )
        mv.visitInsn(Opcodes.POP)
    }
}

3.3、Transform

经过前两步的处理我们已经可以将字节码插入到MainActivity.class的onCreate方法前了,但是此时我们怎么去找到想要的.class文件呢,字节码插入完后我们又要怎么写回到.class文件呢?Transform就可以登场了,如下所示,DemoTransform继承自Transform,同时实现Plugin接口,这个plugin接口还熟悉吧,应用到resources/META-INF/gradle-plugins/xxx.properties的时候需要。然后依次实现所有必须的方法,除了transform()方法其他都是一些比较固定的写法了,直接搬过去即可:

package com.cooloongwu.plugin1

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream


class DemoTransform : Transform(), Plugin<Project> {

    override fun apply(project: Project) {
        println(">>>>>> 1.1.1 this is a log just from DemoTransform")
        val appExtension = project.extensions.getByType(AppExtension::class.java)
        appExtension.registerTransform(this)
    }

    override fun getName(): String {
        return "KotlinDemoTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental(): Boolean {
        return false
    }

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
    }

}

接下来是transform()方法里的内容,大致流程就是查找到所有的.class文件【代码中还添加了一些条件,过滤掉了一些class文件】,然后通过ClassReader读取并解析class文件,然后又经由我们编写的ClassVisitor和MethodVisitor处理后交给ClassWriter,最后通过FileOutputStream将新的字节码内容写回到class文件。

		val inputs = transformInvocation?.inputs
        val outputProvider = transformInvocation?.outputProvider

        if (!isIncremental) {
            outputProvider?.deleteAll()
        }

        inputs?.forEach { it ->
            it.directoryInputs.forEach {
                if (it.file.isDirectory) {
                    FileUtils.getAllFiles(it.file).forEach {
                        val file = it
                        val name = file.name
                        if (name.endsWith(".class") && name != ("R.class")
                            && !name.startsWith("R\$") && name != ("BuildConfig.class")
                        ) {

                            val classPath = file.absolutePath
                            println(">>>>>> classPath :$classPath")

                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            val visitor = DemoClassVisitor(cw)
                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)

                            val bytes = cw.toByteArray()

                            val fos = FileOutputStream(classPath)
                            fos.write(bytes)
                            fos.close()
                        }
                    }
                }

                val dest = outputProvider?.getContentLocation(
                    it.name,
                    it.contentTypes,
                    it.scopes,
                    Format.DIRECTORY
                )
                FileUtils.copyDirectoryToDirectory(it.file, dest)
            }

			//  !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!!
			//使用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)
            }
        }

至此,所有的插件内容基本完成了,最后就是在resources/META-INF/gradle-plugins/myplugin.properties文件中写入我们新的Plugin类:

implementation-class=com.cooloongwu.plugin1.DemoTransform

然后右侧gradle任务中执行uploadArchives,发布我们的插件到本地仓库中。
发布完成后在ASMDemo的app模块中添加依赖信息如下:

...省略

apply plugin: 'myplugin'
buildscript {
    repositories {
        google()
        jcenter()
        maven{
            url 'F:/Repo'
        }
    }
    dependencies {
        classpath 'com.cooloongwu.plugin:asm-plugin:1.1.4'
    }
}

...省略

此时直接运行ASMDemo工程,app运行起来后在控制台是不是就看到了相应的信息呢:

2020-04-08 21:50:17.750 3804-3804/com.cooloongwu.asmdemo E/TAG: ===== This is just a test message =====
2020-04-08 21:50:17.975 3804-3804/com.cooloongwu.asmdemo E/这就是原来的打印: 项目中的打印信息

四、总结

这里唯一需要注意的就是androidx工程需要在transform的时候也需要处理jar包,否则会导致ClassNotFoundException崩溃。我就是在这里又浪费一天啊啊啊!!接下来就是JVM字节码的学习了。
最后提供下查看字节码的插件:ASM Bytecode Outline,祝大家学习愉快~

发布了46 篇原创文章 · 获赞 56 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/u010976213/article/details/105395590