Android AOP-ASM字节码插桩+自界说gradle插件

手机软件开发 2024-9-20 05:06:04 140 0 来自 中国
简介

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态署理实现步伐功能的统一维护的一种技能。AOP是OOP的连续,是软件开辟中的一个热门,也是Spring框架中的一个紧张内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部门进行隔离,从而使得业务逻辑各部门之间的耦合度低沉,进步步伐的可重用性,同时进步了开辟的服从。
常见的AOP工具按照见效时机区分紧张分为两大类:预编译期及运行期,以下枚举出市面上常用的AOP工具及对应开源框架:
1.APT工具

代表开源框架:ButterKnife、Dagger2、DBFlow、AndroidAnnotation 注解处理处罚器 Java5 中叫APT(Annotation Processing Tool),在Java6开始,规范化为 Pluggable Annotation Processing。Apt应该是这此中我们最常见到的了,难度也最低。界说编译期的注解,再通过继承Proccesor实当代码天生逻辑,实现了编译期天生代码的逻辑。
2.AspectJ工具

AspectJ是一种严酷意义上的AOP技能,由于它提供了完备的面向切面编程的注解,如许让利用者可以在不关心字节码原理的情况下完成代码的织入,由于编写的切面代码就是要织入的实际代码。
AspectJ实当代码织入有两种方式,一是自行编写.ajc文件,二是利用AspectJ提供的@Aspect、@Pointcut等注解,二者终极都是通过ajc编译器完成代码的织入。
举个简朴的例子,假设我们想统计全部view的点击变乱,利用AspectJ只必要写一个类即可。
@Aspectpublic class MethodAspect {    private static final String TAG = "MethodAspect5";    //切面表达式,声明必要过滤的类和方法     @Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")    public void callMethod() {    }    //before表现在方法调用前织入    @before("callMethod()")    public void beforeMethodCall(ProceedingJoinPoint joinPoint) {        //编写业务代码    }}复制代码注解简明直观,上手难度近乎为0。
常用的函数耗时统计工具Hugo,就是AspectJ的一个实际应用,Android平台Hujiang开源的AspectJX插件灵感也来自于Hugo,详情见旧文Android 函数耗时统计工具之Hugo。
AspectJ固然好用,但也存在一些严肃的标题。

  • 重复织入、不织入
AspectJ切面表达式支持继承语法,固然方便了开辟,但存在致命的标题,就是在继承树上的类可能都会织入代码,这在多数业务场景下是不实用的,比如无埋点。
别的Java8语法在aspectjx 2.0.0版本开始支持。
3.ASM

ASM黑白常底层的面向字节码编程的AOP框架,理论上可以实现任何关于字节码的修改,非常硬核。很多字节码天生API底层都是用ASM实现,常见比如Groovy、cglib,因此在Android平台下利用ASM无需添加额外的依靠。完备的学习ASM必须相识字节码和JVM相干知识。
比如要织入一句简朴的日记输出
Log.d("tag", " onCreate");复制代码利用ASM编写是下面这个样子,没错由于JVM是基于栈的,函数的调用必要参数先入栈,然后实行函数入栈,末了出栈,统共四条JVM指令。
mv.visitLdcInsn("tag");mv.visitLdcInsn("onCreate");mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);mv.visitInsn(POP);复制代码可以看出ASM与AspectJ有很大的差异,AspectJ织入的代码就是实际编写的代码,但ASM必须利用其提供的API编写指令。一行java代码可能对应多行ASM API代码,由于一行java代码背后可能潜伏这多个JVM指令。
你不必担心不会编写ASM代码,官方提供了ASM Bytecode Outline插件可以直接将java代码天生ASM代码。
4.Javassist

javassit是一个开源的字节码创建、编辑类库,现属于Jboss web容器的一个子模块,特点是简朴、快速,与AspectJ一样,利用它不必要相识字节码和假造机指令,这里是官方文档。
javassit核心的类库包含ClassPool,CtClass ,CtMethod和CtField。

  • ClassPool:一个基于HashMap实现的CtClass对象容器。
  • CtClass:表现一个类,可从ClassPool中通过完备类名获取。
  • CtMethods:表现类中的方法。
  • CtFields :表现类中的字段。
javassit API轻巧直观,比如我们想动态创建一个类,并添加一个helloWorld方法。
ClassPool pool = ClassPool.getDefault();//通过makeClass创建类CtClass ct = pool.makeClass("test.helloworld.Test");//创建类//为ct添加一个方法CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);ct.addMethod(helloMethod);//写入文件ct.writeFile();//加载进内存// ct.toClass();复制代码然后,我们想在helloWorld方法前后织入代码。
ClassPool pool = ClassPool.getDefault();//获取classCtClass ct = pool.getCtClass("test.helloworld.Test");//获取helloWorld方法CtMethod m = ct.getDeclaredMethod("helloWorld");//在方法开头织入m.insertBefore("{ System.out.print(\"before insert\");");//在方法末端织入 可利用this关键字m.insertAfter("{System.out.println(this.x); }");//写入文件ct.writeFile();复制代码javassit的语法直观轻巧的特点,使得在很多开源项目中都有它的身影。
5.动态署理

动态署理是署理模式的一种实现,用于在运行时动态增强原始类的举动,实现方式是运行时直接天生class字节码并将其加载进假造机。
各类框架总结

1.png 下面我们就以ASM这个框架给各人举例解说

一、终极实现的结果

这次我们的目标是在Demo App启动后在MainActivity的onCreate()方法之前自动输出一段简朴的日记信息“Log.e("TAG", "===== This is just a test message =====");”也就是终极我们必要将这个 代码插入到MainActivity的onCreate()方法之前。**
要到达如许的目标我们就必要利用ASM,ASM 是一个 Java 字节码操控的框架,也就是说我们可以直接操纵.class文件。如许我们就可以在不侵入MainActivity类的情况下,直接到达目标。
为了实现目标我们起首必要知道几个简朴的类:

1.1、ClassVisitor

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

由于我们必要在方法实行前插入一些字节码,以是我们必要MethodVisitor来帮我们处理处罚并插入字节码。真正进行方法插桩的地方。
1.3、Transform

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

根据上文的步调我们序次在gradleAOP工程的plugin模块中编写ClassVisitor、MethodVisitor、以及Transform。
这里选用kotlin来编写全部脚本。以是plugin插件的module看起来是如许的:main文件夹下kotlin来分别存储对应的代码

2.png 别的要想实现如许根据语言分文件夹的结果必要在插件module的build.gradle中设置一下sourceSets ,如下代码所示。除了这些,还添加了kotlin插件以及kotlin和gradle的依靠,由于开辟Transform的必要。末了是插件堆栈地点的设置信息.
apply plugin: 'kotlin'apply plugin: 'maven'sourceSets {    main {        kotlin {            srcDir "src/main/kotlin"        }        resources {            srcDir 'src/main/resources'        }    }}dependencies {    implementation gradleApi()    implementation 'org.ow2.asm:asm:7.1'    implementation 'com.android.tools.build:gradle:4.0.2'}uploadArchives {    repositories {        mavenDeployer {            pom.groupId = 'com.cjh.plugin'            pom.artifactId = 'plugin'            pom.version = '1.0'            //天生的文件地点            repository(url: uri('E:/Repo'))        }    }}2.1、ClassVisitor

在ClassVisitor中我们拿到相应class的类名,比如这时间是MainActivity.class,那么类名就是““com/example/mygradleaop/MainActivity””,你可以自行打印实验【留意这里的包名是app工程的包名,而不是gradleAOP工程的包名,由于我们是要处理处罚的是app对吧】。匹配到类名后覆写visitMethod()方法,根据当火线法名是否匹配onCreate方法来将具体的插桩操纵交给DemoMethodVisitor处理处罚。
DemoClassVisitor类源码如下
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    }    //关键方法重写visitMethod方法    //匹配MainActivity的onCreate方法    //匹配到之后进去DemoMethodVisitor方法进行插桩    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)        //com.example.mygradleaop.MainActivity        if (className.equals("com/example/mygradleaop/MainActivity")) {            if (name.equals("onCreate")) {                return DemoMethodVisitor(methodVisitor)            }        }        return methodVisitor    }}2.2、MethodVisitor

经过上一步ClassVisitor的处理处罚我们已经匹配到onCreate方法了,此时我们必要在DemoMethodVisitor类中进行插入字节码操纵。如下所示,直接继承自MethodVisitor,并覆写visitCode()方法。此中的代码就是我们要插入的代码了,乍一看完全不是我们平常那种Log.e("TAG", "===== This is just a test message =====");的写法,而是复杂了很多。是的,这时间你就知道visitCode中的代码和我们上边的Log信息等价就好了,等这篇文章阅读完,咱们就可以去深入学习JVM字节码的相干信息了,现在不要想那么多,直接拿去用。
DemoMethodVisitor类源码如下:
class DemoMethodVisitor(methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM5, methodVisitor) {    //插入:Log.e("TAG", "===== This is just a test message =====");    override fun visitCode() {        super.visitCode()        mv.visitLdcInsn("TAG")        mv.visitLdcInsn("===== This is just a test message cjh=====")        mv.visitMethodInsn(            Opcodes.INVOKESTATIC,            "android/util/Log",            "e",            "(Ljava/lang/String;Ljava/lang/String;)I",            false        )        mv.visitInsn(Opcodes.POP)    }}2.3、Transform

经过前两步的处理处罚我们已经可以将字节码插入到MainActivity.class的onCreate方法前了,但是此时我们怎么去找到想要的.class文件呢,字节码插入完后我们又要怎么写回到.class文件呢?Transform就可以登场了,如下所示,DemoTransform继承自Transform,同时实现Plugin接口,这个plugin接口还认识吧,应用到resources/META-INF/gradle-plugins/xxx.properties的时间必要。然后依次实现全部必须的方法,除了transform()方法其他都是一些比力固定的写法了,直接搬已往即可:
package com.cooloongwu.plugin1import com.android.build.api.transform.Formatimport com.android.build.api.transform.QualifiedContentimport com.android.build.api.transform.Transformimport com.android.build.api.transform.TransformInvocationimport com.android.build.gradle.AppExtensionimport com.android.build.gradle.internal.pipeline.TransformManagerimport com.android.utils.FileUtilsimport org.gradle.api.Pluginimport org.gradle.api.Projectimport org.objectweb.asm.ClassReaderimport org.objectweb.asm.ClassWriterimport java.io.FileOutputStreamclass DemoTransform : Transform(), Plugin<roject> {    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文件。
/*    * 接下来是transform()方法里的内容,大抵流程就是查找到全部的.class文件    * 【代码中还添加了一些条件,过滤掉了一些class文件】,    * 然后通过ClassReader读取并分析class文件,然后又经过    * 我们编写的ClassVisitor和MethodVisitor处理处罚后交给ClassWriter,    * 末了通过FileOutputStream将新的字节码内容写回到class文件。    *    *    * */    override fun transform(transformInvocation: TransformInvocation?) {        super.transform(transformInvocation)        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                        //1.过滤其他不合符条件的class文件                        if (name.endsWith(".class") && name != ("R.class")                            && !name.startsWith("R\$") && name != ("BuildConfig.class")                        ) {                            val classPath = file.absolutePath                            println(">>>>>> classPath classPath")                            //2.ClassReader读取并分析class文件                            val cr = ClassReader(file.readBytes())                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)                            //3.经过我们编写的ClassVisitor和MethodVisitor处理处罚                            val visitor = DemoClassVisitor(cw)                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)                            //4.通过FileOutputStream将新的字节码内容写回到class文件                            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.example.gradleaop.DemoTransform然后右侧gradle使命中实行uploadArchives,发布我们的插件到本地堆栈中。
发布完成后在Demo的根build.gradle中添加依靠信息如下:
// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {    ext.kotlin_version = "1.3.72"    repositories {        google()        jcenter()        maven{            url 'E:/Repo'        }    }    dependencies {        classpath "com.android.tools.build:gradle:4.0.2"        classpath "org.jetbrains.kotlin:kotlin-gradle-pluginkotlin_version"        //implementation-class=com.example.gradleaop.DemoTransform        //这里的路径就是gradle插件内里发布本地插件时写的        //classpath 'groupId:artifactId:version'        /*           mavenDeployer {            pom.groupId = 'com.cjh.plugin'            pom.artifactId = 'plugin'            pom.version = '1.0'            //天生的文件地点            repository(url: uri('E:/Repo'))        }        */        classpath 'com.cjh.plugin:plugin:1.0'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}allprojects {    repositories {        google()        jcenter()        maven{            url 'E:/Repo'        }    }}task clean(type: Delete) {    delete rootProject.buildDir}末了在app model下面build.gradle添加插件.这里的名称就是我们gradle插件内里清单文件
resources/META-INF/gradle-plugins/com.geo.plugin.properties的名称com.geo.plugin

apply plugin: 'com.geo.plugin'此时直接运行Demo工程,app运行起来后在控制台是不是就看到了相应的信息呢:
2020-04-08 21:50:17.750 3804-3804/com.cooloongwu.asmdemo E/TAG: ===== This is just a test message =====此时我们终极在MainActivity的onCreate方法前面插入了这行日记代码
三、总结

1)先明确本身想要干什么,像这个例子我们是必要在某个类的某个方法前面插入一行代码,那我们着实就是对方法进行插桩
2)先通过DemoClassVisitor匹配到必要插桩的类,这里就是MainActivity.class.匹配到onCreate方法后,就对方法进行插桩,实现类是DemoMethodVisitor
3)DemoMethodVisitor内里重写visitCode方法,把必要插入的代码转换成字节码的情势就是插入即可。这里就是最关键的地方,我们 可以利用ASM插件把对应的java代码转换成这种字节码,然后照着写入即可。
4)末了一步也就是利用Transform进行关联。必要用Transform拿到全部的类,然后中途交给前面我们编写的DemoClassVisitor和DemoMethodVisitor处理处罚进行插桩,末了照旧通过Transform写归去,如许就实现中途插入字节码的功能了,这就是字节码插桩。
项目源码:https://gitee.com/canjunhao/MyGradleAOP
引用:https://blog.csdn.net/u010976213/article/details/105395590
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2025-3-15 10:52, Processed in 0.165301 second(s), 36 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表