免费 支付宝购物网站模版,一同看网页打不开,网站开发团队如何接活,电子商务就业岗位问题背景
随着Flutter这一框架的快速发展#xff0c;有越来越多的业务开始使用Flutter来重构或新建其产品。但在我们的实践过程中发现#xff0c;一方面Flutter开发效率高#xff0c;性能优异#xff0c;跨平台表现好#xff0c;另一方面Flutter也面临着插件#xff0c;…问题背景
随着Flutter这一框架的快速发展有越来越多的业务开始使用Flutter来重构或新建其产品。但在我们的实践过程中发现一方面Flutter开发效率高性能优异跨平台表现好另一方面Flutter也面临着插件基础能力底层框架缺失或者不完善等问题。
举个栗子我们在实现一个自动化录制回放的过程中发现需要去修改Flutter框架(Dart层面)的代码才能够满足要求这就会有了对框架的侵入性。要解决这种侵入性的问题更好地减少迭代过程中的维护成本我们考虑的首要方案即面向切面编程。
那么如何解决AOP for Flutter这个问题呢本文将重点介绍一个闲鱼技术团队开发的针对Dart的AOP编程框架AspectD。
AspectD:面向Dart的AOP框架
AOP能力究竟是运行时还是编译时支持依赖于语言本身的特点。举例来说在iOS中Objective C本身提供了强大的运行时和动态性使得运行期AOP简单易用。在Android下Java语言的特点不仅可以实现类似AspectJ这样的基于字节码修改的编译期静态代理也可以实现Spring AOP这样的基于运行时增强的运行期动态代理。 那么Dart呢一来Dart的反射支持很弱只支持了检查(Introspection)不支持修改(Modification)其次Flutter为了包大小健壮性等的原因禁止了反射。
因此我们设计实现了基于编译期修改的AOP方案AspectD。
设计详图 典型的AOP场景
下列AspectD代码说明了一个典型的AOP使用场景:
aop.dartimport package:example/main.dart as app;
import aop_impl.dart;void main() app.main();
aop_impl.dartimport 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 Mapdynamic, dynamic sourceInfos;final Object target;final String function;final String stubId;final Listdynamic positionalParams;final Mapdynamic, 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)
FutureString 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注解即可。
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的内部结构。
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部分逻辑如下所示: overrideMethodInvocation 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)FutureString getCurTime(PointCut pointcut) async{print(Aspectd:KWLM02);print(${pointcut.sourceInfos.toString()});FutureString 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)FutureString getCurTime(PointCut pointcut) async{print(Aspectd:KWLM12);print(${pointcut.sourceInfos.toString()});FutureString 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库中包含了一下这段手势相关代码:
overrideWidget build(BuildContext context) {final MapType, GestureRecognizerFactory gestures Type, GestureRecognizerFactory{};if (onTapDown ! null || onTapUp ! null || onTap ! null || onTapCancel ! null) {gestures[TapGestureRecognizer] GestureRecognizerFactoryWithHandlersTapGestureRecognizer(() 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 IgnoreObject context; //Aspectd Ignoreprint(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的支持。
kylewongKyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch
kylewongKyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp
kylewongKyleWongdeMacBook-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开源欢迎使用。
原文链接 本文为云栖社区原创内容未经允许不得转载。