即刻获取 HarmonyOS应用开发者基础/高级认证 了解详情
写点什么

Beike AspectD 的原理及运用

  • 2020-10-29
  • 本文字数:6049 字

    阅读完需:约 20 分钟

Beike AspectD的原理及运用

1 项目背景

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。


AspectD 是咸鱼针对 Flutter 实现的 AOP 开源库,GitHub 地址如下:https://github.com/alibaba-flutter/aspectd


十分感谢咸鱼团队开源的 AspectD 开源库,AspectD 让 flutter 具备了 aop 的能力,给了贝壳 flutter 团队很多思路,让很多想法成为可能。

2 Flutter 相关知识介绍

首先,我们来回顾一下 flutter 编译相关的一些知识。

2.1 Flutter 编译流程


如上图,flutter 在编译时,首先由编译前端将 dart 代码转换为中间文件 app.dill,然后在 debug 模式下,将 app.dill 转换为 kernel_blob.bin(其实这个文件就是 app.dill 改了个名字),在 release 模式下,app.dill 被转换为 framework 或者 so。


Flutter 的 aop 就是对 app.dill 进行修改实现的。下面我们先来了解一下 app.dill 文件。

2.2 app.dill 文件

dill 文件是 dart 编译的中间文件,是 flutter_tools 调用 frontend_server 将 dart 转换生成的。我们可以在工程的 build 目录下找到编译生成的 dill 文件。


Dill 文件本身是不可读的,我们可以通过 dart vm 中的 dump_kernel.dart 来将 dill 文件转换为可读的文件。命令如下


dart   /path/to/dump_kernel.dart   /path/to/app.dill/U/path/of/output.dill.txt
复制代码


比如我们创建了一个 demo 工程叫做 aop_demo,我们在 main.dart 中有以下代码:


class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: {
'/': (context) => MyHomePage(title: 'Flutter Demo Home Page'),
'/welcome': (context) => WelcomePage(),
'/bye': (context) => ByePage(),
},
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
复制代码


在我们转换后的 output.dill.txt 文件中看到对于的代码如下:


class MyApp extends fra2::StatelessWidget {    synthetic constructor •() → main2::MyApp*      : super fra2::StatelessWidget::•()      ;    @#C7    method build(fra2::BuildContext* context) → fra2::Widget* {      return new app3::MaterialApp::•(title: "Flutter Demo", theme: the3::ThemeData::•(primarySwatch: #C28264, visualDensity: the3::VisualDensity::adaptivePlatformDensity), routes: {"/": (fra2::BuildContext* context) → main2::MyHomePage* => new main2::MyHomePage::•(title: "Flutter Demo Home Page"), "/welcome": (fra2::BuildContext* context) → wel::WelcomePage* => new wel::WelcomePage::•(), "/bye": (fra2::BuildContext* context) → bye::ByePage* => new bye::ByePage::•()}, home: new main2::MyHomePage::•(title: "Flutter Demo Home Page"));    }  }
复制代码


刚才已经提到,flutter 的 aop 是基于对 dill 文件的操作,所有的操作都是基于 AST 的遍历。

2.3 AST

首先我们可以通过以下代码读取 Component(本文 Flutter 使用的是 1.12.13,后同)


final Component component = Component();
final List bytes = File(dillFile).readAsBytesSync();
BinaryBuilderWithMetadata(bytes).readComponent(component);
复制代码


其中 dillFile 为 app.dill 文件的路径。读取的 Component 中包含了我们 app 的所有的 Library,一个 Library 对应我们 flutter 项目中的一个 dart 文件。它的结构如下:



AST 在 flutter 中有很多的运用,如 analyzer 库使用 AST 对代码进行静态分析,dartdevc 使用 AST 进行 dart 和 js 转换,还有就是现有的一些热修复方案也是使用 AST 进行动态解释执行的。

2.4 访问 AST

既然 AST 有这么多运用,那如何对语法树进行分析呢?在这里我们用到的是 kernel 中的 visitor.dart 这个库。


visitor.dart 使用访问者模式,提供了丰富的语法树访问的方法。下面代码中我们列出了该库中的部分方法,可以看到,我们可以对 AST 中变量、属性、super 属性的 set 和 get,方法调用等进行访问。


 R visitVariableGet(VariableGet node) => defaultExpression(node);
R visitVariableSet(VariableSet node) => defaultExpression(node);
R visitPropertyGet(PropertyGet node) => defaultExpression(node);
R visitPropertySet(PropertySet node) => defaultExpression(node);
R visitDirectPropertyGet(DirectPropertyGet node) => defaultExpression(node);
R visitDirectPropertySet(DirectPropertySet node) => defaultExpression(node);
R visitSuperPropertyGet(SuperPropertyGet node) => defaultExpression(node);
R visitSuperPropertySet(SuperPropertySet node) => defaultExpression(node);
R visitStaticGet(StaticGet node) => defaultExpression(node);
R visitStaticSet(StaticSet node) => defaultExpression(node);
R visitMethodInvocation(MethodInvocation node) => defaultExpression(node);
R visitDirectMethodInvocation(DirectMethodInvocation node) =>
defaultExpression(node);
R visitSuperMethodInvocation(SuperMethodInvocation node) =>
defaultExpression(node);
R visitStaticInvocation(StaticInvocation node) => defaultExpression(node);
R visitConstructorInvocation(ConstructorInvocation node) =>
defaultExpression(node);
复制代码


下面我们写一个简单的 demo 来实现方法调用的替换。


如下,我们在 main()函数中读取 dill 文件,然后对读取的 Component 进行访问。


void main() {
final String path =
'/Users/beike/aop_demo/.dart_tool/flutter_build/6840774ade9dd94681307ab48f4846dc/app.dill';
Component component = readComponent(path);
MethodVisitor visitor = MethodVisitor();
component.libraries.forEach((element) {
if (element.reference.canonicalName.name == 'package:aop_demo/main.dart') {
visitor.visitLibrary(element); }
});
writeComponent(path, component);}
复制代码


然后我们对方法调用进行访问,把_MyHomePageState 类中所有对 printCounter()方法的调用替换为调用 printCounterHook()方法。


class MethodVisitor extends Transformer {
@override
MethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) {
methodInvocation.transformChildren(this);
final Node node = methodInvocation.interfaceTargetReference?.node;
if (node is Procedure && node != null) {
final Library library = node.parent.parent;
final Class cls = node.parent;
final String clsName = cls.name;
final String methodName = methodInvocation.name.name;
if (clsName == '_MyHomePageState' && methodName == 'printCounter') {
MethodInvocation hookMethodInvocation = MethodInvocation(
methodInvocation.receiver, Name('printCounterHook'), null);
return hookMethodInvocation;
}
}
return methodInvocation;
}
}
复制代码


这样我们就在不侵入业务代码的前提下做到了更改业务代码。

3 Beike_AspectD 介绍

关于 AspectD,官方已经介绍的比较详细,下面我们主要介绍一下贝壳的 Beike_AspectD。



Beike_AspectD 主要包括三部分:


  • 切入点的设计:包括了 Call、Execute、Inject、Add 四种方式;

  • 代码转换

  • 业务方的 hook 代码

3.1 切入点设计

首先我们来介绍一下切入点的设计。Beike_AspectD 支持四种切入方式:

Call:调用处作为切入点

如下面代码,我们在调用_MyHomePageState 的 printCounter()方法的代码处添加了 print 输出。


  @Call("package:aop_demo/main.dart", "_MyHomePageState", "-printCounter")
@pragma("vm:entry-point")
void hookPrintCounter(PointCut pointcut) {
print('printCounter called');
pointcut.proceed();
}
复制代码

Execute:执行处作为切入点

  @Execute("package:aop_demo/main.dart", "MyApp", "-build")
@pragma("vm:entry-point")
Widget hookBuild(PointCut pointcut) {
print('hookBuild called');
return pointcut.proceed();
}
复制代码

Inject:在指定代码行处插入代码

@Inject("package:flutter/src/material/page.dart", "MaterialPageRoute",
"-buildPage",
lineNum: 92)
@pragma("vm:entry-point")
void hookBuildPage() {
dynamic result; //Aspectd Ignore
dynamic route1 = this;
print(route1);
print('Building page ${result}');
}
复制代码

Add:在指定位置添加方法

  @Add("package:aop_demo\\/.+\\.dart", ".*", isRegex: true)
@pragma("vm:entry-point")
String importUri(PointCut pointCut) {
return pointCut.sourceInfos["importUri"];
}
复制代码


如上面代码我们在 aop_demo 中所有的类中添加了 widgetUri()方法,返回 widget 所在文件的 importUri。

PointCut

Call、Execute、Add 模式下,我们看到在方法中返回 PointCut 对象,PointCut 包含以下信息,其中调用 procceed()就会调用原始方法实现。


class PointCut {
/// PointCut default constructor. @pragma('vm:entry-point') PointCut(this.sourceInfos, this.target, this.function, this.stubKey, this.positionalParams, this.namedParams, this.members, this.annotations);
/// Source infomation like file, linenum, etc for a call. final Map sourceInfos;
/// Target where a call is operating on, like x for x.foo(). final Object target;
/// Function name for a call, like foo for x.foo(). final String function;
/// Unique key which can help the proceed function to distinguish a /// mocked call. final String stubKey;
/// Positional parameters for a call. final List positionalParams;
/// Named parameters for a call. final Map namedParams;
/// Class's members. In Call mode, it's caller class's members. In execute mode, it's execution class's members. final Map members;
/// Class's annotations. In Call mode, it's caller class's annotations. In execute mode, it's execution class's annotations. final Map annotations;
/// Unified entrypoint to call a original method, /// the method body is generated dynamically when being transformed in /// compile time. @pragma('vm:entry-point')
Object proceed() {
return null;
}
}
复制代码

3.2 代码转换

Beike_AspectD 将转换流程集成到 ke_flutter_tools,这样只要集成了贝壳的 flutter 库,就不用再做额外的适配。整个转换的流程如下:



下面我们以 Execute 为例子看一下 Beike_AspectD 对 dill 文件做了怎样的转换。


还是上面的 Execute 替换,我们将 dill 文件转换之后看到 build 方法的实现被替换为直接调用我们 hook 方法 hookBuild。并且在被 hook 的类中添加了方法 build_aop_stub_1,build_aop_stub1 中的实现为 build 方法中的原始实现:


   method build(fra::BuildContext* context) → fra::Widget* {
return new hook::hook::•().hookBuild(new poi::PointCut::•({"importUri": "package:aop_demo/main.dart", "library": "package:aop_demo", "file": "file:///Users/beike/aop_demo/lib/main.dart", "lineNum": "1", "lineOffset": "0", "procedure": "MyApp::build"}, this, "build", "aop_stub_1", [context], {}, {}, {})); }

method build_aop_stub_1(fra::BuildContext* context) → fra::Widget* {
return new app::MaterialApp::•(title: "Flutter Demo", theme: the::ThemeData::•(primarySwatch: #C124), home: new main::MyHomePage::•(title: "Flutter Demo Home Page", $creationLocationd_0dea112b090073317d4: #C132), $creationLocationd_0dea112b090073317d4: #C142);
}
复制代码


在 PointCut 中定义了 aop_stub1 方法,调用了 build_aop_stub_1 方法。


   method proceed() → core::Object* {
if(this.stubKey.==("aop_stub_1")) {
return this.aop_stub_1();
}
return null;
}
method aop_stub_1() → core::Object* {
return (this.target as main::MyApp?).{=main::MyApp::build_aop_stub_1}(this.positionalParams.[](0) as fra::BuildContext*);
}
复制代码


所以整个调用链变成了:


方法调用-> build -> hookBuild -> PointCut.procced -> aop_stub1 -> build_aop_stub_1

4 应用场景

Beike_AspectD 在贝壳已经在性能检测、埋点、JSONModel 转换等库使用。下面我们来通过一个简单的例子看看 Beike_AspectD 如何实现页面展示统计。


@Inject("package:flutter/src/material/page.dart", "MaterialPageRoute",
"-buildPage",
lineNum: 92)
@pragma("vm:entry-point")

void hookBuildPage() {
dynamic result; //Aspectd Ignore
String widgetName = result.toString();
//widgetName为当前展示页面的名字
//后续执行页面展示上报逻辑
//.............
}
复制代码


首先我们对 MaterialPageRoute 的 buildPage 插入代码,获取当前显示 widget 的名字。但问题是 dart 中允许定义同名类,只是获取 widget 的名字还无法唯一确定页面,我们需要知道 widget 定义所在的文件,于是我们做了如下更改:


  @Inject("package:flutter/src/material/page.dart", "MaterialPageRoute",
"-buildPage",
lineNum: 92)
@pragma("vm:entry-point")
void hookBuildPage() {
dynamic result; //Aspectd Ignore
String widgetName = result.toString();
String importUri = result.importUri(null);
print(widgetName + importUri);
//widgetName为当前展示页面的名字,importUri为widget所在文件的uri
//后续执行页面展示上报逻辑
//.............
}
@Add("package:aop_demo\\/.+\\.dart", ".*", isRegex: true)
@pragma("vm:entry-point")
String importUri(PointCut pointCut) {
return pointCut.sourceInfos["importUri"];
}
复制代码


我们通过 Add 给 widget 添加了获取 importUri 的方法,这样有了 importUri 和 widgetName 我们就能够唯一的确定 widget,然后就可以完成剩下的上报流程。

5 参考资料


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


Beike AspectD的原理及运用


2020-10-29 10:002471

评论

发布
暂无评论
发现更多内容

Spring MVC框架:第七章:REST架构风格(1)

Java 程序员 后端

Spring Cloud Gateway修改请求和响应body的内容

Java 程序员 后端

Spring Boot 集成 Elasticsearch 实战

Java 程序员 后端

Spring Boot在微服务中的最佳实践

Java 程序员 后端

外包学生管理系统详细架构设计文档

Beyond Ryan

SAP为Java 16贡献JEP 387 “弹性元空间”

Java 程序员 后端

Spring Cloud Gateway限流实战

Java 程序员 后端

Spring Cloud:第二章:eureka服务发现

Java 程序员 后端

Spring MVC框架:第六章:传统增删改查

Java 程序员 后端

RocketMQ源码分析之NameServer

Java 程序员 后端

spring boot 自定义配置文件&参数绑定

Java 程序员 后端

Spring Boot 中三种跨域场景总结,这篇必看!不看后悔系列

Java 程序员 后端

Spring MVC框架:第七章:REST架构风格

Java 程序员 后端

Go WebSocket开发与测试实践【/net/websocket】

FunTester

Java websocket 接口测试 Go 语言 FunTester

Servlet 入门

Java 程序员 后端

Spring Cloud 2020 版本最佳实践,你落伍了

Java 程序员 后端

Spring @Lookup实现单例bean依赖注入原型bean

Java 程序员 后端

Spring Boot 快速入门(一)

Java 程序员 后端

Spring Boot 操作 Redis 的各种实现

Java 程序员 后端

Vue进阶(幺柒伍):色彩搭配

No Silver Bullet

Vue 11月日更

Spring boot —— 创建parent工程

Java 程序员 后端

Spring MVC温故而知新 – 从零开始

Java 程序员 后端

fastposter 2.2.0 新版本发布 电商级海报生成器

物有本末

Java Vue 海报 fastposter 海报生成器

Serverless 如何在阿里巴巴实现规模化落地?

Java 程序员 后端

Spring Boot + EasyExcel 导入导出,好用到爆!

Java 程序员 后端

Spring Boot 实战(11)整合MyBatis-Plus

Java 程序员 后端

Spring Boot 接入 GitHub 第三方登录,只要两行配置!

Java 程序员 后端

Spring Boot 核心的 25 个注解

Java 程序员 后端

Spring Boot+Mybatis+thymeleaf整合

Java 程序员 后端

【LeetCode】合并两个有序数组Java题解

Albert

算法 LeetCode 11月日更

【Flutter 专题】12 图解圆形与权重/比例小尝试

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

Beike AspectD的原理及运用_开源_肖鹏_InfoQ精选文章