With the booming development of mobile smart devices, a mobile multi-terminal development framework has become a general trend. Download the Flutter Analysis and Practice: Evolution and Innovation of Xianyu Technologies eBook for step-by-step analyses based on real-life problems, as well as clear explanations of the important concepts of Flutter.
In practice, we have found that, on the one hand, Flutter features advantages, such as high development efficiency, excellent performance, and good cross-platform performance. On the other hand, Flutter also has problems, such as missing or imperfect plug-ins, basic capabilities, and the underlying framework.
For example, to implement automatic recording and playback, the Flutter framework (Dart level) code needs to be modified to meet the requirements. This leads to the risk of the framework becoming vulnerable to intrusion. To solve this problem and reduce the maintenance cost in the iteration process, the first solution we consider is AOP.
Then, how can we implement AOP for Flutter? This article focuses on AspectD, a Dart-oriented AOP framework developed by Xianyu.
Whether the AOP capability is supported at runtime or compile-time depends on the characteristics of the language. For example, on iOS, objective C provides powerful runtime and dynamic features, making AOP easy to use at runtime. On Android, Java can implement compile-time static proxies (such as AspectJ) based on bytecode modification, and runtime dynamic proxies (such as Spring AOP) based on runtime enhancements. What about Dart? Firstly, the reflection support of Dart is poor. Only introspection is supported, while modification is not supported. Secondly, Flutter disables reflection to reduce the packet size and improve robustness.
Therefore, we have designed and implemented an AOP solution based on the compile-time modification, AspectD, as shown in Figure 3-13.
Figure 3-13
The following AspectD code illustrates a typical AOP application scenario:
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!');
}
}
@Call("package:app/calculator.dart","Calculator","-getCurTime")
PointCut
needs to fully characterize how to add the AOP logic, for example in what way (Call/Execute)
, and to which library, which class (this item is empty in the case of Library Method), and which method. The data structure of PointCut
is:
@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;
}
}
It contains the source code information (such as the library name, file name, and row number), method call object, function, and parameter information. Note: The @pragma('vm:entry-point')
annotation here. Its core logic is Tree-Shaking. In AOT compilation, if the logic cannot be called by the main entry of the app, it will be discarded as useless code. The injection logic of the AOP code is non-invasive, so it will not be called by the main entry. Therefore, this annotation is required to instruct the compiler not to discard this logic. Here, the proceed method is similar to the ProceedingJoinPoint.proceed()
method in AspectJ, and the original logic can be called by calling the pointcut.proceed()
method. The proceed method body in the original definition is empty and its content will be dynamically generated at runtime.
@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
...
return result;
}
The pointCut
object is passed into the AOP method as a parameter so developers can obtain relevant information about the source code call to implement its logic or call the original logic through pointcut.proceed()
.
@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();
...
}
The Aspect annotation can enable the AOP implementation class, such as ExecuteDemo
, to be easily identified and extracted, and can also be used as a switch. If we want to disable the AOP logic, remove the @Aspect
annotation.
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 aop.dart
is compiled.
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
.
In the AOT compilation (in release mode), the Tree-Shaking logic effects that the content in aop_impl.dart
is not compiled into the Dart Intermediate Language (Dill) file when the content is not called by the main function in AOP. @pragma("vm:entry-point")
can be added to avoid impact.
When we use AspectD
to write the AOP code and generate intermediates by compiling aop.dart
to ensure that the Dill file contains both original project code and the AOP code, we need to consider how to modify it. In AspectJ, modifications are implemented through operations on the Class file. In AspectD
, modifications are implemented through operations on the Dill file.
The Dill file is a concept in Dart compilation. Both Script Snapshot and AOT compilation require the Dill file as the intermediate.
We can use dump_kernel.dart
provided by the VM package in the Dart SDK to print the internal structure of the Dill file.
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
...
library from "package:aspectd_impl/aspectd_impl.dart" as asp {
import "package:example/main.dart" as app;
import "package:aspectd_impl/aop_impl.dart";
static method main() → void
return main::main();
}
...
Dart provides a Kernel-to-Kernel Transform method to transform the Dill file through recursive AST traversal of the Dill file.
Based on the AspectD
annotation written by developers, the libraries, classes, and methods, the specific AOP code to be added can be extracted from the transformation part of AspectD
. Then, features, such as Call/Execute
, can be implemented through operations on target classes during AST recursion.
The following is part of typical transform logic:
@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;
}
After traversing the AST objects in the Dill file (the visitMethodInvocation
function), we can transform the original AST objects (methodInvocation
) to change the original code logic, namely the transform process, according to the AspectD
annotation (aspectdInfoMap
and aspectdItemInfo
) written by developers.
Unlike the Before, Around, and After syntax provided in AspectJ, only the unified abstraction Around is available in AspectD
. In terms of whether to modify the original method, two types, Call and Execute, are available. The PointCut
of the former is the call point and the PointCut
of the latter is the execution point.
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;
}
}
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;
}
Only Call and Execute are supported, which is not enough for Flutter (Dart.) On one hand, Flutter does not allow reflection. Even if Flutter allowed reflection, it would still not be enough to meet the needs. For a typical scenario, if the class "y" in the x.dart
file defines a private method "m" or a member variable "p" in the Dart code to be injected, the code cannot be accessed in aop_impl.dart
; not to mention obtaining multiple consecutive private variable properties. On the other hand, it may not be enough to operate the entire method. We need to insert processing logic into the method. To solve this problem, the syntax Inject is designed in AspectD
. For more information, see the following example. The Flutter library contains the following gesture-related code:
@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;
},
);
}
If we want to add a processing logic for the instance and context after onTapCancel
, Call and Execute are not feasible. However, after Inject is used, only a few simple statements are needed to solve the problem.
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');
}
}
Based on the preceding processing logic, the GestureDetector.build
method in the Dill file after compilation is:
@#C7
method build(fra::BuildContext* context) → fra::Widget* {
final core::Map<core::Type*, ges::GestureRecognizerFactory<rec::GestureRecognizer*>*>* gestures = <core::Type*, ges::GestureRecognizerFactory<rec::GestureRecognizer*>*>{};
if(!this.{ges::GestureDetector::onTapDown}.{core::Object::==}(null) || !this.{ges::GestureDetector::onTapUp}.{core::Object::==}(null) || !this.{ges::GestureDetector::onTap}.{core::Object::==}(null) || !this.{ges::GestureDetector::onTapCancel}.{core::Object::==}(null)) {
gestures.{core::Map::[]=}(tap::TapGestureRecognizer*, new ges::GestureRecognizerFactoryWithHandlers::•<tap::TapGestureRecognizer*>(() → tap::TapGestureRecognizer* => new tap::TapGestureRecognizer::•(debugOwner: this), (tap::TapGestureRecognizer* instance) → core::Null? {
let final tap::TapGestureRecognizer* #t2163 = instance in let final void #t2164 = #t2163.{tap::TapGestureRecognizer::onTapDown} = this.{ges::GestureDetector::onTapDown} in let final void #t2165 = #t2163.{tap::TapGestureRecognizer::onTapUp} = this.{ges::GestureDetector::onTapUp} in let final void #t2166 = #t2163.{tap::TapGestureRecognizer::onTap} = this.{ges::GestureDetector::onTap} in let final void #t2167 = #t2163.{tap::TapGestureRecognizer::onTapCancel} = this.{ges::GestureDetector::onTapCancel} in #t2163;
core::print(instance);
core::print(context);
core::print("Aspectd:KWLM25");
}));
}
In addition, compared with Call/Execute
, the input parameters of Inject contain the additional lineNum
parameter, which is used to specify the specific row number of the insert logic.
We can compile aop.dart
to achieve the purpose of compiling both the original project code and the AspectD
code into the Dill file, and then implement the Dill transformation to implement AOP. However, the standard Flutter build (flutter_tools
) does not support this process. Therefore, the build process needs to be slightly modified. In AspectJ, this process is implemented by AspectJ compiler (Ajc), a non-standard Java compiler. In AspectD, the application patch can be appended to flutter_tools
to support 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...
Based on AspectD, we have successfully removed all invasive code for the Flutter framework in practice, and implemented the same features as those when the intrusive code was used, supporting the recording and playback of hundreds of scripts and the stable and reliable operation of automatic regression.
From the perspective of AspectD, Call and Execute can help us easily implement features, such as performance tracking (call duration of key methods), log enhancement (obtaining details about the place where a method is specifically called), and Doom recording and playback (such as the recording and playback of random number sequence generation.) The Inject syntax is more powerful. It can implement the free injection of logic by means similar to source code and support complex scenarios, such as application recording and automatic regression (for example, recording and playback of user touch events.)
Furthermore, the AspectD principle is based on the Dill transformation. With the power of Dill, developers can freely operate on Dart compilation outputs. In addition, this transformation is powerful, reliable, and targeted for AST objects at nearly the source code level. Whether it is the logic replacement or the JSON-model transformation, Dill provides a new perspective and possibility.
Flutter Analysis and Practice: Design of Lightweight Dynamic Rendering Engine
Flutter Analysis and Practice: Practices of High-Performance Dynamic Template Rendering
56 posts | 4 followers
FollowAlibaba Tech - July 11, 2019
XianYu Tech - September 7, 2020
XianYu Tech - November 10, 2020
XianYu Tech - September 10, 2020
Alibaba Clouder - September 21, 2020
XianYu Tech - September 7, 2020
56 posts | 4 followers
FollowHelp enterprises build high-quality, stable mobile apps
Learn MoreAn enterprise-level continuous delivery tool.
Learn MoreProvides comprehensive quality assurance for the release of your apps.
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by XianYu Tech