基于ASM的AOP编程
AOP是什么
AOP,国内大致译作“面向切面编程”。可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
AOP技术有哪些
- AspectJ,可参考比较稳定的一个开源框架gradle_plugin_android_aspectjx
- Javassist,我前面有提到过,使用起来比较简单,但是生成代码的效率会比较低
- ASM,是大多数开源框架的首选,但是上手难度比较大,对新手不太友好
本文会围绕如何用ASM来实现butterknife的功能来介绍ASM的核心原理和使用方法,贴出的代码皆为实验demo,参数及函数体的改变可能会要做出修改,实现不是很优雅,仅供参考(butterknife核心实现是基于AbstractProcessor,我们这里只是把AbstractProcessor的功能替换成ASM)。
1.0-包插件架构
首先我们创建好annotations库,runtime运行库,及其gradle-plugin插件库。
- curdhook-annotations
- curdhook-runtime
- curdhook-gradle-plugin
之所以命名为curd,是因为我开始本来想做一个可配置化的增删改查的插件,配置想要生成的代码,或是想要删除的方法或者类,
但是后来发现工作量太大,而且对于asm来说,我们往代码中加入的代码也需要是字节码代码,会对使用者有很大的学习成本,
还不如直接编写插件对代码进行变更要来得快,所以后来我就放弃了,不过我已经在demo中实现了增删改查,如果有业务需求的,可以按我写的例子去实现即可
1.1-curdhook-annotations
创建BindView注解和Unbinder接口(完全仿造butterknife)
|
|
1.2-curdhook-runtime
因为我们使用asm生成的是类文件xxx_ViewBinding.class,此类中会对findViewById做出处理,我们在每个需要注入的地方需要new出生成类的实例,所以我们这里单独抽出一个库来包装这一功能,代码完全参考butterknife
|
|
1.3-curdhook-gradle-plugin
如何编写一个Android插件,我前面已经介绍过,这里不再重复,这里我们主要利用Transform对class->dex这个过程进行hook,
plugin入口
123456789101112class CurdPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {println 'project apply crud-register plugin'def isApp = project.plugins.hasPlugin(AppPlugin)if(isApp){def android = project.extensions.getByType(AppExtension)def transform = new CurdTransform()android.registerTransform(transform)}}}在我们自己实现的CurdTransform中实现transform方法
123456789101112131415161718192021222324@Overridevoid transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {println "transform start ....."if (!isIncremental) {outputProvider.deleteAll()}inputs.each { TransformInput transformInput ->transformInput.jarInputs.each { JarInput jarInput ->println("jarPath:" + jarInput.file.absolutePath)File src = jarInput.fileFile dest = getDestFile(jarInput, outputProvider)FileUtils.copyFile(src, dest)}transformInput.directoryInputs.each { DirectoryInput directoryInput ->println("Path:" + directoryInput.file.absolutePath)directoryInput.file.eachFileRecurse { File file ->//将目录下面所有的文件和目录输出injectFile(file)}}File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)FileUtils.copyDirectory(directoryInput.file, dest)}}}然后将功能委托出去,我们这里只处理.class文件,对jar文件过滤
123456789private void injectFile(File file) {def name = file.nameif (name.endsWith(".class")&& !name.startsWith("R\$")&& "R.class" != name&& "BuildConfig.class" != name) {new ScanTask(file).scan().write2File()}}
2.0-扫描
在注入代码之前,我们需要对类代码扫描。得到我们需要注入的信息,包括注入的字段,值,类信息等
|
|
这里先将文件转为字节流,然后使用asm api去读取,然后将我们自己的实现类传入,在asm字节码代码中会有一些debug信息,我们可以直接传入flag忽略这些信息
2.1-ClassVisitor
顾名思义,这是我们对class输入流的一个抽象类,我们需要继承并实现对应的方法,class中类信息,字段,方法,匿名内部类,类注解都有相应的抽象方法,这样在扫描过程中就会被逐个调用
|
|
2.2-FieldVisitor
字段扫描代理,例如我们如果需要知道类中字段上的注解信息,我们就继承FieldVisitor,然后实现visitAnnotation,我们就能得到字段上注解信息处理的入口,需要注意的是,我们这里自己实现的FieldVisitor需要在ClassVisitor中实现visitField方法然后传入,下面的method,注解也是一个道理,有点像是事件的分发代理
|
|
2.3-MethodVisitor
方法扫描代理,类似FieldVisitor,如果我们也需要知道方法上的注解信息,我们同样也可以实现然后实现visitAnnotation,得到处理注解的入口
2.4-AnnotationVisitor
上面字段和方法实现visitAnnotation得到的注解入口,如果需要知道详细的注解信息,我们还需要自己实现AnnotationVisitor,然后实现下面几个方法才能拿到更为详细的信息,同样的,我们自己实现的实例需要再对应方法或是字段实现类中的visitAnnotation传入
|
|
3.0-注入
在经过扫描之后,我们已经获取了扫描类的信息,接下来我们就需要将代码注入,
|
|
这里实现类InjectClassVisitor并没有对ClassVisitor做其它处理,因为我们只是根据被扫描类,来决定是否创建xx_ViewBing.class,所以只是实现了visitEnd方法,在末尾执行我们创建类的操作,如果我们需要在被扫描类中注入字段,或者是方法,或者是修改方法,可以在相应的visitField,visitMethod中方法实现,
|
|
3.1-检查
我们需要对扫描类进行检查是否应该创建新的xx_ViewBind.class
|
|
3.2-创建输出流
|
|
3.3-根据字节码生成代码
此处我们可以先编写好java文件,然后利用ASM Bytecode Viewer生成字节码代码,然后剔除掉无用的代码
- 创建一个类
|
|
- 生成成员变量
|
|
- 构造方法
|
|
调用方法
123456789101112Integer id = (Integer) annotationEntity.getValue();if (id == null || id.intValue() < 0)continue;mw.visitVarInsn(Opcodes.ALOAD, 0);mw.visitFieldInsn(Opcodes.GETFIELD, getCreateJavaPath(), "target", getCreateVarPath());mw.visitVarInsn(Opcodes.ALOAD, 2);mw.visitLdcInsn(id);mw.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/view/View", "findViewById", "(I)Landroid/view/View;", false);mw.visitTypeInsn(Opcodes.CHECKCAST, Type.getType(entity.desc).getInternalName());mw.visitFieldInsn(Opcodes.PUTFIELD, getClassPath(), entity.name, entity.desc);mw.visitInsn(Opcodes.RETURN);mw.visitMaxs(3 * i, 3 * i);创建方法
1234567891011MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "unbind", "()V", null, null);mv.visitCode();mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitInsn(Opcodes.ACONST_NULL);mv.visitFieldInsn(Opcodes.PUTFIELD, getCreateJavaPath(), "target", getCreateVarPath());mv.visitVarInsn(Opcodes.ALOAD, 0);mv.visitFieldInsn(Opcodes.GETFIELD, getCreateJavaPath(), "target", getCreateVarPath());mv.visitInsn(Opcodes.ACONST_NULL);mv.visitFieldInsn(Opcodes.PUTFIELD, getClassPath(), "content", "Landroid/widget/TextView;");mv.visitInsn(Opcodes.RETURN);mv.visitMaxs(2, 1);
上面贴出的代码为demo的代码,详细可以参考我写的demo
ASM注意的地方
插件库需要统一
123451.com.android.tools.build:gradle:2.2.02.classpath 'com.android.tools.build:gradle:3.0.1'3.distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip上面几个库有版本依赖关系,不然会出现插件库里没法识别找到 com.android.tools里的代码编写字节码
1可借助ASM Bytecode Viewer插件生成,然后剔除无用代码