https://github.com/alibaba-flutter/aspectdnode
随着Flutter这一框架的快速发展,有愈来愈多的业务开始使用Flutter来重构或新建其产品。但在咱们的实践过程当中发现,一方面Flutter开发效率高,性能优异,跨平台表现好,另外一方面Flutter也面临着插件,基础能力,底层框架缺失或者不完善等问题。git
举个栗子,咱们在实现一个自动化录制回放的过程当中发现,须要去修改Flutter框架(Dart层面)的代码才可以知足要求,这就会有了对框架的侵入性。要解决这种侵入性的问题,更好地减小迭代过程当中的维护成本,咱们考虑的首要方案即面向切面编程。github
那么如何解决AOP for Flutter这个问题呢?本文将重点介绍一个闲鱼技术团队开发的针对Dart的AOP编程框架AspectD。编程
AOP能力到底是运行时仍是编译时支持依赖于语言自己的特色。举例来讲在iOS中,Objective C自己提供了强大的运行时和动态性使得运行期AOP简单易用。在Android下,Java语言的特色不只能够实现相似AspectJ这样的基于字节码修改的编译期静态代理,也能够实现Spring AOP这样的基于运行时加强的运行期动态代理。
那么Dart呢?一来Dart的反射支持很弱,只支持了检查(Introspection),不支持修改(Modification);其次Flutter为了包大小,健壮性等的缘由禁止了反射。数据结构
所以,咱们设计实现了基于编译期修改的AOP方案AspectD。app
下列AspectD代码说明了一个典型的AOP使用场景:框架
aop.dart import 'package:example/main.dart' as app; import 'aop_impl.dart'; void main()=> app.main();
aop_impl.dart import 'package:aspectd/aspectd.dart'; @Aspect() @pragma("vm:entry-point") class ExecuteDemo { @pragma("vm:entry-point") ExecuteDemo(); @Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter") @pragma("vm:entry-point") void _incrementCounter(PointCut pointcut) { pointcut.proceed(); print('KWLM called!'); } }
PointCut的设计async
@Call("package:app/calculator.dart","Calculator","-getCurTime")
PointCut须要完备表征以怎么样的方式(Call/Execute等),向哪一个Library,哪一个类(Library Method的时候此项为空),哪一个方法来添加AOP逻辑。
PointCut的数据结构:ide
@pragma('vm:entry-point') class PointCut { final Map<dynamic, dynamic> sourceInfos; final Object target; final String function; final String stubId; final List<dynamic> positionalParams; final Map<dynamic, dynamic> namedParams; @pragma('vm:entry-point') PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams); @pragma('vm:entry-point') Object proceed(){ return null; } }
其中包含了源代码信息(如库名,文件名,行号等),方法调用对象,函数名,参数信息等。
请注意这里的@pragma('vm:entry-point')
注解,其核心逻辑在于Tree-Shaking。在AOT(ahead of time)编译下,若是不能被应用主入口(main)最终可能调到,那么将被视为无用代码而丢弃。AOP代码由于其注入逻辑的无侵入性,显然是不会被main调到的,所以须要此注解告诉编译器不要丢弃这段逻辑。
此处的proceed方法,相似AspectJ中的ProceedingJoinPoint.proceed()方法,调用pointcut.proceed()方法便可实现对原始逻辑的调用。原始定义中的proceed方法体只是个空壳,其内容将会被在运行时动态生成。函数
Advice的设计
@pragma("vm:entry-point") Future<String> getCurTime(PointCut pointcut) async{ ... return result; }
此处的@pragma("vm:entry-point")
效果同a中所述,pointCut对象做为参数传入AOP方法,使开发者能够得到源代码调用信息的相关信息,实现自身逻辑或者是经过pointcut.proceed()调用原始逻辑。
Aspect的设计
@Aspect() @pragma("vm:entry-point") class ExecuteDemo { @pragma("vm:entry-point") ExecuteDemo(); ... }
Aspect的注解可使得ExecuteDemo这样的AOP实现类被方便地识别和提取,也能够起到开关的做用,即若是但愿禁掉此段AOP逻辑,移除@Aspect注解便可。
包含原始工程中的main入口
从上文能够看到,aop.dart引入import 'package:example/main.dart' as app;
,这使得编译aop.dart时可包含整个example工程的全部代码。
Debug模式下的编译
在aop.dart中引入import 'aop_impl.dart';
这使得aop_impl.dart中内容即使不被aop.dart显式依赖,也能够在Debug模式下被编译进去。
Release模式下的编译
在AOT编译(Release模式下),Tree-Shaking逻辑使得当aop_impl.dart中的内容没有被aop中main调用时,其内容将不会编译到dill中。经过添加@pragma("vm:entry-point")
能够避免其影响。
当咱们用AspectD写出AOP代码,透过编译aop.dart生成中间产物,使得dill中既包含了原始项目代码,也包含了AOP代码后,则须要考虑如何对其修改。在AspectJ中,修改是经过对Class文件进行操做实现的,在AspectD中,咱们则对dill文件进行操做。
dill文件,又称为Dart Intermediate Language,是Dart语言编译中的一个概念,不管是Script Snapshot仍是AOT编译,都须要dill做为中间产物。
Dill的结构
咱们能够经过dart sdk中的vm package提供的dump_kernel.dart打印出dill的内部结构。
dart bin/dump_kernel.dart /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill.txt
Dill变换
dart提供了一种Kernel to Kernel Transform的方式,能够经过对dill文件的递归式AST遍历,实现对dill的变换。
基于开发者编写的AspectD注解,AspectD的变换部分能够提取出是哪些库/类/方法须要添加怎样的AOP代码,再在AST递归的过程当中经过对目标类的操做,实现Call/Execute这样的功能。
一个典型的Transform部分逻辑以下所示:
@override MethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) { methodInvocation.transformChildren(this); Node node = methodInvocation.interfaceTargetReference?.node; String uniqueKeyForMethod = null; if (node is Procedure) { Procedure procedure = node; Class cls = procedure.parent as Class; String procedureImportUri = cls.reference.canonicalName.parent.name; uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod( procedureImportUri, cls.name, methodInvocation.name.name, false, null); } else if(node == null) { String importUri = methodInvocation?.interfaceTargetReference?.canonicalName?.reference?.canonicalName?.nonRootTop?.name; String clsName = methodInvocation?.interfaceTargetReference?.canonicalName?.parent?.parent?.name; String methodName = methodInvocation?.interfaceTargetReference?.canonicalName?.name; uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod( importUri, clsName, methodName, false, null); } if(uniqueKeyForMethod != null) { AspectdItemInfo aspectdItemInfo = _aspectdInfoMap[uniqueKeyForMethod]; if (aspectdItemInfo?.mode == AspectdMode.Call && !_transformedInvocationSet.contains(methodInvocation) && AspectdUtils.checkIfSkipAOP(aspectdItemInfo, _curLibrary) == false) { return transformInstanceMethodInvocation( methodInvocation, aspectdItemInfo); } } return methodInvocation; }
经过对于dill中AST对象的遍历(此处的visitMethodInvocation函数),结合开发者书写的AspectD注解(此处的_aspectdInfoMap_和aspectdItemInfo),能够对原始的AST对象(此处methodInvocation)进行变换,从而改变原始的代码逻辑,即Transform过程。
不一样于AspectJ中提供的BeforeAroundAfter三种预发,在AspectD中,只有一种统一的抽象即Around。
从是否修改原始方法内部而言,有Call和Execute两种,前者的PointCut是调用点,后者的PointCut则是执行点。
Call
import 'package:aspectd/aspectd.dart'; @Aspect() @pragma("vm:entry-point") class CallDemo{ @Call("package:app/calculator.dart","Calculator","-getCurTime") @pragma("vm:entry-point") Future<String> getCurTime(PointCut pointcut) async{ print('Aspectd:KWLM02'); print('${pointcut.sourceInfos.toString()}'); Future<String> result = pointcut.proceed(); String test = await result; print('Aspectd:KWLM03'); print('${test}'); return result; } }
Execute
import 'package:aspectd/aspectd.dart'; @Aspect() @pragma("vm:entry-point") class ExecuteDemo{ @Execute("package:app/calculator.dart","Calculator","-getCurTime") @pragma("vm:entry-point") Future<String> getCurTime(PointCut pointcut) async{ print('Aspectd:KWLM12'); print('${pointcut.sourceInfos.toString()}'); Future<String> result = pointcut.proceed(); String test = await result; print('Aspectd:KWLM13'); print('${test}'); return result; }
Inject
仅支持Call和Execute,对于Flutter(Dart)而言显然非常单薄。一方面Flutter禁止了反射,退一步讲,即使Flutter开启了反射支持,依然很弱,并不能知足需求。
举个典型的场景,若是须要注入的dart代码里,x.dart文件的类y定义了一个私有方法m或者成员变量p,那么在aop_impl.dart中是没有办法对其访问的,更不用说多个连续的私有变量属性得到。另外一方面,仅仅对方法总体进行操做多是不够的,咱们可能须要在方法的中间插入处理逻辑。
为了解决这一问题,AspectD设计了一种语法Inject,参见下面的例子:
flutter库中包含了一下这段手势相关代码:
@override Widget build(BuildContext context) { final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{}; if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) { gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance ..onTapDown = onTapDown ..onTapUp = onTapUp ..onTap = onTap ..onTapCancel = onTapCancel; }, ); }
若是咱们想要在onTapCancel以后添加一段对于instance和context的处理逻辑,Call和Execute是不可行的,而使用Inject后,只须要简单的几句便可解决:
import 'package:aspectd/aspectd.dart'; @Aspect() @pragma("vm:entry-point") class InjectDemo{ @Inject("package:flutter/src/widgets/gesture_detector.dart","GestureDetector","-build", lineNum:452) @pragma("vm:entry-point") static void onTapBuild() { Object instance; //Aspectd Ignore Object context; //Aspectd Ignore print(instance); print(context); print('Aspectd:KWLM25'); } }
经过上述的处理逻辑,通过编译构建后的dill中的GestureDetector.build方法以下所示:
此外,Inject的输入参数相对于Call/Execute而言,多了一个lineNum的命名参数,可用于指定插入逻辑的具体行号。
虽然咱们能够经过编译aop.dart达到同时编译原始工程代码和AspectD代码到dill文件,再经过Transform实现dill层次的变换实现AOP,但标准的flutter构建(即flutter_tools)并不支持这个过程,因此仍是须要对构建过程作细微修改。
在AspectJ中,这一过程是由非标准Java编译器的Ajc来实现的。在AspectD中,经过对flutter_tools打上应用Patch,能够实现对于AspectD的支持。
kylewong@KyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch kylewong@KyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp kylewong@KyleWongdeMacBook-Pro fluttermaster % flutter doctor -v Building flutter tool...
基于AspectD,咱们在实践中成功地移除了全部对于Flutter框架的侵入性代码,实现了同有侵入性代码一样的功能,支撑上百个脚本的录制回放与自动化回归稳定可靠运行。
从AspectD的角度看,Call/Execute能够帮助咱们便捷实现诸如性能埋点(关键方法的调用时长),日志加强(获取某个方法具体是在什么地方被调用到的详细信息),Doom录制回放(如随机数序列的生成记录与回放)等功能。Inject语法则更为强大,能够经过相似源代码诸如的方式,实现逻辑的自由注入,能够支持诸如App录制与自动化回归(如用户触摸事件的录制与回放)等复杂场景。
进一步来讲,AspectD的原理基于Dill变换,有了Dill操做这一利器,开发者能够自由地对Dart编译产物进行操做,并且这种变换面向的是近乎源代码级别的AST对象,不只强大并且可靠。不管是作一些逻辑替换,仍是是Json<-->模型转换等,都提供了一种新的视角与可能。
AspectD做为闲鱼技术团队新开发的面向Flutter的AOP框架,已经能够支持主流的AOP场景并在Github开源,欢迎使用。Aspectd for Flutter
若是你在使用过程当中,有任何问题或者建议,欢迎提issue或者PR.或者直接联系做者
本文为云栖社区原创内容,未经容许不得转载。