会当凌绝顶,一览众山小。
(杜甫《望岳》)java
刚开始ASM的学习就直接又被绊了一天,真的太难了,这道题我不会作,不会作~~
好了首先环境以下:Android Studio3.6.2,gradle3.6.2,kotlin1.3.71,androidx。若是用的不是androidx的话估计也不会出问题,可是用了androidx的话记得按照本文来编码,不然你会耽误好久的时间。本文基于上一篇文章 Android 编译插桩之–自定义Gradle插件 ,全部工程也跟上文中的同样。一切就绪咱们准备开始。android
此次咱们的目标是在ASMDemo App启动后在MainActivity的onCreate()方法以前自动输出一段简单的日志信息。要达到这样的目的咱们就须要使用ASM,ASM 是一个 Java 字节码操控的框架,也就是说咱们能够直接操做.class文件。这样咱们就能够在不侵入MainActivity类的状况下,直接达到目的。至于ASM的具体介绍,本文再也不具体介绍,请各位移步Google。
为了实现目标咱们首先须要知道几个简单的类:web
首先咱们是要处理单个.class文件,那确定须要访问到这个.class文件的内容,ClassVisitor就是处理这些的,他能够拿到class文件的类名,父类名,接口,包含的方法,等等信息。编程
由于咱们须要在方法执行前插入一些字节码,因此咱们须要MethodVisitor来帮咱们处理并插入字节码。api
Transform是gradle构建的时候从class文件转换到dex文件期间处理class文件的一套方案,也就是说处理class的吧。上文的ClassVisitor能够是看作处理单个class文件,那这里的话Transform能够处理一系列的class文件:从查找到全部class文件,到交给ClassVisitor和MethodVisitor处理后,再到从新覆盖原来的class文件这么一个流程。app
根据上文的步骤咱们顺序在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处理。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 } }
通过上一步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) } }
通过前两步的处理咱们已经能够将字节码插入到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,祝你们学习愉快~