查看原文
其他

白话 AOP,带你入门一波Gradle Plugin

lenebf 鸿洋 2021-10-12

本文作者


作者:lenebf

链接:

https://juejin.cn/post/6893917892061413389

本文由作者授权发布。


1概述


AOP(Aspect Oriented Programming 的缩写),意为:面向切面编程,和OOP(Object Oriented Programming,面向对象编程)以对象为核心不同,AOP 则是针对业务处理过程中的相同或者相似的代码逻辑(切面)进行提取,然后统一处理,它所面对的是处理过程中的某个步骤或阶段。


这两种设计思想在目标上有着本质的差异,但是 AOP 和 OOP 并不是对立的,相反,巧妙的结合这两种思想来指导代码编写,会让我们的代码保持可重用性的同时,显著降低各个部分之间的耦合度。

OOP 和 AOP 都是方法论[1],是我个人认为对这两种思想最准确的描述和总结。


我们先设一个具体的需求场景,结合需求实现讲解 Android AOP,需求如下:在App里所有 Activity 的 onResume, onPause 方法执行时打印日志,日志内容随意。


2使用继承


利用对象继承的特性,我们可以抽象出一个 BaseActivity,在 BaseActivity 的 onResume, onPause 方法体里打印日志。然后其它所有的 Activity 对象都继承于 BaseActivity,这样就实现需求:


// BaseActivity.java
public abstract class BaseActivity extends AppCompatActivity {
    private static final String TAG = "lenebf";

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, getClass().getSimpleName() + ": onPause");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, getClass().getSimpleName() + ": onResume");
    }
}

// MainActivity.java
public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

// SecondActivity.java
public class SecondActivity extends BaseActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

这样做新项目还好,如果是一个包含几十或者上百个 Activity 的大型项目,挨个去改各个 Activity 的继承关系是不是有点愚公移山的意思?


3ActivityLifecycle


通过注册 ActivityLifecycleCallbacks 我们能够获取到所有 Activity 的生命周期回调,也就轻松实现我们的需求了。


// DemoApplication.java
public class DemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            ......
            @Override
            public void onActivityResumed(@NonNull Activity activity) {
                Log.d(TAG, "${activity.javaClass.simpleName}: onResume");
            }

            @Override
            public void onActivityPaused(@NonNull Activity activity) {
                Log.d(TAG, "${activity.javaClass.simpleName}: onPause");
            }
            ......
        });
    }
}

我们细细的品一下上面的代码,相同的逻辑在同一个地方处理,这难道就是传说中的 AOP?对的,你真的特别聪明!


并不是所有需求都像上述例子一样有现成的切面供我们使用,下面我们学习Transform.


4Transform


Android Gradle Plugin 从 1.5.0 开始支持 Transform API,以允许第三方插件在经过编译的 .class 文件转换为 .dex 文件之前对其进行操纵。使用起来很简单,新建一个Gradle Plugin Module,然后创建一个类来实现 Transform 接口,获取 AppExtension 并使用 android.registerTransform(theTransform) 或 android.registerTransform(theTransform, dependencies) 向 Android Gradle Plugin 的扩展属性中注册 Transform。



Transform 参考文档:

https://developer.android.com/reference/tools/gradle-api/4.1/com/android/build/api/transform/Transform


ASM


要想修改 .class 文件,我们首先得搞清楚 .class 的文件结构,这可不是个简单的事情,好在有大神创建了神器 ASM。


ASM是一个通用的Java字节码操作和分析框架。它可以用来修改现有的类,也可以直接以二进制形式动态生成类。


ASM 开发文档:

https://asm.ow2.io/asm4-guide.pdf


简单来讲,ASM 将 .class 文件抽象为 ClassVisitor、MethodVisitor 等对象,类访问器 ClassVisitor 读取和分析 .class 文件,然后发现方法后,交给方法访问器 MethodVisitor 分析修改方法逻辑,当然也可以在类访问器中新增方法。


ASM Bytecode Outline


虽然 ASM 降低了解析和修改 .class 文件的门槛,但其本身的 API 也有一定的学习成本,本文并不打算详细讲解这部分内容,大家可以自行阅读 ASM 的开发文档。


这里借助另一个神器 ASM Bytecode Outline,这是一个 IDEA 插件,兼容 IntelliJ IDEA, Android Studio,通过这个插件我们可以查看 java 文件对应的 ASM 代码,还可以对比两个 java 文件的 ASM 代码的差异,具体到我们的例子,通过对比无日志打印逻辑和有日志打印逻辑的差异 ASM代码就能得到我们需要的 ASM 代码。

https://plugins.jetbrains.com/plugin/5918-asm-bytecode-outline


ASM Bytecode Outline 安装:


ASM Bytecode Outline 的最新版本,无法在 Android Studio 4.1 上使用,只支持 IntelliJ IDEA,我们使用 IntelliJ IDEA 安装。
Preferences -> Plugins -> Markplace 搜索 ASM,下载量第一的就是 ASM Bytecode Outline 插件,安装然后重启 IntelliJ IDEA。


查看 ASM 代码


(类文件或者代码)右键 -> Show Bytecode Outline -> ASMified


显示 ASM 代码差异


(类文件或者代码)右键 -> Show Bytecode Outline -> ASMified -> Show differences


创建 Gradle Plugin


插件名字就叫 ac_logger,创建 Gradle Plugin Module 的具体步骤请看 Android Gradle 插件开发入门指南(一) 

https://juejin.cn/post/6887581345384497165


由于 Transform 属于 Android Gradle Plugin 的 API,所以我们的插件需要依赖com.android.tools.build:gradle;我们还需要用到 ASM,所以插件也需要依赖 org.ow2.asm:asm:9.0 具体步骤请看 Android Gradle 插件开发入门指南(二)

https://juejin.cn/post/6887583351348133895


创建 Transform 实现类


注意,插件编写支持 groovy 或者 java,使用的语言不同,类的存放位置有所差异:


  • java - src/main/java/...

  • groovy - src/main/groovy/...


逻辑比较简单,就直接上代码了,注释写的比较清楚:


public class LoggerTransform extends Transform {

    @Override
    public String getName() {
        // 转换器的名字
        return "ac_logger";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        // 返回转换器需要消费的数据类型。我们需要处理所有的 class 内容
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        // 返回转换器的作用域,即处理范围。我们只处理 Project 里面的类
        return TransformManager.PROJECT_ONLY;
    }

    @Override
    public boolean isIncremental() {
        // 是否支持增量,我们简单点不支持
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        // TODO 实现转换逻辑
    }
}

注册 Transform


public class ActivityLoggerPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Register a transform
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(new LoggerTransform())
    }
}

遍历所有 .class 文件


Trasnform API 的输入类型有两种,一种是目录,一种是 Jar 文件(对应着三方库),我这里只处理目录输入。我们需要从目录输入中过滤出所有的 .class 文件。


@Override
public void transform(TransformInvocation transformInvocation) throws TransformException,
        InterruptedException, IOException {
    super.transform(transformInvocation);
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    if (outputProvider == null) {
        return;
    }
    // 由于我们不支持增量编译,清空 OutputProvider 的内容
    outputProvider.deleteAll();
    Collection<TransformInput> transformInputs = transformInvocation.getInputs();
    transformInputs.forEach(transformInput -> {
        // 存在两种转换输入,一种是目录,一种是Jar文件(三方库)
        // 处理目录输入
        transformInput.getDirectoryInputs().forEach(directoryInput -> {
            File directoryInputFile = directoryInput.getFile();
            // 找到转化输入中所有的 class 文件,具体逻辑请看 github 代码
            List<File> files = filterClassFiles(directoryInputFile);
            // TODO 编辑 class 文件,添加日志打印逻辑
            // 有输入进来就必须将其输出,否则会出现类缺失的问题,
            // 无论是否经过转换,我们都需要将输入目录复制到目标目录
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(),
                    directoryInput.getScopes(),
                    Format.DIRECTORY);
            try {
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 处理 jar 输入
        transformInput.getJarInputs().forEach(jarInput -> {
            // 有输入进来就必须将其输出,否则会出现类缺失的问题,
            // 这里我们不需要修改Jar文件,直接将其输出
            File jarInputFile = jarInput.getFile();
            File dest = outputProvider.getContentLocation(jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            try {
                FileUtils.copyFile(jarInputFile, dest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    });
}

这里需要注意的是, Transform API 要求有输入必须有输出,所以即使不需要处理的文件,也需要将其输出到目标目录。


修改 .class 文件


在 ASM 中,提供了一个 ClassReader 类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept 方法,这个方法接受一个继承于 ClassVisitor 抽象类的对象实例作为参数,然后依次调用 ClassVisitor 抽象类的各个方法。


ClassWriter 类编写器,继承于 ClassVisitor,实现了具体的字节码编辑功能。各个 ClassVisitor 通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

我们首先需要实现我们自己的 ClassVisitor,实现里面相关的 visit 函数,来添加日志打印代码:


public class ActivityClassVisitor extends ClassVisitor {

    /**
     * Activity 的父类完整类名,这里我们只处理了 AppcompatActivity 的子类,
     * 生产中需要处理其他的 Activity 子类
     */

    private static final String ACTIVITY_SUPER_NAME = "androidx/appcompat/app/AppCompatActivity";
    private static final String ON_PAUSE = "onPause";
    private static final String ON_RESUME = "onResume";

    private String superName = null;
    private boolean visitedOnPause = false;
    private boolean visitedOnResume = false;

    public ActivityClassVisitor(ClassVisitor classVisitor) {
        // Opcodes.ASM9 表示我们使用的 ASM API 的版本,这里使用的最新版本的 API 9
        super(Opcodes.ASM9, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName,
                      String[] interfaces) 
{
        super.visit(version, access, name, signature, superName, interfaces);
        // 访问到了具体的类信息,name 当前类的完整类名,superName 表示父类完整类名,access 可访问性
        // 排除掉抽象类
        if ((access & Opcodes.ACC_ABSTRACT) != Opcodes.ACC_ABSTRACT) {
            this.superName = superName;
        }
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        if (superName != null && superName.equals(ACTIVITY_SUPER_NAME)) {
            // 类解析结束,还没有遍历到 onPause 或者 onResume 方法,直接生成完整函数
            if (!visitedOnResume) {
                visitedOnResume = true;
                insertMethodAndLog(ON_RESUME);
            }
            if (!visitedOnPause) {
                visitedOnPause = true;
                insertMethodAndLog(ON_PAUSE);
            }
        }
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                     String[] exceptions) 
{
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (superName != null && superName.equals(ACTIVITY_SUPER_NAME)) {
            // AppcompatActivity 的子类
            if (ON_PAUSE.equals(name)) {
                // onPause 方法
                visitedOnPause = true;
                addLogCodeForMethod(mv, name);
            } else if (ON_RESUME.equals(name)) {
                // onResume 方法
                visitedOnResume = true;
                addLogCodeForMethod(mv, name);
            }
        }
        return mv;
    }

    private void addLogCodeForMethod(MethodVisitor mv, String methodName) {
        mv.visitLdcInsn("lenebf");
        // 新建一个 StringBuilder 实例
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        // 调用 StringBuilder 的初始化方法
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder""<init>""()V"false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        // 获取当前类的 SimpleName
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object""getClass""()Ljava/lang/Class;"false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class""getSimpleName""()Ljava/lang/String;"false);
        // 将当前类的 SimpleName 追加进 StringBuilder
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""append""(Ljava/lang/String;)Ljava/lang/StringBuilder;"false);
        // 将方法名追加进 StringBuilder
        mv.visitLdcInsn(": " + methodName);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""append""(Ljava/lang/String;)Ljava/lang/StringBuilder;"false);
        // 调用 StringBuilder 的 toString 方法将 StringBuilder 转化为 String
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""toString""()Ljava/lang/String;"false);
        // 调用 Log.d 方法
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log""d""(Ljava/lang/String;Ljava/lang/String;)I"false);
        mv.visitInsn(Opcodes.POP);
    }

    private void insertMethodAndLog(String methodName) {
        // 创建新方法
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PROTECTED, methodName, "()V"nullnull);
        // 访问新方法填充方法逻辑,
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "androidx/appcompat/app/AppCompatActivity", methodName, "()V"false);
        mv.visitLdcInsn("lenebf");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder""<init>""()V"false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object""getClass""()Ljava/lang/Class;"false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class""getSimpleName""()Ljava/lang/String;"false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""append""(Ljava/lang/String;)Ljava/lang/StringBuilder;"false);
        mv.visitLdcInsn(": " + methodName);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""append""(Ljava/lang/String;)Ljava/lang/StringBuilder;"false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder""toString""()Ljava/lang/String;"false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log""d""(Ljava/lang/String;Ljava/lang/String;)I"false);
        mv.visitInsn(Opcodes.POP);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitEnd();
    }
}

然后在 transform 函数中使用我们的 ClassVisitor:


@Override
public void transform(TransformInvocation transformInvocation) throws TransformException,
        InterruptedException, IOException 
{
    ......
    transformInputs.forEach(transformInput -> {
        // 存在两种转换输入,一种是目录,一种是Jar文件(三方库)
        // 处理目录输入
        transformInput.getDirectoryInputs().forEach(directoryInput -> {
            File directoryInputFile = directoryInput.getFile();
            List<File> files = filterClassFiles(directoryInputFile);
            for (File file : files) {
                FileInputStream inputStream = null;
                FileOutputStream outputStream = null;
                try {
                    //对class文件的写入
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    //访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
                    ClassVisitor classVisitor = new ActivityClassVisitor(classWriter);
                    //对class文件进行读取与解析
                    inputStream = new FileInputStream(file);
                    ClassReader classReader = new ClassReader(inputStream);
                    // 依次调用 ClassVisitor接口的各个方法
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    // toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
                    byte[] bytes = classWriter.toByteArray();
                    //通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
                    outputStream = new FileOutputStream(file.getPath());
                    outputStream.write(bytes);
                    outputStream.flush();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                } finally {
                    closeQuietly(inputStream);
                    closeQuietly(outputStream);
                }
            }
            ......
        });
        ......
    });
}

至此,我们的需求就完成了,具体效果如何呢?


检验结果


我们有多种方式检查我们的插件是否完成了我们的需求,最直白的就是直接运行我们的 Demo,看是否有对应的日志输出: 


 

的确如我们所愿,输出了正确的日志信息。我们也可以直接查看 Transform 输出的 .class 文件,位于 app/build/intermediates/transforms 目录: 


 

我们的日志打印代码准确的加进去了。还可以借助 Apktool 工具反编译我们的 Apk 查看里面的代码实现。


本文代码地址:

https://github.com/lenebf/AndroidAOPTutorial


参考资料

[1] 深入理解Android之AOP

https://blog.csdn.net/innost/article/details/49387395

[2] AOP 的利器:ASM 3.0 介绍

https://developer.ibm.com/zh/articles/j-lo-asm30/





最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!



推荐阅读


RecyclerView 是如何实现炫酷的Item动画的?
Android 性能优化“基石”是什么? Fps,Memory,Cpu如何采集?
经历这么多版本,RxJava本质上不变的是什么?


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存