日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

重磅开源|AOP for Flutter开发利器——AspectD

發(fā)布時間:2024/8/23 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 重磅开源|AOP for Flutter开发利器——AspectD 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

問題背景

隨著Flutter這一框架的快速發(fā)展,有越來越多的業(yè)務(wù)開始使用Flutter來重構(gòu)或新建其產(chǎn)品。但在我們的實踐過程中發(fā)現(xiàn),一方面Flutter開發(fā)效率高,性能優(yōu)異,跨平臺表現(xiàn)好,另一方面Flutter也面臨著插件,基礎(chǔ)能力,底層框架缺失或者不完善等問題。

舉個栗子,我們在實現(xiàn)一個自動化錄制回放的過程中發(fā)現(xiàn),需要去修改Flutter框架(Dart層面)的代碼才能夠滿足要求,這就會有了對框架的侵入性。要解決這種侵入性的問題,更好地減少迭代過程中的維護成本,我們考慮的首要方案即面向切面編程。

那么如何解決AOP for Flutter這個問題呢?本文將重點介紹一個閑魚技術(shù)團隊開發(fā)的針對Dart的AOP編程框架AspectD。

AspectD:面向Dart的AOP框架

AOP能力究竟是運行時還是編譯時支持依賴于語言本身的特點。舉例來說在iOS中,Objective C本身提供了強大的運行時和動態(tài)性使得運行期AOP簡單易用。在Android下,Java語言的特點不僅可以實現(xiàn)類似AspectJ這樣的基于字節(jié)碼修改的編譯期靜態(tài)代理,也可以實現(xiàn)Spring AOP這樣的基于運行時增強的運行期動態(tài)代理。
那么Dart呢?一來Dart的反射支持很弱,只支持了檢查(Introspection),不支持修改(Modification);其次Flutter為了包大小,健壯性等的原因禁止了反射。

因此,我們設(shè)計實現(xiàn)了基于編譯期修改的AOP方案AspectD。

設(shè)計詳圖

典型的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!');} }

面向開發(fā)者的API設(shè)計

PointCut的設(shè)計

@Call("package:app/calculator.dart","Calculator","-getCurTime")

PointCut需要完備表征以怎么樣的方式(Call/Execute等),向哪個Library,哪個類(Library Method的時候此項為空),哪個方法來添加AOP邏輯。
PointCut的數(shù)據(jù)結(jié)構(gòu):

@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;} }

其中包含了源代碼信息(如庫名,文件名,行號等),方法調(diào)用對象,函數(shù)名,參數(shù)信息等。
請注意這里的@pragma('vm:entry-point')注解,其核心邏輯在于Tree-Shaking。在AOT(ahead of time)編譯下,如果不能被應(yīng)用主入口(main)最終可能調(diào)到,那么將被視為無用代碼而丟棄。AOP代碼因為其注入邏輯的無侵入性,顯然是不會被main調(diào)到的,因此需要此注解告訴編譯器不要丟棄這段邏輯。
此處的proceed方法,類似AspectJ中的ProceedingJoinPoint.proceed()方法,調(diào)用pointcut.proceed()方法即可實現(xiàn)對原始邏輯的調(diào)用。原始定義中的proceed方法體只是個空殼,其內(nèi)容將會被在運行時動態(tài)生成。

Advice的設(shè)計

@pragma("vm:entry-point") Future<String> getCurTime(PointCut pointcut) async{...return result; }

此處的@pragma("vm:entry-point")效果同a中所述,pointCut對象作為參數(shù)傳入AOP方法,使開發(fā)者可以獲得源代碼調(diào)用信息的相關(guān)信息,實現(xiàn)自身邏輯或者是通過pointcut.proceed()調(diào)用原始邏輯。

Aspect的設(shè)計

@Aspect() @pragma("vm:entry-point") class ExecuteDemo {@pragma("vm:entry-point")ExecuteDemo();...}

Aspect的注解可以使得ExecuteDemo這樣的AOP實現(xiàn)類被方便地識別和提取,也可以起到開關(guān)的作用,即如果希望禁掉此段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中內(nèi)容即便不被aop.dart顯式依賴,也可以在Debug模式下被編譯進去。

Release模式下的編譯

在AOT編譯(Release模式下),Tree-Shaking邏輯使得當(dāng)aop_impl.dart中的內(nèi)容沒有被aop中main調(diào)用時,其內(nèi)容將不會編譯到dill中。通過添加@pragma("vm:entry-point")可以避免其影響。

當(dāng)我們用AspectD寫出AOP代碼,透過編譯aop.dart生成中間產(chǎn)物,使得dill中既包含了原始項目代碼,也包含了AOP代碼后,則需要考慮如何對其修改。在AspectJ中,修改是通過對Class文件進行操作實現(xiàn)的,在AspectD中,我們則對dill文件進行操作。

Dill操作

dill文件,又稱為Dart Intermediate Language,是Dart語言編譯中的一個概念,無論是Script Snapshot還是AOT編譯,都需要dill作為中間產(chǎn)物。

Dill的結(jié)構(gòu)

我們可以通過dart sdk中的vm package提供的dump_kernel.dart打印出dill的內(nèi)部結(jié)構(gòu)。

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遍歷,實現(xiàn)對dill的變換。

基于開發(fā)者編寫的AspectD注解,AspectD的變換部分可以提取出是哪些庫/類/方法需要添加怎樣的AOP代碼,再在AST遞歸的過程中通過對目標(biāo)類的操作,實現(xiàn)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函數(shù)),結(jié)合開發(fā)者書寫的AspectD注解(此處的_aspectdInfoMap_和aspectdItemInfo),可以對原始的AST對象(此處methodInvocation)進行變換,從而改變原始的代碼邏輯,即Transform過程。

AspectD支持的語法

不同于AspectJ中提供的BeforeAroundAfter三種預(yù)發(fā),在AspectD中,只有一種統(tǒng)一的抽象即Around。
從是否修改原始方法內(nèi)部而言,有Call和Execute兩種,前者的PointCut是調(diào)用點,后者的PointCut則是執(zhí)行點。

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中是沒有辦法對其訪問的,更不用說多個連續(xù)的私有變量屬性獲得。另一方面,僅僅對方法整體進行操作可能是不夠的,我們可能需要在方法的中間插入處理邏輯。
為了解決這一問題,AspectD設(shè)計了一種語法Inject,參見下面的例子:
flutter庫中包含了一下這段手勢相關(guān)代碼:

@overrideWidget 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 IgnoreObject context; //Aspectd Ignoreprint(instance);print(context);print('Aspectd:KWLM25');} }

通過上述的處理邏輯,經(jīng)過編譯構(gòu)建后的dill中的GestureDetector.build方法如下所示:

此外,Inject的輸入?yún)?shù)相對于Call/Execute而言,多了一個lineNum的命名參數(shù),可用于指定插入邏輯的具體行號。

構(gòu)建流程支持

雖然我們可以通過編譯aop.dart達到同時編譯原始工程代碼和AspectD代碼到dill文件,再通過Transform實現(xiàn)dill層次的變換實現(xiàn)AOP,但標(biāo)準(zhǔn)的flutter構(gòu)建(即flutter_tools)并不支持這個過程,所以還是需要對構(gòu)建過程做細(xì)微修改。
在AspectJ中,這一過程是由非標(biāo)準(zhǔn)Java編譯器的Ajc來實現(xiàn)的。在AspectD中,通過對flutter_tools打上應(yīng)用Patch,可以實現(xiàn)對于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...

實戰(zhàn)與思考

基于AspectD,我們在實踐中成功地移除了所有對于Flutter框架的侵入性代碼,實現(xiàn)了同有侵入性代碼同樣的功能,支撐上百個腳本的錄制回放與自動化回歸穩(wěn)定可靠運行。

從AspectD的角度看,Call/Execute可以幫助我們便捷實現(xiàn)諸如性能埋點(關(guān)鍵方法的調(diào)用時長),日志增強(獲取某個方法具體是在什么地方被調(diào)用到的詳細(xì)信息),Doom錄制回放(如隨機數(shù)序列的生成記錄與回放)等功能。Inject語法則更為強大,可以通過類似源代碼諸如的方式,實現(xiàn)邏輯的自由注入,可以支持諸如App錄制與自動化回歸(如用戶觸摸事件的錄制與回放)等復(fù)雜場景。

進一步來說,AspectD的原理基于Dill變換,有了Dill操作這一利器,開發(fā)者可以自由地對Dart編譯產(chǎn)物進行操作,而且這種變換面向的是近乎源代碼級別的AST對象,不僅強大而且可靠。無論是做一些邏輯替換,還是是Json<-->模型轉(zhuǎn)換等,都提供了一種新的視角與可能。

寫在最后

AspectD作為閑魚技術(shù)團隊新開發(fā)的面向Flutter的AOP框架,已經(jīng)可以支持主流的AOP場景并在Github開源,歡迎使用。


原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。

總結(jié)

以上是生活随笔為你收集整理的重磅开源|AOP for Flutter开发利器——AspectD的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。