Flutter Analysis and Practice: AOP Design Practices

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

3.3.1 Typical AOP Scenarios

The following AspectD code illustrates a typical AOP application scenario:

3.3.2 Developer-Oriented API Design

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:

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.

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

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.

3.3.3 Compilation of AOP Code

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.

3.3.4 Dill File Operations

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

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.

3.3.5 Syntax Supported by AspectD

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.

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:

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.

Based on the preceding processing logic, the GestureDetector.build method in the Dill file after compilation is:

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.

3.3.6 Build Process Support

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.

3.3.7 Practices and Considerations

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.

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.