Android中的AOP编程

一、什么是AOP

AOP是Aspect Oriented Programming的缩写,即面向切面编程。平时我们接触比较多的是OOP,即面向对象编程。

OOP

提倡的是将功能模块化,对象化,每个模块专心于自己的事情。但是有些功能是每个模块都需要的,比如日志模块,性能监控模块,按照我们平常的做法就是每个模块中再各自加上这些功能代码,这样做一方面显得代码很冗余,另一方面也不利于后期的拓展和维护。

AOP

提倡的是针对同一类问题的统一处理,我们不需要关注是哪个类哪个对象在使用,我们关注的是具体的方法和功能点。

二、AOP的实现

我认为不管是何种方法实现AOP,最主要的部分就是代码注入。

常见的有三种方式实现AOP,分别是APT、AspectJ、Javassist ,区别在于它们作用的时间不一样,即上面所说的代码注入时机不一样。如下图:

APT

实现方法类似butterknife,具体可以参考我的另一篇文章,Android编译时注解项目学习,用到了编译时注解的技术。

AspectJ

  1. AspectJ是一个代码生成工具(Code Generator)。

  2. AspectJ语法就是用来定义代码生成规则的语法。

使用AspectJ有两种方法:

  • 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
  • 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ,一般都用这种方法进行开发。

语法

深入理解Android之AOP这边文章中介绍的比较好。这里我就简单说下几个比较重要的点,

PointCut切入点,告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。

JPoints连接点,程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。

Advice通知,注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。

下面这张图简要总结了一下上述这些概念。

简单概括:PointCut声明执行范围,JPoints声明执行位置,Advice声明执行时机

具体实现

我们可以直接引入AspectJ但是Android上使用的话需要另外配置build文件,可以参考【翻译】Android中的AOP编程这篇文章

如果不想自己配置也可以引入现成的SDK,github地址

上述两种方法只是引入的方法不一样具体语法是一致的

这里我们写个简单的例子,实现对Activity的onCreate方法的监听

  • 引入AspectJ

具体方法参考上方

  • 声明AspectJ类
/**
 * 声明一个AspectJ
 */
@Aspect
public class LifeAspect {
    private static final String TAG = "LifeAspect";
    private long onCreateTime;

    /**
     * Pointcut,告诉代码在每个onCreate方法中注入
     * execution,JPoints类型,函数内部执行,还有call等
     */
    @Pointcut("execution(* android.app.Activity.onCreate(..))" )
    public void lifeOnCreate(){
    }

    /**
     * Before,Advice的一种类型,切入点之前执行
     * @param joinPoint
     */
    @Before("lifeOnCreate()")
    public void lifeOnCreateHandle(JoinPoint joinPoint){
        Log.v(TAG, "lifeOnCreate -->");
        onCreateTime = System.currentTimeMillis();
        Log.v(TAG, "onCreateTime -->"+onCreateTime);
    }
}
  • 执行结果如下:

执行原理

AspectJ是通过对目标工程的.class文件进行代码注入的方式将通知(Advise)插入到目标代码中。 
第一步:根据pointCut切点规则匹配的joinPoint; 
第二步:将Advise插入到目标JoinPoint中。 
这样在程序运行时被重构的连接点将会回调Advise方法,就实现了AspectJ代码与目标代码之间的连接。


Javassist

Javassist作用是在编译器间修改class文件,需要解决下面的问题:

1.首先要知道什么时候编译完成,

2.并且要赶在class文件被转化为dex文件之前去修改

在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的task中可以使用javassist或者asm对class进行操作。

Transform更为方便,Transform会有他自己的执行时机,不需要我们插入到某个Task前面。Transform一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。

我们想要使用Transform就必须自定义Plugin,这里我们还要初步了解一下Gradle

Gradle

Android Studio项目是使用Gradle构建的,构建工具Gradle可以看做是一个脚本,包含一系列的Task,依次执行这些Task后,项目就打包成功了。

Task有个很重要的概念,就是inputs和outputs

Task通过inputs拿到一些东西,处理完毕之后就输出outputs,而下一个Task的inputs则是上一个Task的outputs。

例如:一个Task的作用是将java编译成class,这个Task的inputs就是java文件的保存目录,outputs这是编译后的class的输出目录,它的下一个Task的inputs就会是编译后的class的保存目录了。

Gradle由一个个Task组成,而这些Task都是由Plugin来定义的。

比如:

apply plugin : 'com.android.application' 这个 插件定义了将 Module 编译成 application 的一系列 Task。

apply plugin : 'com.android.library' 这个 插件定义了将 Module 编译成 library 的一系列 Task。

不同的 Plugin 提供了不同的 Task 来实际不同的功能。

可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task。我们需要在整个 Gradle 工作的过程中,找到合适的时机来插入自定义的 Plugin,然后在 Plugin 中使用 Javassist 对字节进行操作 ,所以使用 Javassit 的前提是掌握自定义 Gradle 插件。具体可以参考这篇文章

具体实现

这里通过一个demo来讲解具体的步骤,功能是实现在每个onClick方法中弹出一个Toast。

  • 自定义Gradle插件,具体方法参考上方
  • 导入Javassit

  • 自定义Transform,遍历class,再通过Javassit的API对代码进行修改
package javassist.huangm2.wangsu.com

import com.android.SdkConstants
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.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.CtField
import javassist.CtMethod
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project


class ModifyTransform extends Transform {

    private static final def CLICK_LISTENER = "android.view.View\$OnClickListener"

    def pool = ClassPool.default
    def project

    ModifyTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "ModifyTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)

        project.android.bootClasspath.each {
            pool.appendClassPath(it.absolutePath)
        }

        transformInvocation.inputs.each {

            it.jarInputs.each {
                pool.insertClassPath(it.file.absolutePath)

                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = it.name
                def md5Name = DigestUtils.md5Hex(it.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def dest = transformInvocation.outputProvider.getContentLocation(
                        jarName + md5Name, it.contentTypes, it.scopes, Format.JAR)
                FileUtils.copyFile(it.file, dest)
            }


            it.directoryInputs.each {
                def preFileName = it.file.absolutePath
                pool.insertClassPath(preFileName)

                findTarget(it.file, preFileName)

                // 获取output目录
                def dest = transformInvocation.outputProvider.getContentLocation(
                        it.name,
                        it.contentTypes,
                        it.scopes,
                        Format.DIRECTORY)

                println "copy directory: " + it.file.absolutePath
                println "dest directory: " + dest.absolutePath
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(it.file, dest)
            }
        }
    }

    private void findTarget(File dir, String fileName) {
        if (dir.isDirectory()) {
            dir.listFiles().each {
                findTarget(it, fileName)
            }
        } else {
            modify(dir, fileName)
        }
    }

    private void modify(File dir, String fileName) {
        def filePath = dir.absolutePath

        if (!filePath.endsWith(SdkConstants.DOT_CLASS)) {
            return
        }
        if (filePath.contains('R$') || filePath.contains('R.class')
                || filePath.contains("BuildConfig.class")) {
            return
        }

        def className = filePath.replace(fileName, "")
                .replace("\\", ".")
                .replace("/", ".")
        def name = className.replace(SdkConstants.DOT_CLASS, "")
                .substring(1)

        CtClass ctClass = pool.get(name)
        CtClass[] interfaces = ctClass.getInterfaces()
        if (interfaces.contains(pool.get(CLICK_LISTENER))) {
            if (name.contains("\$")) {
                println "class is inner class:" + ctClass.name
                println "CtClass: " + ctClass
                CtClass outer = pool.get(name.substring(0, name.indexOf("\$")))

                CtField field = ctClass.getFields().find {
                    return it.type == outer
                }
                if (field != null) {
                    println "fieldStr: " + field.name
                    def body = "android.widget.Toast.makeText(" + field.name + "," +
                            "\"javassist\", android.widget.Toast.LENGTH_SHORT).show();"
                    addCode(ctClass, body, fileName)
                }
            } else {
                println "class is outer class: " + ctClass.name
                //更改onClick函数
                def body = "android.widget.Toast.makeText(\$1.getContext(), \"javassist\", android.widget.Toast.LENGTH_SHORT).show();"
                addCode(ctClass, body, fileName)
            }
        }
    }

    private void addCode(CtClass ctClass, String body, String fileName) {

        ctClass.defrost()
        CtMethod method = ctClass.getDeclaredMethod("onClick", pool.get("android.view.View"))
        method.insertAfter(body)

        ctClass.writeFile(fileName)
        ctClass.detach()
        println "write file: " + fileName + "\\" + ctClass.name
        println "modify method: " + method.name + " succeed"
    }

}
  • 运行结果

javassist的API介绍

具体可以参考这篇文章

常用类

ClassPool:javassist的类池,使用ClassPool类可以跟踪和控制所操作的类,它的工作方式与 JVM类装载器非常相似,

CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。

CtField:用来访问域

CtMethod :用来访问方法 

CtConstructor:用来访问构造器

 基本用法

1、添加类搜索路径

ClassPool pool =ClassPool.getDefault();

           pool.insertClassPath("/usr/local/javalib");

2、添加方法

CtClass point =ClassPool.getDefault().get("Point");

CtMethod m =CtNewMethod.make( "public int xmove(int dx) { x += dx; }", point);point.addMethod(m);

3、修改方法

         CtClass point =ClassPool.getDefault().get("Point"); 

         CtMethod m= point.getDeclaredMethod(“show", null)

         m.insertAfter(“System.out.prinln(“x:” + x + “,y:) + y”))

4、添加字段

         CtClass point =ClassPool.getDefault().get("Point");

          CtField f = newCtField(CtClass.intType, "z", point);

          point.addField(f);

ASM

ASM通过修改字节码来实现AOP,我们知道Android打包过程是:.java文件 -> .class文件 -> .dex文件,它的实现和Javassist类似,也是需要自定义Gradle插件,利用Transform获取class。不同的是,它是直接修改字节码,一般是获取修改后的class文件,然后查看它对应的字节码文件,再通过ASM的api进行添加。

关于ASM操作字节码的介绍可以参考这篇文章

这里可以利用Jbe工具来查看class文件的字节码,具体如下图:

具体实现

这里同样是通过demo来说明操作步骤,demo的功能是在newFunc方法的头部增加一行输出

public void newFunc(String str) {
        System.out.println(str);
        for (int i = 0; i < 100; i++) {
            if (i % 10 == 0) {
                System.out.println(i);
            }
        }
    }

也就是实际上要实现下面这个效果:

public void newFunc(String str) {
        System.out.println("=========start=========");
        System.out.println(str);
        for (int i = 0; i < 100; i++) {
            if (i % 10 == 0) {
                System.out.println(i);
            }
        }
    }
  • 自定义Gradle插件
  • 获取最终实现效果代码的字节码,即我们要获取System.out.println("=========start=========");这行代码的字节码,一般做法是可以把代码编译成class文件再用jbe工具进行查看,

  • 自定义Transform遍历class
public class AutoTransform extends Transform {

    @Override
    String getName() {
        return "AutoTrack"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
        //此处会遍历所有文件
        /**遍历输入文件*/
        inputs.each { TransformInput input ->
            /**
             * 遍历jar
             */
            input.jarInputs.each { JarInput jarInput ->
            }
            /**
             * 遍历目录
             */
            input.directoryInputs.each { DirectoryInput directoryInput ->
                if (directoryInput.file.isDirectory()) {
                    directoryInput.file.eachFileRecurse { File file ->
                        def name = file.name
                        if (name.endsWith(".class") && !name.startsWith("R\$") &&
                                !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {

                            println name + ' is changing...'

                            //解析编译过的字节码文件
                            ClassReader cr = new ClassReader(file.bytes)
                            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            //自定义Visitor访问具体的成员信息
                            ClassVisitor cv = new AutoClassVisitor(cw)

                            cr.accept(cv, EXPAND_FRAMES)

                            byte[] code = cw.toByteArray()

                            FileOutputStream fos = new FileOutputStream(
                                    file.parentFile.absolutePath + File.separator + name)
                            fos.write(code)
                            fos.close()
                        }
                    }
                }
                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes,
                        Format.DIRECTORY)


                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each { JarInput jarInput ->
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }

                def dest = outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)

                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}
  • 通过ClassVisitor进行筛选
class AutoClassVisitor extends ClassVisitor {
    AutoClassVisitor(final ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        //如果methodName是newFunc,则返回我们自定义的TestMethodVisitor
        if ("newFunc".equals(name)) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            return new TestMethodVisitor(mv);
        }
        if (cv != null) {
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        return null;
    }
}
public class TestMethodVisitor extends MethodVisitor {
    public TestMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }
    @Override
    public void visitCode() {
        //方法体内开始时调用
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("========start=========");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        super.visitCode();
    }
    @Override
    public void visitInsn(int opcode) {
        //每执行一个指令都会调用
//        if (opcode == Opcodes.RETURN) {
//            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//            mv.visitLdcInsn("========end=========");
//            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
//        }
        super.visitInsn(opcode);
    }
}
  • 执行结果

ASM的一些核心类介绍

可以参考这篇文章

ASM框架中的核心类有以下几个:

ClassReader:该类用来解析编译过的class字节码文件。

ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。

ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。

AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。

总结

AOP的实现方法有多种,可以根据具体项目去选择,但是核心的应该是它面向切面编程的思想,拓宽了问题处理的思路。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值