Look! Xianyu has open sourced another Flutter development tool

Alimei's introduction: With the rapid development of the Flutter framework, more and more businesses have begun to use Flutter to refactor or build new products. However, in our practice process, we found that on the one hand, Flutter has high development efficiency, excellent performance, and good cross-platform performance. On the other hand, Flutter also faces problems such as plug-ins, basic capabilities, and the lack or imperfection of the underlying framework. Today, the real thing from the Xianyu team takes us to solve a problem: how to solve AOP for Flutter?

problem background

In the process of implementing an automatic recording and playback, we found that we need to modify the code of the Flutter framework (Dart level) to meet the requirements, which will be intrusive to the framework. To solve this intrusive problem and better reduce the maintenance cost in the iterative process, the first solution we consider is aspect-oriented programming.

So how to solve the problem of AOP for Flutter? This article will focus on AspectD, an AOP programming framework for Dart developed by the Xianyu technical team.

AspectD: AOP Framework for Dart

Whether AOP capabilities are supported at runtime or compile time depends on the characteristics of the language itself. For example, in iOS, Objective C itself provides a powerful runtime and dynamism to make runtime AOP easy to use. Under Android, the characteristics of the Java language can not only implement a compile-time static proxy based on bytecode modification like AspectJ, but also can implement a runtime-enhanced runtime dynamic proxy like Spring AOP. What about Dart? First, Dart's reflection support is very weak, only supports inspection ( Introspection ), does not support modification ( Modification ); secondly, Flutter prohibits reflection for reasons such as package size and robustness.

Therefore, we design and implement AspectD, an AOP scheme based on compile-time modification.

1. Design details

2. Typical AOP scenario

The following AspectD code illustrates a typical AOP usage 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!');
}
}

3. API design for developers

Design of PointCut

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

PointCut needs to fully characterize in what way (Call/Execute, etc.), to which Library, which class (this item is empty in the case of Library Method), and which method to add AOP logic. Data structure of PointCut:

@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 source code information (such as library name, file name, line number, etc.), method call object, function name, parameter information, etc. Please pay attention to the @pragma('vm:entry-point') annotation here, its core logic lies in Tree-Shaking. Under AOT (ahead of time) compilation, if it cannot be called by the application main entry (main), it will be regarded as useless code and discarded. Because of the non-invasiveness of its injection logic, AOP code will obviously not be called by main, so this annotation is needed to tell the compiler not to discard this logic. The proceed method here 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 just an empty shell, and its content will be dynamically generated at runtime.

Advice's design

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

The effect of @pragma("vm:entry-point") here is the same as that described in a. The pointCut object is passed into the AOP method as a parameter, so that developers can obtain relevant information about the source code call information, implement their own logic or use pointcut .proceed() calls the original logic.

Aspect's design

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

Aspect annotation can make AOP implementation classes such as ExecuteDemo easily identified and extracted, and can also act as a switch, that is, if you want to disable this AOP logic, just remove the @Aspect annotation.

4. Compilation of AOP code

Contains the main entry of the original project

As can be seen from the above, aop.dart introduces import'package:example/main.dart'as app; this makes all the code of the entire example project included when compiling aop.dart.

Compiling in Debug Mode

Introduce import 'aop_impl.dart' in aop.dart; this allows the content in aop_impl.dart to be compiled in Debug mode even if it is not explicitly dependent on aop.dart.

Compilation in Release Mode

In AOT compilation (in Release mode), the Tree-Shaking logic makes the content of aop_impl.dart not compiled into dill when it is not called by main in aop. Its effect can be avoided by adding @pragma("vm:entry-point") .

When we use AspectD to write AOP code and generate intermediate products by compiling aop.dart, so that dill contains both original project code and AOP code, we need to consider how to modify it. In AspectJ, modification is done by manipulating the Class file, in AspectD we work on the dill file.

5. Dill operation

The dill file, also known as Dart Intermediate Language, is a concept in Dart language compilation. Whether it is Script Snapshot or AOT compilation, dill is required as an intermediate product.

Dill's structure

We can print out the internal structure of dill through dump_kernel.dart provided by the vm package in the dart sdk

Dill transform

Dart provides a way of Kernel to Kernel Transform, which can transform dill through recursive AST traversal of dill files.

Based on the AspectD annotations written by developers, the transformation part of AspectD can extract which libraries/classes/methods need to add what AOP code, and then implement functions such as Call/Execute by operating on the target class in the process of AST recursion. .

A typical Transform part of the logic is as follows:

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

By traversing the AST object in dill (the visitMethodInvocation function here), combined with the AspectD annotations written by the developer (aspectdInfoMap and aspectdItemInfo here), the original AST object (here methodInvocation ) can be transformed to change the original Code logic, that is, the Transform process.

6. Syntax supported by AspectD

Different from the three types of BeforeAroundAfter provided in AspectJ, in AspectD, there is only one unified abstraction, Around. In terms of whether to modify the internal of the original method, there are Call and Execute, the former's PointCut is the calling point, and the latter's PointCut is the execution point.

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 supports Call and Execute, which is obviously very thin for Flutter (Dart). On the one hand, Flutter prohibits reflection. To take a step back, even if Flutter enables reflection support, it is still very weak and cannot meet the needs. For a typical scenario, if the class y of the x.dart file defines a private method m or member variable p in the dart code to be injected, there is no way to access it in aop_impl.dart, let alone more A continuous private variable attribute is obtained. On the other hand, it may not be enough to just operate on the method as a whole, we may need to insert processing logic in the middle of the method. In order to solve this problem, AspectD designed a syntax Inject, see the following example: The flutter library contains this gesture-related code:

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 piece of processing logic for instance and context after onTapCancel, Call and Execute are not feasible, but after using Inject, we only need a few simple sentences to solve:



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

Through the above processing logic, the GestureDetector.build method in the compiled and built dill is as follows:

In addition, compared to Call/Execute, the input parameter of Inject has an additional named parameter of lineNum, which can be used to specify the specific line number of the insertion logic.

7. Build process support

Although we can compile aop.dart to compile the original project code and AspectD code to the dill file at the same time, and then implement the transformation of the dill level through Transform to achieve AOP, but the standard flutter build (ie fluttertools) does not support this process, so it is still necessary to Make minor modifications to the build process. In AspectJ, this process is implemented by Ajc, the non-standard Java compiler. In AspectD, support for AspectD can be achieved by applying Patch to fluttertools.

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

Actual combat and thinking

Based on AspectD, we have successfully removed all intrusive code for the Flutter framework in practice, achieved the same function as the intrusive code, and supported the recording and playback of hundreds of scripts and automated regression to run stably and reliably.

From the point of view of AspectD, Call/Execute can help us easily implement performance tracking (calling duration of key methods), log enhancement (getting detailed information about where a method is called), Doom recording and playback ( Such as the generation, recording and playback of random number sequences) and other functions. The Inject syntax is more powerful, and it can freely inject logic in a similar way to source code, and can support complex scenarios such as App recording and automated regression (such as the recording and playback of user touch events).

Further, the principle of AspectD is based on the Dill transformation. With the Dill operation, developers can freely operate the Dart compilation products, and this transformation is oriented to AST objects at the almost source code level, which is not only powerful but also reliable. . Whether it is to do some logical replacement or Json<--> model conversion, etc., it provides a new perspective and possibility.


Link to the original text
This article is the original content of Yunqi Community, and may not be reproduced without permission.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324160308&siteId=291194637