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.或者直接聯繫做者
本文爲雲棲社區原創內容,未經容許不得轉載。