鸿蒙架构之AOP

07-19 1558阅读

零、主要内容

  • AOP 简介
  • ArkTs AOP 实现原理
    • JS 原型链
    • AOP实现原理
    • AOP的应用场景
      • 统计类: 方法调用次数统计、方法时长统计
      • 防御式编程:参数校验
      • 代理模式实现
      • AOP的注意事项

        一、AOP简介

        对于Android、Java Web 开发者来说, AOP编程思想并不陌生。 AOP的使用核心在于要找到 Aspect(切面),然后再根据自己的需要,对某个“业务操作进”行 前置或者后置的处理,甚至可以替换“该业务操作”。 AOP的操作粒度就是方法级别, 一个方法包括 接收数据、处理数据和返回数据这么三个部分:

        鸿蒙架构之AOP

        AOP 在这三个阶段都可以添加自己的逻辑处理。 Java中常见的AOP框架有很多:AspectJ、SpringAOP、Javassist、Guice、Byte Buddy等。ArkTs在4.0版本中也支持了AOP,那么ArkTs是如何实现AOP的呢?

        二、ArkTs AOP 实现原理

        接下来,我们首先要了解一下JS对象的在继承体系中的引用关系,这样才能够精准的选择合适的方法来进行切面编程。 然后我们在了解一下AOP是如何实现的。

        2.1 JS 原型链

        鸿蒙架构之AOP

        如上图所示:

        水平维度:类通过prototype 引用着其原型对象, 通过constructor引种着其构造函数; 该类的构造函数中,关联着该类的静态方法;

        竖直维度:类的原型对象通过__proto__指向父类原型对象;类的构造函数通过__proto__指向父类的构造函数;类的实例对象通过__proto__指向该类的原型对象;

        那么对于实例对象a和对象b来说,其实例方法的定位如下图红色路径所示;对于类A和类B类说,其静态方法的的定位流程如下图蓝色路径所示:

        鸿蒙架构之AOP

        通过上图,我们可以得出如下结论:

        类的原型对象承载着该类对象的实例实例方法(非静态方法),并且通过__proto__ 指向父类的原型对象,通过constructor指向类(也就是类的构造函数,需要额外指出的是 类的静态方法存储在构造函数中)。 类(类的构造函数)通过__proto__指向父类(父类的构造构造函数)。

        2.2 AOP实现原理

        AOP的实现依赖于 插桩和替换来实现的, 其本质上将回调参数和原方法组合成一个新的函数,再用新的函数替换原方法,具体如下图所示:

        “计算机科学中的所有问题都可以通过增加一个额外的间接层来解决”

        鸿蒙架构之AOP

        2.2.1 AddBefore 原理的伪代码

        // addBefore 的伪代码实现
        static addBefore(targetClass, methodName , isStatic , before:Function) : void {
            // 根据是否静态方法,获取要插装的对象(是“类” ,还是“类的原型对象”)
            let target = isStatic ? targetClass : targetClass.prototype;
            // 根据方法名,获取原有的方法
            let origin = target[methodName];
            /**
            * 定义新的方法(包装一层),实现优先执行before的逻辑,然后执行原有方法origin,
            * 最后将返回结果给 外层调用者。
            */
            let newFuncs = function(...args) {
                // 先执行before方法,再执行当前方法
                before(this,...args);
                return origin.bind(this)(...args);    
            }
            // 使用新函数生效
            target[methodName] = newFuncs;
        }
        

        2.2.2 AddAfter 原理的伪代码

        // addAfter 的伪代码实现
        static addAfter(targetClass, methodName , isStatic , after:Function) :void {
            let target = isStatic ? targetClass : target.protoType;
            let original = target[methodName];
            let newFuncs = function(...args) {
                let ret = origin.bind(this)(...args);
                return after(this,r,...args); 
            }
        }
        

        2.2.3 Repalce 原理的伪代码

        static replace(targetClass, methodName , isStatic , instead) :void {
            let target = isStatic ? targetClass : target.protoType;
            let newFuncs = function(...args) {
                return instead(this,...args); 
            }
            target[methodName] = newFuncs;
        }
        

        三、AOP的应用场景

        • 统计类: 方法调用次数统计、方法时长统计
        • 防御式编程:参数校验、返回值校验
        • 继承体系中的精确Hook
        • 代理模式和IOC

          3.1 统计类

          3.1.1 方法调用次数统计

          export class Test {
              hello () {
                  console.log('hello world')    
              }
          }
          

          我们通过Aspect.addBefore实现对Test类 hello方法调用次数的统计。

          function main() {
              let countHello = 0;
              util.Aspect.addBefore(Test,'hello',false , ()=> {
                  countHello++;
              });
              let h = new Test();
              console.log(`countHello : ${countHello}`)
              h.hello();
              console.log(`countHello : ${countHello}`)
          }
          

          3.1.2 方法时长统计

          function addTimePrinter(target:Object, methodName:string, isStatic:boolean) {
              let t1 = 0;
              let t2 = 0;
              util.Aspect.addBefore(targetClass, methodName, isStatic, () => {
                  t1 = new Date().getTime();
              });
              util.Aspect.addAfter(targetClass, methodName, isStatic, () => {
                  t2 = new Date().getTime();
                  console.log("t2---t1 = " + (t2 - t1).toString());
              });
          }
          

          测试addTimePrinter的功能:

          export class View {
              onDraw() {
                  // ...             
              }
              
              static cinit() {
                  // ... 
              }
          }
          function main() {
              // 测试静态方法的时长统计
              addTimePrinter(Test,'cinit',true);
              View.cinit();
              // 测试实例方法的时长统计
              addTimePrinter(Test,'onDraw',true);
              new View().cinit();
          }
          

          3.2 防御式编程

          • 校验参数
          • 纠正返回值

            3.2.1 校验参数

            export class P004_View {
              children:P004_View[];
              constructor(children:Array) {
                this.children = children
              }
              getViewByIndex(index:number):P004_View {
                return this.children[index];
              }
            }
            

            上述View类的实例方法 getViewByIndex 的入参是一个index, 为了避免索引越界情况,我们可以通过Aspect类addBefore,增加一层”参数校验“的逻辑。

            util.Aspect.addBefore(P004_View,"getViewByIndex",false, (view:P004_View, index:number)=> {
              if(view.children) {
                throw Error('view.children is undefined !')
              }
              if(index 
                throw Error('index can not be negative !')
              }
              if((view.children as P004_View[]).length 
                throw Error('index is too big !')
              }
            })
                    
                static randomSmallerThan50():number {
                    return Math.floor(Math.random() * 52);
                }
            }
            
              util.Aspect.addAfter(P004_Random,'randomSmallerThan50',true,(target:P004_Random,ret:number)= {
                if(ret  50) {
                  return P004_Random.randomSmallerThan50()
                } else {
                  console.log(`P004_Random_randomSmallerThan50_addAfter ${ret}`)
                  return ret;
                }
              })
              P004_Random.randomSmallerThan50()
            }
            

            3.3 子类实例方法替换

            export class AirCraft {
              fly() {
                console.log('fight....')
              }
            }
            export class USA_AirCraft extends AirCraft{}
            export class CN_AirCraft extends AirCraft{}
            

            我们也可以通过Aspect类实现对子类的某个方法的 插桩或者替换。 下面是替换USA_AirCraft类的fly方法的代码:

            export function testAirCraft() {
              let cn = new CN_AirCraft()
              let usa = new USA_AirCraft();
              cn.fly()
              usa.fly()
              util.Aspect.replace(USA_AirCraft,"fly",false,()=> {
                console.log('runaway....')
              })
              cn.fly()
              usa.fly();
            }
            

            3.4 控制反转(IOC)

            AOP 也可以实现 控制反转。 如下图所示, PlayerManager 封装了播放器IPlayer接口,IPlayer 有ijkPlayer和mediaPlayer两个子类。 我们可以通过AOP 替换PlayerManager中的init() start() 等方法,来实现 两种Player对象的切换 。

            鸿蒙架构之AOP

            上图中UML中的类,对应代码如下:

            interface IPlayer {
              init(): void
              start(): void
              stop(): void
              release(): void
            }
            export class PlayManager {
              player?: IPlayer
              init(): void {
              }
              start(): void {
              }
              stop(): void {
              }
              release(): void {
              }
            }
            export class IjkPlayer implements IPlayer {
              init(): void {
                console.log('IjkPlayer init ...')
              }
              start(): void {
                console.log('IjkPlayer start ...')
              }
              stop(): void {
                console.log('IjkPlayer stop ...')
              }
              release(): void {
                console.log('IjkPlayer release ...')
              }
            }
            export class MediaPlayer implements IPlayer {
              init(): void {
                console.log('MediaPlayer init ...')
              }
              start(): void {
                console.log('MediaPlayer start ...')
              }
              stop(): void {
                console.log('MediaPlayer stop ...')
              }
              release(): void {
                console.log('MediaPlayer release ...')
              }
            }
            

            接下来,我们通过Aspect的replace方法来实现 player对象的替换:

            /*
            * 该方法 根据methodName,返回一个函数。该函数中会 当前player的对应的方法,并返回。 
            */
            export function providePlayer(methodName: string, playerFetcher: ()=>IPlayer) {
              return (manager: PlayManager) => {
                if (methodName === 'init') {
                  return playerFetcher().start()
                } else if (methodName === 'init') {
                  return playerFetcher().start()
                } else if (methodName === 'start') {
                  return playerFetcher().start()
                } else if (methodName === 'stop') {
                  return playerFetcher().start()
                } else if (methodName === 'release') {
                  return playerFetcher().release()
                }
              }
            }
            export function testPlayer() {
              let player:IPlayer = new IjkPlayer()
              // 通过replace, 替换对应的方法。
              util.Aspect.replace(PlayManager, "init", false, providePlayer("init",()=> player))
              util.Aspect.replace(PlayManager, "start", false, providePlayer("start",()=> player))
              util.Aspect.replace(PlayManager, "stop", false, providePlayer("stop",() => player))
              util.Aspect.replace(PlayManager, "release", false, providePlayer("release",()=> player))
              let playManager = new PlayManager()
              playManager.init()
              // 替换成MediaPlayer
              player = new MediaPlayer()
              playManager.start()
            }
            

            四、AOP注意事项

            1.插桩的目标类通常需要导入进来,对于没有导出的场景,如果有实例,可以通过实例的constructor属性获取目标类。(这里告诉我们导入的类是一个类对象)

             // 类实例对象的constructor ,指向类对象。 
             util.Aspect.addBefore(this.context.constructor, 'startAbility', false,
                  (instance: Object, wantParam: Want) => {
                    console.info('UIAbilityContext startAbility: want.bundleName is ' + wantParam.bundleName);
                  });
            

            2.需要明确插桩的影响范围(可以根据JS原型链去理解)。

            3. addBefore 注意事项:

            util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 该函数的参数 第一个是一个对象,后续参数 则需要参考 源于函数声明
              ....
            });
            // 如果想要调用原有的函数,可以使用一个变量进行传递:
            let oringalFoo = new Test().foo;
            util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 该函数的参数 第一个是一个对象,后续参数 则需要参考 原函数声明
              // 方式一:如果原方法没有使用this,则可以直接调用原方法
              oringalFoo();
              // 方式二:如果原方法中使用了this,应该使用bind绑定instance,但是会有编译warning
              oringalFoo.bind(instance);
            });
            

            4.addAfter 注意事项:

            util.Aspect.addAfter(Test, 'foo', false, (instance: Test, ret: string) => { // 该函数的参数 第一个是一个对象,第二个参数是 原函数的返回值
              console.log('execute foo');
              return ret;  // 一定要将原方法的返回值 传递出去
            });
            

            5.struct 不能插桩和替换; 方法的属性为只读时,不可以插桩和替换; 构造函数也不能被插桩和替换;

            五、参考链接

            鸿蒙官网-应用切面编程设计

            es6的class&继承,揭开静态属性的原理和calss的本质

            鸿蒙架构之AOP

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]