会当凌绝顶,一览众山小。
(杜甫《望岳》)
刚开始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。
为了实现目标我们首先需要知道几个简单的类:
首先我们是要处理单个.class文件,那肯定需要访问到这个.class文件的内容,ClassVisitor就是处理这些的,他可以拿到class文件的类名,父类名,接口,包含的方法,等等信息。
因为我们需要在方法执行前插入一些字节码,所以我们需要MethodVisitor来帮我们处理并插入字节码。
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'))
}
}
}
在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
}
}
经过上一步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)
}
}
经过前两步的处理我们已经可以将字节码插入到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,祝大家学习愉快~