零、主要内容
- AOP 简介
- ArkTs AOP 实现原理 - JS 原型链- AOP实现原理
- AOP的应用场景 - 统计类: 方法调用次数统计、方法时长统计- 防御式编程:参数校验- 代理模式实现
- AOP的注意事项
一、AOP简介
对于Android、Java Web 开发者来说, AOP编程思想并不陌生。 AOP的使用核心在于要找到 Aspect(切面),然后再根据自己的需要,对某个“业务操作进”行 前置或者后置的处理,甚至可以替换“该业务操作”。 AOP的操作粒度就是方法级别, 一个方法包括 接收数据、处理数据和返回数据这么三个部分:
AOP 在这三个阶段都可以添加自己的逻辑处理。 Java中常见的AOP框架有很多:AspectJ、SpringAOP、Javassist、Guice、Byte Buddy等。ArkTs在4.0版本中也支持了AOP,那么ArkTs是如何实现AOP的呢?
二、ArkTs AOP 实现原理
接下来,我们首先要了解一下JS对象的在继承体系中的引用关系,这样才能够精准的选择合适的方法来进行切面编程。 然后我们在了解一下AOP是如何实现的。
2.1 JS 原型链
如上图所示:
水平维度:类通过prototype 引用着其原型对象, 通过constructor引种着其构造函数; 该类的构造函数中,关联着该类的静态方法;
竖直维度:类的原型对象通过__proto__指向父类原型对象;类的构造函数通过__proto__指向父类的构造函数;类的实例对象通过__proto__指向该类的原型对象;
那么对于实例对象a和对象b来说,其实例方法的定位如下图红色路径所示;对于类A和类B类说,其静态方法的的定位流程如下图蓝色路径所示:
通过上图,我们可以得出如下结论:
类的原型对象承载着该类对象的实例实例方法(非静态方法),并且通过__proto__ 指向父类的原型对象,通过constructor指向类(也就是类的构造函数,需要额外指出的是 类的静态方法存储在构造函数中)。 类(类的构造函数)通过__proto__指向父类(父类的构造构造函数)。
2.2 AOP实现原理
AOP的实现依赖于 插桩和替换来实现的, 其本质上将回调参数和原方法组合成一个新的函数,再用新的函数替换原方法,具体如下图所示:
“计算机科学中的所有问题都可以通过增加一个额外的间接层来解决”
2.2.1 AddBefore 原理的伪代码
// addBefore 的伪代码实现staticaddBefore(targetClass, methodName , isStatic ,before:Function):void{// 根据是否静态方法,获取要插装的对象(是“类” ,还是“类的原型对象”)let target = isStatic ? targetClass : targetClass.prototype;// 根据方法名,获取原有的方法let origin = target[methodName];/**
* 定义新的方法(包装一层),实现优先执行before的逻辑,然后执行原有方法origin,
* 最后将返回结果给 外层调用者。
*/letnewFuncs=function(...args){// 先执行before方法,再执行当前方法before(this,...args);returnorigin.bind(this)(...args);}// 使用新函数生效
target[methodName]= newFuncs;
}
2.2.2 AddAfter 原理的伪代码
// addAfter 的伪代码实现staticaddAfter(targetClass, methodName , isStatic ,after:Function):void{let target = isStatic ? targetClass : target.protoType;let original = target[methodName];letnewFuncs=function(...args){let ret =origin.bind(this)(...args);returnafter(this,r,...args);}}
2.2.3 Repalce 原理的伪代码
staticreplace(targetClass, methodName , isStatic , instead):void{let target = isStatic ? targetClass : target.protoType;letnewFuncs=function(...args){returninstead(this,...args);}
target[methodName]= newFuncs;}
三、AOP的应用场景
- 统计类: 方法调用次数统计、方法时长统计
- 防御式编程:参数校验、返回值校验
- 继承体系中的精确Hook
- 代理模式和IOC
3.1 统计类
3.1.1 方法调用次数统计
exportclassTest{hello(){console.log('hello world')}}
我们通过Aspect.addBefore实现对Test类 hello方法调用次数的统计。
functionmain(){let countHello =0;
util.Aspect.addBefore(Test,'hello',false,()=>{
countHello++;});let h =newTest();console.log(`countHello : ${countHello}`)
h.hello();console.log(`countHello : ${countHello}`)}
3.1.2 方法时长统计
functionaddTimePrinter(target:Object, methodName:string, isStatic:boolean){let t1 =0;let t2 =0;
util.Aspect.addBefore(targetClass, methodName, isStatic,()=>{
t1 =newDate().getTime();});
util.Aspect.addAfter(targetClass, methodName, isStatic,()=>{
t2 =newDate().getTime();console.log("t2---t1 = "+(t2 - t1).toString());});}
测试addTimePrinter的功能:
exportclassView{onDraw(){// ... }staticcinit(){// ... }}functionmain(){// 测试静态方法的时长统计addTimePrinter(Test,'cinit',true);
View.cinit();// 测试实例方法的时长统计addTimePrinter(Test,'onDraw',true);newView().cinit();}
3.2 防御式编程
- 校验参数
- 纠正返回值
3.2.1 校验参数
exportclassP004_View{
children:P004_View[];constructor(children:Array<P004_View>){this.children = children
}getViewByIndex(index:number):P004_View {returnthis.children[index];}}
上述View类的实例方法 getViewByIndex 的入参是一个index, 为了避免索引越界情况,我们可以通过Aspect类addBefore,增加一层”参数校验“的逻辑。
util.Aspect.addBefore(P004_View,"getViewByIndex",false,(view:P004_View, index:number)=>{if(view.children){throwError('view.children is undefined !')}if(index <=0){throwError('index can not be negative !')}if((view.children as P004_View[]).length <= index){throwError('index is too big !')}})
3.2.2 纠正返回值
exportclassP004_Random{staticrandomSmallerThan50():number{return Math.floor(Math.random()*52);}}
randomSmallerThan50 方法的返回值期望是[0,50], 但是目前返回之返回是[0,51] , 我们可以使用Aspect类的addAfter方法,对返回值进行修正
exportfunctiontestRandom(){
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 子类实例方法替换
exportclassAirCraft{fly(){console.log('fight....')}}exportclassUSA_AirCraftextendsAirCraft{}exportclassCN_AirCraftextendsAirCraft{}
我们也可以通过Aspect类实现对子类的某个方法的 插桩或者替换。 下面是替换USA_AirCraft类的fly方法的代码:
exportfunctiontestAirCraft(){let cn =newCN_AirCraft()let usa =newUSA_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对象的切换 。
上图中UML中的类,对应代码如下:
interfaceIPlayer{init():voidstart():voidstop():voidrelease():void}exportclassPlayManager{
player?: IPlayer
init():void{}start():void{}stop():void{}release():void{}}exportclassIjkPlayerimplementsIPlayer{init():void{console.log('IjkPlayer init ...')}start():void{console.log('IjkPlayer start ...')}stop():void{console.log('IjkPlayer stop ...')}release():void{console.log('IjkPlayer release ...')}}exportclassMediaPlayerimplementsIPlayer{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的对应的方法,并返回。
*/exportfunctionprovidePlayer(methodName:string,playerFetcher:()=>IPlayer){return(manager: PlayManager)=>{if(methodName ==='init'){returnplayerFetcher().start()}elseif(methodName ==='init'){returnplayerFetcher().start()}elseif(methodName ==='start'){returnplayerFetcher().start()}elseif(methodName ==='stop'){returnplayerFetcher().start()}elseif(methodName ==='release'){returnplayerFetcher().release()}}}exportfunctiontestPlayer(){let player:IPlayer =newIjkPlayer()// 通过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 =newPlayManager()
playManager.init()// 替换成MediaPlayer
player =newMediaPlayer()
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 =newTest().foo;
util.Aspect.addBefore(Test,'foo',false,(instance: Test)=>{// 该函数的参数 第一个是一个对象,后续参数 则需要参考 原函数声明// 方式一:如果原方法没有使用this,则可以直接调用原方法oringalFoo();// 方式二:如果原方法中使用了this,应该使用bind绑定instance,但是会有编译warningoringalFoo.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的本质
版权归原作者 Sharknade 所有, 如有侵权,请联系我们删除。