|
Beike AspectD的原理及运用
Beike AspectD的原理及运用
肖鹏@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2020年10月14日 20:21
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中有以下代码:classMyAppextendsStatelessWidget{//Thiswidgetistherootofyourapplication.@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'FlutterDemo',theme:ThemeData(primarySwatch:Colors.blue,),routes:{'/'context)=>MyHomePage(title:'FlutterDemoHomePage'),'/welcome'context)=>WelcomePage(),'/bye'context)=>ByePage(),},home:MyHomePage(title:'FlutterDemoHomePage'),);}}在我们转换后的output.dill.txt文件中看到对于的代码如下:classMyAppextendsfra2::StatelessWidget{syntheticconstructor()→main2::MyApp*:superfra2::StatelessWidget:);@#C7methodbuild(fra2::BuildContext*context)→fra2::Widget*{returnnewapp3::MaterialApp:title:"FlutterDemo",theme:the3::ThemeData:primarySwatch:#C28264,visualDensity:the3::VisualDensity::adaptivePlatformDensity),routes:{"/"fra2::BuildContext*context)→main2::MyHomePage*=>newmain2::MyHomePage:title:"FlutterDemoHomePage"),"/welcome"fra2::BuildContext*context)→wel::WelcomePage*=>newwel::WelcomePage:),"/bye":(fra2::BuildContext*context)→bye::ByePage*=>newbye::ByePage::()},home:newmain2::MyHomePage::(title:"FlutterDemoHomePage"));}}刚才已经提到,flutter的aop是基于对dill文件的操作,所有的操作都是基于AST的遍历。2.3 AST首先我们可以通过以下代码读取Component(本文Flutter使用的是1.12.13,后同)finalComponentcomponent=Component();finalListbytes=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,方法调用等进行访问。RvisitVariableGet(VariableGetnode)=>defaultExpression(node);RvisitVariableSet(VariableSetnode)=>defaultExpression(node);RvisitPropertyGet(PropertyGetnode)=>defaultExpression(node);RvisitPropertySet(PropertySetnode)=>defaultExpression(node);RvisitDirectPropertyGet(DirectPropertyGetnode)=>defaultExpression(node);RvisitDirectPropertySet(DirectPropertySetnode)=>defaultExpression(node);RvisitSuperPropertyGet(SuperPropertyGetnode)=>defaultExpression(node);RvisitSuperPropertySet(SuperPropertySetnode)=>defaultExpression(node);RvisitStaticGet(StaticGetnode)=>defaultExpression(node);RvisitStaticSet(StaticSetnode)=>defaultExpression(node);RvisitMethodInvocation(MethodInvocationnode)=>defaultExpression(node);RvisitDirectMethodInvocation(DirectMethodInvocationnode)=>defaultExpression(node);RvisitSuperMethodInvocation(SuperMethodInvocationnode)=>defaultExpression(node);RvisitStaticInvocation(StaticInvocationnode)=>defaultExpression(node);RvisitConstructorInvocation(ConstructorInvocationnode)=>defaultExpression(node);下面我们写一个简单的demo来实现方法调用的替换。如下,我们在main()函数中读取dill文件,然后对读取的Component进行访问。voidmain(){finalStringpath='/Users/beike/aop_demo/.dart_tool/flutter_build/6840774ade9dd94681307ab48f4846dc/app.dill';Componentcomponent=readComponent(path);MethodVisitorvisitor=MethodVisitor();component.libraries.forEach((element){if(element.reference.canonicalName.name=='package:aop_demo/main.dart'){visitor.visitLibrary(element);}});writeComponent(path,component);}然后我们对方法调用进行访问,把_MyHomePageState类中所有对printCounter()方法的调用替换为调用printCounterHook()方法。classMethodVisitorextendsTransformer{@overrideMethodInvocationvisitMethodInvocation(MethodInvocationmethodInvocation){methodInvocation.transformChildren(this);finalNodenode=methodInvocation.interfaceTargetReference.node;if(nodeisProcedure&node!=null){finalLibrarylibrary=node.parent.parent;finalClasscls=node.parent;finalStringclsName=cls.name;finalStringmethodName=methodInvocation.name.name;if(clsName=='_MyHomePageState'&methodName=='printCounter'){MethodInvocationhookMethodInvocation=MethodInvocation(methodInvocation.receiver,Name('printCounterHook'),null);returnhookMethodInvocation;}}returnmethodInvocation;}}这样我们就在不侵入业务代码的前提下做到了更改业务代码。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")voidhookPrintCounter(PointCutpointcut){print('printCountercalled');pointcut.proceed();}Execute:执行处作为切入点@Execute("package:aop_demo/main.dart","MyApp","-build")@pragma("vm:entry-point")WidgethookBuild(PointCutpointcut){print('hookBuildcalled');returnpointcut.proceed();}Inject:在指定代码行处插入代码@Inject("package:flutter/src/material/page.dart","MaterialPageRoute","-buildPage",lineNum:92)@pragma("vm:entry-point")voidhookBuildPage(){dynamicresult;//AspectdIgnoredynamicroute1=this;print(route1);print('Buildingpage${result}');}Add:在指定位置添加方法@Add("package:aop_demo\\/.+\\.dart",".*",isRegex:true)@pragma("vm:entry-point")StringimportUri(PointCutpointCut){returnpointCut.sourceInfos["importUri"];}如上面代码我们在aop_demo中所有的类中添加了widgetUri()方法,返回widget所在文件的importUri。PointCutCall、Execute、Add模式下,我们看到在方法中返回PointCut对象,PointCut包含以下信息,其中调用procceed()就会调用原始方法实现。classPointCut{///PointCutdefaultconstructor.@pragma('vm:entry-point')PointCut(this.sourceInfos,this.target,this.function,this.stubKey,this.positionalParams,this.namedParams,this.members,this.annotations);///Sourceinfomationlikefile,linenum,etcforacall.finalMapsourceInfos;///Targetwhereacallisoperatingon,likexforx.foo().finalObjecttarget;///Functionnameforacall,likefooforx.foo().finalStringfunction;///Uniquekeywhichcanhelptheproceedfunctiontodistinguisha///mockedcall.finalStringstubKey;///Positionalparametersforacall.finalListpositionalParams;///Namedparametersforacall.finalMapnamedParams;///Class'smembers.InCallmode,it'scallerclass'smembers.Inexecutemode,it'sexecutionclass'smembers.finalMapmembers;///Class'sannotations.InCallmode,it'scallerclass'sannotations.Inexecutemode,it'sexecutionclass'sannotations.finalMapannotations;///Unifiedentrypointtocallaoriginalmethod,///themethodbodyisgenerateddynamicallywhenbeingtransformedin///compiletime.@pragma('vm:entry-point')Objectproceed(){returnnull;}}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方法中的原始实现:methodbuild(fra::BuildContext*context)→fra::Widget*{returnnewhook::hook::().hookBuild(newpoi:ointCut::({"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],{},{},{}));}methodbuild_aop_stub_1(fra::BuildContext*context)→fra::Widget*{returnnewapp::MaterialApp::(title:"FlutterDemo",theme:the::ThemeData::(primarySwatch:#C124),home:newmain::MyHomePage::(title:"FlutterDemoHomePage",$creationLocationd_0dea112b090073317d4:#C132),$creationLocationd_0dea112b090073317d4:#C142);}在PointCut中定义了aop_stub1方法,调用了build_aop_stub_1方法。methodproceed()→core::Object*{if(this.stubKey.==("aop_stub_1")){returnthis.aop_stub_1();}returnnull;}methodaop_stub_1()→core::Object*{return(this.targetasmain::MyApp).{=main::MyApp::build_aop_stub_1}(this.positionalParams.[](0)asfra::BuildContext*);}所以整个调用链变成了:方法调用-> build -> hookBuild -> PointCut.procced -> aop_stub1 -> build_aop_stub_14 应用场景Beike_AspectD在贝壳已经在性能检测、埋点、JSONModel转换等库使用。下面我们来通过一个简单的例子看看Beike_AspectD如何实现页面展示统计。@Inject("package:flutter/src/material/page.dart","MaterialPageRoute","-buildPage",lineNum:92)@pragma("vm:entry-point")voidhookBuildPage(){dynamicresult;//AspectdIgnoreStringwidgetName=result.toString();//widgetName为当前展示页面的名字//后续执行页面展示上报逻辑//.............}首先我们对MaterialPageRoute的buildPage插入代码,获取当前显示widget的名字。但问题是dart中允许定义同名类,只是获取widget的名字还无法唯一确定页面,我们需要知道widget定义所在的文件,于是我们做了如下更改:@Inject("package:flutter/src/material/page.dart","MaterialPageRoute","-buildPage",lineNum:92)@pragma("vm:entry-point")voidhookBuildPage(){dynamicresult;//AspectdIgnoreStringwidgetName=result.toString();StringimportUri=result.importUri(null);print(widgetName+importUri);//widgetName为当前展示页面的名字,importUri为widget所在文件的uri//后续执行页面展示上报逻辑//.............}@Add("package:aop_demo\\/.+\\.dart",".*",isRegex:true)@pragma("vm:entry-point")StringimportUri(PointCutpointCut){returnpointCut.sourceInfos["importUri"];}我们通过Add给widget添加了获取importUri的方法,这样有了importUri和widgetName我们就能够唯一的确定widget,然后就可以完成剩下的上报流程。5 参考资料https://blog.csdn.net/yunqiinsight/article/details/92814036http://gityuan.com/2019/09/14/flutter_frontend_server/
预览时标签不可点
移动端37大前端69Flutter15移动端 · 目录#移动端上一篇贝塞尔曲线在iOS端的绘图实践下一篇贝壳APP iOS14权限管理适配总结修改于2020年10月14日关闭更多小程序广告搜索「undefined」网络结果
修改于2020年10月14日
|
|