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,祝大家學習愉快~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章