阿里妹导读:随着 Flutter 这一框架的快速发展,有愈来愈多的业务开始使用 Flutter 来重构或新建其产品。但在咱们的实践过程当中发现,一方面 Flutter 开发效率高,性能优异,跨平台表现好,另外一方面 Flutter 也面临着插件,基础能力,底层框架缺失或者不完善等问题。今天,闲鱼团队的正物带咱们解决一个问题:如何解决 AOP for Flutter?node
咱们在实现一个自动化录制回放的过程当中发现,须要去修改 Flutter 框架( Dart 层面)的代码才可以知足要求,这就会有了对框架的侵入性。要解决这种侵入性的问题,更好地减小迭代过程当中的维护成本,咱们考虑的首要方案即面向切面编程。git
那么如何解决 AOP for Flutter 这个问题呢?本文将重点介绍一个闲鱼技术团队开发的针对 Dart 的 AOP 编程框架 AspectD。编程
AOP 能力到底是运行时仍是编译时支持依赖于语言自己的特色。举例来讲在 iOS 中,Objective C 自己提供了强大的运行时和动态性使得运行期 AOP 简单易用。在 Android下,Java 语言的特色不只能够实现相似 AspectJ 这样的基于字节码修改的编译期静态代理,也能够实现 Spring AOP 这样的基于运行时加强的运行期动态代理。那么 Dart 呢?一来 Dart 的反射支持很弱,只支持了检查( Introspection ),不支持修改( Modification );其次 Flutter 为了包大小,健壮性等的缘由禁止了反射。数据结构
所以,咱们设计实现了基于编译期修改的 AOP 方案 AspectD。app
一、设计详图框架
二、典型的 AOP 场景async
下列 AspectD 代码说明了一个典型的 AOP 使用场景:ide
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!'); } }
三、面向开发者的API设计函数
PointCut 的设计性能
@Call("package:app/calculator.dart","Calculator","-getCurTime")
PointCut 须要完备表征以什么样的方式( Call/Execute 等),向哪一个 Library,哪一个类(Library Method 的时候此项为空),哪一个方法来添加 AOP 逻辑。PointCut 的数据结构:
@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 的设计
@pragma("vm:entry-point") class ExecuteDemo { @pragma("vm:entry-point") ExecuteDemo(); ... }
Aspect 的注解可使得 ExecuteDemo 这样的 AOP 实现类被方便地识别和提取,也能够起到开关的做用,即若是但愿禁掉此段 AOP 逻辑,移除 @Aspect 注解便可。
四、AOP 代码的编译
包含原始工程的 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操做
dill 文件,又称为 Dart Intermediate Language,是 Dart 语言编译中的一个概念,不管是 Script Snapshot 仍是 AOT 编译,都须要 dill 做为中间产物。
Dill 的结构
咱们能够经过 dart sdk 中的 vm package 提供的 dump_kernel.dart 打印出 dill 的内部结构
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 过程。
六、AspectD 支持的语法
不一样于 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 库中包含了一下这段手势相关代码:
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 后,只须要简单的几句便可解决:
@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 构建(即fluttertools) 并不支持这个过程,因此仍是须要对构建过程作细微修改。在 AspectJ 中,这一过程是由非标准 Java 编译器的 Ajc 来实现的。在 AspectD 中,经过对fluttertools 打上应用 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<--> 模型转换等,都提供了一种新的视角与可能。
原文连接 本文为云栖社区原创内容,未经容许不得转载。