AspectD: An Effective AOP Solution for Flutter — Open Sourced by Alibaba Xianyu Tech Team

Background

With the rapid development of the Flutter framework, more and more businesses begin to use Flutter to refactor or build new products. However, in practice, we have found that, on the one hand, Flutter has a high development efficiency, excellent performance, and good cross-platform performance. On the other hand, Flutter also faces problems, such as missing or imperfect plug-ins, basic capabilities and the underlying framework.

AspectD: Dart-Oriented AOP Framework

Whether the AOP capability is supported at runtime or compile-time, depends on the characteristics of the language itself. For example, in iOS, Objective C itself provides powerful runtime and dynamic features, making runtime AOP easy to use. In Android, Java can not only implement compile-time static proxies (such as AspectJ) based on bytecode modification, but also implement runtime dynamic proxies (such as Spring AOP) based on runtime enhancements.

Design Details

Typical AOP Scenarios

The following AspectD code illustrates a typical AOP application scenario:

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

Developer-Oriented API design

Design of PointCut

@Call("package:app/calculator.dart","Calculator","-getCurTime")
@pragma('vm:entry-point')
class PointCut {
final Map<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;
}
}

Design of Advice

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

Design of Aspect

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

Compilation of AOP code

Contain the Main Entry in the Original Project

As we can see from the above, import 'package:example/main.dart' as app; is introduced in aop.dart, which allows all code for the entire example project to be included when compiling aop.dart.

Compilation in Debug Mode

The introduction of import 'aop_impl.dart'; into aop.dart enables the content in aop_impl.dart to be compiled in Debug mode, even if it is not explicitly dependent by aop.dart

Compilation in Release Mode

In AOT compilation (in Release mode), the Tree-Shaking logic makes the content in aop_impl.dart not be compiled into Dill when they are not called by the Main entry in aop. The impact can be avoided by adding @pragma("vm:entry-point").

Dill Operation

The Dill file, also known as Dart Intermediate Language, is a concept in Dart language compilation. Either Script Snapshot or AOT compilation requires Dill as the intermediate.

Structure of Dill

We can use dump_kernel.dart provided by the VM package in the Dart SDK to print the internal structure of 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

Transformation of Dill

Dart provides a Kernel-to-Kernel Transform method, which can transform Dill through recursive AST traversal of the Dill file

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

Syntax Supported by AspectD

Unlike the BeforeAroundAfter advances provided in AspectJ, only one unified abstraction is available in AspectD, which is, Around.

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

Only Call and Execute are supported, which is obviously not enough for Flutter (Dart). On the one hand, Flutter does not allow reflection. To say the least, even if Flutter enables reflection, it is still not enough and cannot meet the needs.

@override
Widget build(BuildContext context) {
final Map<TapGestureRecognizer> 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;
},
);
}
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');
}
}

Build Process Support

Although we can compile aop.dart to compile both the original engineering code and the AspectD code into the dill file, and then implement the Dill hierarchy transformation through Transform to implement AOP, the standard Flutter build (flutter tools) does not support this process, so minor changes to the build process are still required.

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...

Practice and Consideration

Based on AspectD, we have successfully removed all invasive code for the Flutter framework in practice, and implemented the same features as when the intrusive code was not removed, supporting the recording and playback of hundreds of scripts and the stable and reliable operation of automatic regression.

Conclusion

As a new Flutter-oriented AOP framework developed by the XianYu technical team, AspectD supports mainstream AOP scenarios, and is open-source on Github. AspectD for Flutter

Original Source

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alibaba Cloud

Alibaba Cloud

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com