安卓Android AOP技术Android

从Android优雅权限框架理解AOP思想(2) 原理篇

2019-12-02  本文已影响0人  波澜步惊

前言

上一个大的系列文章叫 "手把手讲解", 历时10个月,出产博文二十余篇,讲解细致,几乎每一篇都提供了详实的原理讲解,提供了可运行github Demo,并且针对Demo中的关键地地方进行了重点拆解。相信每一位详细阅读文章的同行都会有所收获。但是,讲解虽详细,但是缺乏对于技术的深度的挖掘。

从今天开始开辟新的专题: 移动架构师专业技能深入浅出,以一步步成为架构师为目标,详述一项架构师技能的最直接使用价值横向周边知识以及纵深专业技术.

最直接使用价值: 网上最怕看到一种文章,全文开篇高大上,让人觉得遥不可及,通篇看下来却没有展示技术如何落地,落地之后是何种效果。文章写出来,就要以最容易让人接受的方式带读者进入作者的世界,而不是装作一副高高在上的样子俯视众生。所以,文章开篇,一定是最直接的展示技术的落地效果。提供可运行Demo可以让读者亲自尝试。

横向周边知识: 一项核心技术,必然不是独立存在,技术是一个体系,但是一篇文章能够详述的技术有限,必然是以一项技术为中心,其他技术作为辅助。核心技术需要详述,但是周边技术,也需要交代,参天大树拔地而起也少不得土壤作为依附。用简明的语言交代周边知识,并提供这些知识正确的研究方向。也是一个负责任的博文作者不可忽视的一步。

纵深专业技术: 做技术,最忌讳的就是浅尝辄止。稍微深入一点就退出去,一来不利于理解底层实现,长此以往永远只是一个技术小白,成不了大师;二来不利于长久记忆, 记忆力再强的人时间长了,技术细节必然会记忆模糊。但是如果深入内核,理解了原理,在技术的大方向上绝对不会偏差。作为要成为架构师的男人,即使记不了那么多细节,但是对于大方向的把握绝对不能错。所以,技术纵深很有必要。

正文大纲

  • AOP思想回顾
  • AOP实现方式一览
  • AspectJ原理详解

正文

承接上文 从Android优雅权限框架理解AOP思想(1) 表层篇

AOP思想回顾

上一篇提到,所谓AOP思想,就是在OOP面向对象编程的基础上,为了避免大量雷同的代码,也为了更加灵活的分离业务代码辅助代码,我们可以采用AOP的思想进行代码解耦,让业务代码更加纯粹,让程序更加优雅。

上一篇文章的demo 解耦的具体表现为:在原本辅助代码的位置,使用注解来替换原本代码,让辅助代码脱离业务代码,而在框架层面,解读@注解,执行辅助代码,由此达成解耦。

aop (1).jpg
但是,AOP是一个宽泛的概念,可以使用注解作为标记,也可以不用注解。AOP切入的时机也可以不同。

AOP 实现方式一览

java开源框架中,AOP的框架一箩筐,跟我们关系不大,我就不列举了,但是 android开发中,常用的AOP框架是传说中的:
AOP三剑客APT , AspectJ , javasist

我们进行android开发,使用的是java语言(kotlin代码,但是kotlin也会被当作java来编译,所以没有区别), 上述这三个AOP框架分别对应了 java文件到dex文件的3个阶段:

借用一张图

三个AOP切入时机

  • APT:
    java代码编译之前的时机作为切入点,切入之前和之后的区别是,多出了若干个 xxx.java文件, 然后利用反射,在 我们写的业务代码中,去使用多出来的这些java文件. 从而节省业务代码量,也避免了代码的耦合。
  • AspectJ
    编译器把所有java文件整合好了之后,下一个步骤是将 java文件 用javac命令编译成 class文件。这时,使用AspectJ,可以干扰 .class 二进制字节码文件的生成过程,可以改变 原有.class文件的内容,也可以生成新的.class文件。我们想要插入的业务逻辑,把编译期作为 切入点,众多class文件,都会集中打包成dex文件。
  • Javassist:
    所有的.class都已经生成,即将用dex命令打包成classes.dex文件。这个时候,我们还是有机会去干扰业务逻辑。使用Javassist框架,赶在dex命令执行之前, 对现有的class文件进行更改。

AspectJ原理详解

现象

Demo地址:https://github.com/18598925736/GracefulPermissionFramework/commits/dev_aspectJ

以上面的Demo为例:上一篇文章 从Android优雅权限框架理解AOP思想(1) 表层篇说道,我们利用框架思想,将 权限申请的 多种情形 的差异化消除,让所有需要使用权限的地方都能以普通java类的形式来编码。形如:

public class LocationUtil {

    @PermissionNeed(
            permissions = {Manifest.permission.BODY_SENSORS},
            requestCode = 0)
    public void getLocation() {
        Log.d("LocationUtilTag","普通Java类:权限已获得");
        ToastUtil.showToast("普通Java类:权限已获得");
    }

    /**
     * 这里写的要特别注意,denied方法,必须是带有一个int参数的方法,下面的也一样
     *
     * @param requestCode
     */
    @PermissionDenied
    private void denied(int requestCode) {
        Log.d("LocationUtilTag","普通Java类:权限被拒绝");
        ToastUtil.showToast("普通Java类:权限被拒绝");
    }

    @PermissionDeniedForever
    private void deniedForever(int requestCode) {
        Log.d("LocationUtilTag","普通Java类:权限被永久拒绝");
        ToastUtil.showToast("普通Java类:权限被永久拒绝");
    }
}

只需要3个注解 @PermissionNeed @PermissionDenied @PermissionDeniedForever 就能完成权限的动态申请,以及 处理 申请结果回调. 所以说,到底是怎么做到的???
让我们把思维归0,就以此处为起点,来探索AspectJ是如何做到 逻辑插入的。

  1. Apk反编译,看一眼 apk dex里面的LocationUtil这个类变成了什么样儿。
    (推荐一个apk反编译工具 jadx-0.6.1,可以直接看到apk的dex源代码,但,如果混淆了,你看到的将会是混淆之后的源码.)
public class LocationUtil {
    private static /* synthetic */ Annotation ajc$anno$0;
    private static final /* synthetic */ StaticPart ajc$tjp_0 = null;

    public class AjcClosure1 extends AroundClosure {
        public AjcClosure1(Object[] objArr) {
            super(objArr);
        }

        public Object run(Object[] objArr) {
            objArr = this.state;
            LocationUtil.getLocation_aroundBody0((LocationUtil) objArr[0], (JoinPoint) objArr[1]);
            return null;
        }
    }

    static {
        ajc$preClinit();
    }

    private static /* synthetic */ void ajc$preClinit() {
        Factory factory = new Factory("LocationUtil.java", LocationUtil.class);
        ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, factory.makeMethodSig("1", "getLocation", "com.zhou.graceful.LocationUtil", "", "", "", "void"), 20);
    }

    static final /* synthetic */ void getLocation_aroundBody0(LocationUtil ajc$this, JoinPoint joinPoint) {
        joinPoint = "普通Java类:权限已获得";
        Log.d("LocationUtilTag", joinPoint);
        ToastUtil.showToast(joinPoint);
    }

    @PermissionNeed(permissions = {"android.permission.BODY_SENSORS"}, requestCode = 0)
    public void getLocation() {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, this, this);
        PermissionAspect aspectOf = PermissionAspect.aspectOf();
        ProceedingJoinPoint linkClosureAndJoinPoint = new AjcClosure1(new Object[]{this, makeJP}).linkClosureAndJoinPoint(69648);
        Annotation annotation = ajc$anno$0;
        if (annotation == null) {
            annotation = LocationUtil.class.getDeclaredMethod("getLocation", new Class[0]).getAnnotation(PermissionNeed.class);
            ajc$anno$0 = annotation;
        }
        aspectOf.doPermission(linkClosureAndJoinPoint, (PermissionNeed) annotation);
    }

    @PermissionDenied
    private void denied(int requestCode) {
        String str = "普通Java类:权限被拒绝";
        Log.d("LocationUtilTag", str);
        ToastUtil.showToast(str);
    }

    @PermissionDeniedForever
    private void deniedForever(int requestCode) {
        String str = "普通Java类:权限被永久拒绝";
        Log.d("LocationUtilTag", str);
        ToastUtil.showToast(str);
    }
}

经过两方对比,我们发现AspectJ给我们原本的LocationUtil.java源码中做的改动为:

  • 新增了 2个成员变量,1个内部类,1个静态代码块,2个成员方法
  • 改动了原本的 getLocation() 方法 的逻辑

从结论上来看,AspectJ进行AOP的手段为: 在我们编写好源码之后,改动 class字节码,让程序执行的逻辑发生了变化.

  1. 还是jadx,现在看看整个工程的源码目录结构。

AspectJ 在我们的字节码class中加入了一个新的package,里面全都是框架性质的代码。

小结

AspectJ 通过改动字节码class 从而改变了程序逻辑。而且是可以像我们操作源码一样,随意新增新的类以及改动现有的类。相当于把我们的源码进行了重构.

那么随之而来,就有下一个疑问,AspectJ是按照什么思路重构我们原有的代码逻辑的
但是在此之前,现象背后有几个概念先声明:

现象背后

在此感谢博主:https://blog.csdn.net/woshimalingyi/article/details/73252013 ,网上翻了很多博客,就这个写的比较完善。

AspectJ 的核心分为两部分: 编译器 ajc织入器 weaver , 在源代码的编译期间扫描目标程序,根据切入点PointCut的匹配,将我们编写的 Aspect切面程序 编织到目标程序的.class中,对目标文件做了重构。然后我们就看到了上一节 中,class反编译出来 之后 和 我们编写的源文件有很大区别

概念

定义一个切面类,通常以功能作为划分。比如,日志打印切面,方法执行耗时切面,权限申请切面等。每一个切面都有它的独特功能。定义了切面类,该切面的所有功能就被集中在这一个类中,该类的名字要有意义。
具体的做法非常简单,只需要加入一个@Aspect注解在类的头上。

@Aspect // 把一个类定义为切面类
public class PermissionAspect {//  权限切面类,集中处理所有权限问题
  //...
}

定义切入点,来让AspectJ框架知道 我们要改变 目标程序的哪些Method的逻辑,通过MethodSignature匹配。
所谓MethodSignature就是,写一个表达式,给AspectJ框架去解析,让他找出目标程序中所有符合要求的Method,然后作为切入点,准备插入切面逻辑。
其写法规则如下:

最常用的PointCut切入点
除此之外,还有一些不那么常用的切入点,算是高级特性:
image.png

当我们确定了切入点之后,这个切入点上原来的代码逻辑,就是连接点,因为 我们插入的逻辑,终将和原来的逻辑合为一体。 这里说的原来的逻辑,就称为 JoinPoint连接点。举个生动的例子,树木的嫁接。

image.png
如图所示,原来的树木,我们劈开一个切口,就相当于 PointCut切入点,表示 这里就作为原木和新木的交接处。而图中,PointCut的左边,是原木,也就是 我们的连接点JoinPoint右边,则是 新木,就是我们定义的Aspect切面。当我们的程序最终编译之后,就会是右边树木的最终样子,合并成一个class,逻辑也合并到一起。
JoinPoint常用的只有2个接口:
JoinPoint.javaProceedingJoinPoint.java,他们的关系为 ProceedingJoinPoint extends JoinPoint
  • @Before、@After
    插入的逻辑放在 原JoinPoint逻辑的前面或者后面,不影响原逻辑
  • @Around
    插入的逻辑完全替代原JoinPoint逻辑,但是保留是否要执行原逻辑的决定权。
  • @AfterThrowing
    在原JoinPoint逻辑 抛出异常之后,该操作优先于下一个切点的@Before()

现在来看看AspectJ对我们的LocationUtil类做了什么

image.png

上图中,划分为3个部分,
1) 平白无故多出来的部分
2) 原先存在的getLocation()方法被修改了,方法体被完全替换
3) 并未改动的部分

按照这个类被加载时的执行顺序来看:
首先是 静态代码块:

    static {
        ajc$preClinit();
    }

    private static /* synthetic */ void ajc$preClinit() {
        Factory factory = new Factory("LocationUtil.java", LocationUtil.class);
        ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, 
            factory.makeMethodSig("1", "getLocation", "com.zhou.graceful.LocationUtil", "", "", "", "void"), 20);
    }

在JVM开始创建这个类的class对象的时候,ajc$preClinit()方法便开始执行。
创建了一个Factory,然后用Factory.makeSJP()方法创建了StaticPart.
读一遍代码,可以发现:

LocationUtil.java 是PointCut切入点类。
JoinPoint.METHOD_EXECUTION PointCut切入点的其中一个时机 execution.

factory.makeMethodSig(...) 返回的是MethodSignature,也就是用于标记Method的签名。

可以作结论了,第一步,AspectJ框架,先通过注入代码的方式,确定了LocationUtil类的PointCut切入点,切入点就是getLocation方法,并且最终创建了一个静态的StaticPart ajc$tjp_0对象,保存了起来(其实它就是一个中间产物"静态部分信息",后续将会用它来创建JointPoint对象).
我们继续。
发现一个奇怪的东西: 闭包

   public class AjcClosure1 extends AroundClosure {
        public AjcClosure1(Object[] objArr) {
            super(objArr);
        }

        public Object run(Object[] objArr) {
            objArr = this.state;
            LocationUtil.getLocation_aroundBody0((LocationUtil) objArr[0], (JoinPoint) objArr[1]);
            return null;
        }
    }
    static final /* synthetic */ void getLocation_aroundBody0(LocationUtil ajc$this, JoinPoint joinPoint) {
        joinPoint = "普通Java类:权限已获得";
        Log.d("LocationUtilTag", joinPoint);
        ToastUtil.showToast(joinPoint);
    }

关于闭包的概念:闭包是一段程序的封装,它不同于method方法,它可以在java代码中作为参数传递给其他闭包或method.

这个闭包把我们原先的getLocation()方法的方法体,重新封装起来了。既然是封装起来,那么 肯定是要使用的。

继续往下走,进入改过之后的getLocation()

    @PermissionNeed(permissions = {"android.permission.BODY_SENSORS"}, requestCode = 0)
    public void getLocation() {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, this, this); 
        PermissionAspect aspectOf = PermissionAspect.aspectOf();
        ProceedingJoinPoint linkClosureAndJoinPoint = new AjcClosure1(new Object[]{this, makeJP}).linkClosureAndJoinPoint(69648);
        Annotation annotation = ajc$anno$0;
        if (annotation == null) {
            annotation = LocationUtil.class.getDeclaredMethod("getLocation", new Class[0]).getAnnotation(PermissionNeed.class);
            ajc$anno$0 = annotation;
        }
        aspectOf.doPermission(linkClosureAndJoinPoint, (PermissionNeed) annotation);
    }
  • 第一行 Factory.makeJP(ajc$tjp_0, this, this);
    使用之前的中间产物 StaticPart 构建一个JoinPoint连接点对象;
    注意: 后面两个this,第一个是 给_this赋值,后一个是 target赋值。_this是当前插入代码的类的对象。target则是当前插入代码的类的调用者的对象。在这里,我们都传的this,这是因为我们用的是切入点pointcut是execution,如果是call,两种则不会相同。
  • 第二行PermissionAspect aspectOf = PermissionAspect.aspectOf();
    得到了一个PermissionAspect类对象,即 我们最先定义的@Aspect修饰的切面类。
  • 第三行 ProceedingJoinPoint linkClosureAndJoinPoint = new AjcClosure1(new Object[]{this, makeJP}).linkClosureAndJoinPoint(69648);
    利用第一行得到的JoinPoint对象,配合之前的闭包AjcClosure1,构建一个ProceedingJoinPoint 对象。(他是@Around策略特有的JoinPoint子类,因为@Around要保留执行原有逻辑的可能,ProceedingJoinPoint有一个proceed()方法)
    跟踪参数this, makeJP: this是当前运行时对象,即LocationUtil类对象,makeJP是JoinPoint对象。把这两个传给闭包,闭包内部的 LocationUtil.getLocation_aroundBody0() 方法才能执行,调用到当前运行时对象的getLocation_aroundBody0() 方法。
  • 第四行到倒数第二行
    这里由于我们的优雅权限框架使用了自定义注解,所以,这里拿到Annotation对象并且缓存到静态成员变量中.
  • 最后一行
    使用@Aspect切面类 PermissionAspect,执行doPermission()方法。并且把之前的前面得到的ProceedingJoinPointAnnotation作为参数传递给doPermission(ProceedingJoinPoint joinPoint, PermissionNeed permissionNeed)方法。

做个结论

上面的分析,非常直观的展示了AspectJ对我们的原逻辑一顿猛如虎的操作之后,LocationUtil类中的逻辑被完全重构,但是并没有对我们手动编码的部分造成影响,而是只影响了生成的class. 这就是AOP的目的,在不干扰原有业务逻辑的情况下,改变现有逻辑,或者加入新的逻辑。

特别注意

@Pointcut(argNames = "permissionNeed", value = "execution(@com.zhou.zpermission.annotation.PermissionNeed * *(..)) && @annotation(permissionNeed)")
这种方法签名很容易写错,特别是针对不同需求,当你想直接在签名中加入参数列表的,或者注解的时候,写法还各不一样。目前我还没特别总结过,目前常用的就两种:参数args / 注解@annotation
注解args的写法如Demo中所示

image.png

而 参数args写法 则是 如下:

image.png

预计接下来研究方向

现在知道了 AspectJ 这个AOP框架对我们的 源代码逻辑做了什么改动。但是具体是如何改动的,尚未可知。 其实,AspectJ的底层是ASM,一种更加底层字节码操作框架,class内容重构的api都包含在内。计划下一步继续深入挖掘。


结语

出于篇幅原因,本文只是 分析了@Around策略下 AspectJ的代码织入过程。其实要写详细,要做的事情还有很多,demo也可以更加完善。后续的有时间再详解吧。

上一篇下一篇

猜你喜欢

热点阅读