Android 编译插桩之--ASM入门

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

1、前言

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

2、目标和提示

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

2.一、ClassVisitor

首先咱们是要处理单个.class文件,那确定须要访问到这个.class文件的内容,ClassVisitor就是处理这些的,他能够拿到class文件的类名,父类名,接口,包含的方法,等等信息。编程

2.二、MethodVisitor

由于咱们须要在方法执行前插入一些字节码,因此咱们须要MethodVisitor来帮咱们处理并插入字节码。api

2.三、Transform

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

3、开始编程

根据上文的步骤咱们顺序在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.一、ClassVisitor

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

DemoClassVisitor类源码以下:ide

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.二、MethodVisitor

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

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.三、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/这就是原来的打印: 项目中的打印信息

4、总结

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