贝壳Flutter调试工具-FDB

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"开源地址","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GitHub地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/LianjiaTech/flutter_fdb_package","title":"","type":null},"content":[{"type":"text","text":"https://github.com/LianjiaTech/flutter_fdb_package","attrs":{}}]}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前Flutter在贝壳的使用量越来越高,业务中Flutter页面达到600+,甚至在某些业务线Flutter页面占比达到70%。这种状况下我们迫切需要一个功能完善、体验流畅的Flutter调试工具。调研市面上Flutter调试工具之后,结合我们公司的业务特点,开发了自己的Flutter调试工具——FDB。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文将简要介绍FDB有哪些功能,并重点介绍核心功能是如何实现的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.FDB有什么用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB(Flutter Debug)不仅仅是只面向开发过程的工具,也解决性能优化、设计走查、QA测试等环节的痛点问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter的研发过程中,您肯定遇见过以下问题:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"设计反复确认Widget的大小、对齐等UI问题","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于不知道包中代码是否是最新代码,导致重新打包测试","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"开发过程中反复设置背景色,来确定Widget的边界范围","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"性能优化时,没有快捷的工具查看内存使用情况、内存泄漏情况","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"...","attrs":{}}]}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,我们开发了以下功能,为开发者在开发过程中或优化性能时,提供精确的数据支撑和问题定位。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d166bc9fdd554fe3a68ea29a5800dbed.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出FDB的功能包含:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"组件信息检查","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内存详细数据的展示","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内存泄漏自动检测","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FPS检测","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"源码文件和源码具体行数的展示","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这些功能都需要我们在合适的节点中获取,这些节点穿插在Dart代码的运行流程中,这些功能的实现需要我们对Dart代码的运行流程有大概的了解,这样我们知道在哪个节点能获取哪些信息。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.前置知识点","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"总的来说,Dart代码的运行可分为:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"前端编译阶段、虚拟机运行阶段","attrs":{}},{"type":"text","text":"。接下来,我们从编译、运行两个方面来说。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dart VM有多种方式运行Dart代码:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• JIT模式下运行源码或者Kernal binary","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 通过snapshot的方式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鉴于本文描述的使用场景,我们以Debug模式为例来介绍Dart VM是如何运行我们的代码。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 Flutter前端编译器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter Debug模式下我们的Dart源码被gen_kernel处理成Kernal binary,也就是dill文件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以在Debug产物中找到该dill文件,这便是Kernal binary。Kernal是一种从Dart衍生而来的高级语言,用于分析和进一步的转化。Kernal binary就是该语言的描述,它包含了序列化的Kernel ASTs以及内存标识。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Kernel AST","attrs":{}},{"type":"text","text":"是CFE(前端编译器 common front-end)生成,有语义分析的作用,可交付于Dart VM、dev_compiler以及dart2js等工具直接用于语义的分析。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"内存标识","attrs":{}},{"type":"text","text":"可以序列化成可被虚拟机运行的机器码,类似于Class和JVM的关系。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的前端编译流程如下图所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1cc0da9a6ce36a5561aacc820eaab162.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#ffffff","name":"user"}}],"text":"snapshot_delegate","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过以上简单的介绍,我们了解了Debug模式下代码是如何编译成产物的。接下来我们重点来看:编译的产物是如何被VM运行的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2. JIT产物的运行","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.1. VM的初始化工作","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虚拟机要执行任务需要初始化以下几个功能模块:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"运行时系统: ","attrs":{}},{"type":"text","text":"负责运行期间代码的装载(懒加载)和释放。比如:对象的实例化、类成员信息的读取,方法的调用,内存GC,并保存了签名信息(快照)。由此可知,我们获取内存信息、方法调用链等运行时数据或主动触发GC都是在此处。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"service协议: ","attrs":{}},{"type":"text","text":"这是VM为方便我们拿到运行时数据而开启的一个服务,连接此服务之后,我们就可以通过Dart VM的相关协议,拿到运行时系统的数据。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"其他","attrs":{}},{"type":"text","text":":包含核心库原生方法、编译流水线、解释器、ARM 模拟器。鉴于本文不涉及VM的其他部分,暂不讲解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下图为VM的整体结构:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/23/232a7847310628e678817b39a9ea3a4a.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"总结一下:运行时数据代表了Dart虚拟机的具体运行状态,比如","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"内存快照、对象分配、方法调用","attrs":{}},{"type":"text","text":"等等。并且我们可以通过service协议来获取运行时数据,像Dev-Tools、Android Studio中的调试功能都是通过这个协议来实现的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那么,如果我们想获取当前虚拟机的内存信息、内存中的Class、Class的实例、实例的具体信息、方法调用链等属性,就需要借助此Service去找运行时数据要。具体的如何通过Service获取VM运行时中的数据,后面的2.4补充知识点中会详细介绍。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,到此VM已经具备了执行我们Dart代码的能力。因为Dart VM所需要的运行数据包含在Flutter的“三棵树”中,接下来,我们来看Flutter的“三棵树”。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.2. Flutter的“三棵树”","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们开发的代码,从runApp开始到页面展示出来,期间Flutter会用我们的代码生成“三棵树”:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Widget树","attrs":{}},{"type":"text","text":":是开发者使用Flutter对一个页面的描述;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Element树","attrs":{}},{"type":"text","text":":每一个Widget都会有一个Element与之对应,因此Flutter会根据Widget树生成Element,并且Element树是Widget树转化为RenderObject树的中间产物,起到“上下文”的作用;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"RenderObject树","attrs":{}},{"type":"text","text":":真正用于绘制的树,包含了尺寸等具体布局信息;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/78/783e112a866f3c6c3067974bd90a0e57.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此可见“三棵树”关系如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Widget可通过createElement()方法创建Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Element通过调用Widget的createRenderObject()方法创建RenderObject","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Element直接持有Widget和RenderObject。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• RenderObject通过DebugCreator包装器的方式间接持有Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那么,如果我们想获取页面上某个元素的属性,找到Element就是关键,因为Element作为上下文,可以拿到具体的Widget及RendObject,从而拿到其属性及布局信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,到此,我们知道VM将我们代码通过三棵树的原理转化为可渲染的对象,从而渲染出来,而且我们可以通过Element获取到Widget,需要注意的是“三棵树”给我们的信息是组件的静态信息,但是如果我们想获取某个Widget的轮廓等动态信息以及对应我们的源码文件名和行列等信息那就要介绍另两个概念:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspector和WidgetInspectorService","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.3. WidgetInspector和WidgetInspectorService","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter的入口main函数中,会使用runApp()创建一个WidgetsApp,WidgetsApp便会在Debug模式下为我们开启一个WidgetInspector。那么WidgetInspector是什么呢,WidgetInspector和WidgetInspectorService之间有什么关系呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspector","attrs":{}},{"type":"text","text":":可以检查屏幕上一个widget的结构,包括轮廓、属性及对应源码的位置。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspectorService","attrs":{}},{"type":"text","text":":WidgetInspector实例不能被用户直接获取使用,WidgetInspectorService为用户提供了全局的单例,用来操作WidgetInspector从而获取我们想要的信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那么,如果我们想获取界面上某个具体Widget的轮廓、属性及对应源码的位置信息,就需要通过WidgetInspectorService.instance单例去获取。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.4. 补充知识点:怎么在VM运行时中获取数据","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们知道VM运行时是C++开发的,在Flutter中直接获取VM的运行时数据需要借助一个中间件,幸运的是官方为我们提供了Service,借助Service的能力,我们便可以实现拿到VM 运行时的数据。既然是Service,那么就需要一个数据传输的协议。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"协议双方遵循C-S架构,虚拟机作为远端服务来响应工具的请求,请求和响应的格式是Json。服务端暴露了很多接口,比如获取版本信息,获取内存快照、获取方法调用和对象信息等等。我们以getVersion为例看一下调用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调试工具以下面的格式发出请求:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n \"jsonrpc\": \"2.0\",\n \"method\": \"getVersion\",\n \"params\": {},\n \"id\": \"1\"\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服务端以下面的格式返回响应数据:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"type\": \"Version\",\n \"major\": 3,\n \"minor\": 5\n }\n \"id\": \"1\"\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,虚拟机通过ObjRef、Obj、id这三个字段,来描述一个具体的对象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ObjRef","attrs":{}},{"type":"text","text":":对象的基本信息,比如name、id,但是不包括完整信息,我们可以把它理解成是一个对象指针。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"•","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"id","attrs":{}},{"type":"text","text":":即ObjRef中的id,它是对象的唯一标识,VM通过id来识别一个对象。举个例子,如果你想获取一个对象的详细信息,就需要给VM该对象的id。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Obj","attrs":{}},{"type":"text","text":":对象的详细信息,也就是我们通过id在VM处获取到的完整对象。它包含该对象所有的信息,比如字段、父类等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图示三个字段的关系:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5cb99435296c78cb1235d852077d2dbe.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样一个完整的数据就从虚拟机获得了,那虚拟机具体支持哪些接口呢?大家可以看这一篇","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md","title":"","type":null},"content":[{"type":"text","text":"官方文档","attrs":{}}]},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般常用接口如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getAllocationProfile","attrs":{}},{"type":"text","text":":获取当前运行内存中Class、实例等的内存使用数据,另外该方法可获取每次GC的时间戳,还可以主动触发GC。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getClassList","attrs":{}},{"type":"text","text":":获取VM中所有类。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getInstances","attrs":{}},{"type":"text","text":":获取某个类所有实例的引用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getObject","attrs":{}},{"type":"text","text":":获取指定实例的类型、内存大小、变量等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getScripts","attrs":{}},{"type":"text","text":":获取所有文件和文件对应id的list。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,具备了以上知识,我们进入下一部分:FDB的具体实现。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB一共分为三类:UI相关、性能优化相关、功能代码相关。下面着重介绍:性能优化相关、Widget拾取、页面代码工具的核心原理与实现。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.性能优化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们知道性能优化的成果,需要依赖数据指标佐证,Flutter虽然提供了Dev Tools和Observatory,来帮助我们开发者采集性能数据,但是操作复杂,上手难度高。比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/development/tools/devtools/memory","title":"","type":null},"content":[{"type":"text","text":"内存检测","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/development/tools/devtools/performance","title":"","type":null},"content":[{"type":"text","text":"帧率检测","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都需要先在Android Studio连接上应用的前提下,在网页上具体操作相应的工具,网页失败次数较高,等待时间较长。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此根据原生优化的经验,我们提供了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"内存信息、内存泄漏检测、帧率检测","attrs":{}},{"type":"text","text":"三个工具,功能如下所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/0206c6c440fe5ca57f77665803caf642.gif","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左图是内存泄漏演示,演示了整个检测的过程以及检测结果。中图是内存信息演示。右图是帧率检测演示。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1. 内存信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"说到内存信息,回忆上面的知识,我们想到的是内存数据一定在VM运行时中,对应我们1.2.1章节的内容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内存信息主要包含:类信息获取和对象信息的获取。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"类信息获取","attrs":{}},{"type":"text","text":":虚拟机的getAllocationProfile协议可以获取线程的所有信息:包含了class、已经使用了多少内存、GC时间戳等。以及虚拟机的Isolate线程对象封装了Library信息。依靠类和Library就可以进行分组。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"对象信息","attrs":{}},{"type":"text","text":":虚拟机的getInstances协议,可以获取某个类下面所有实例的id,通过获取到的实例id和getObject协议,可以查到具体的实例实体:类型、所有的字段、内存大小等。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"获取到虚拟机的原始数据后,通过我们的加工,使展示给用户的内存信息数据","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更简洁,更有用","attrs":{}},{"type":"text","text":":以分组的方式只展示内存中的类信息,并且不仅可查看类信息,还具体到了对象信息、属性信息。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2. 内存泄漏检测","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同Java类似,Dart 语言也具有垃圾回收机制,有垃圾回收就避免不了会内存泄漏。那么如何检测内存泄漏呢?关于内存泄漏检测的核心原理,","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.cn/community/tutorials/memory-leak-monitoring-on-flutter","title":"","type":null},"content":[{"type":"text","text":"这篇文章","attrs":{}}]},{"type":"text","text":"有很好的描述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"总结来说,就是使用弱引用引用待观测对象,并在合适的时机,发起虚拟机GC任务,然后检查弱引用的对象是否为null。如果不为null,说明发生了内存泄漏。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于我们是从页面的维度检查内存泄漏,那么待观测对象就是Flutter页面——Widget、State、Element。那时机就是页面的打开和关闭了,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"页面打开的时候进行观测,页面关闭的时候进行检查","attrs":{}},{"type":"text","text":"。检测流程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17601027419f03fda447a25c7deb8924.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们自定义了Route观察者,把检测的任务封装到自定义的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"NavigatorObserver","attrs":{}},{"type":"text","text":"中,比如didPush、didPop方法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"关于Route观察者,一般的做法是 业务方给自己的MaterialApp的navigatorObservers属性赋值。但是,我们FDB的核心原则是:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更少的侵入业务代码","attrs":{}},{"type":"text","text":",所以我们使用自动监听的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/25de7e264d1085a7c7e8c8aadd2233c4.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上图可知,左侧的方式需要业务方自己设置,而右侧的方式业务方代码不需要任何变动。那么自动方式是如何实现的呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用业务代码的子节点,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"向上查找到业务的Navigator节点,并为此Navigator新增一个路由观察者","attrs":{}},{"type":"text","text":"。这样就解决了兄弟节点无法查找Navigator以及业务代码手动绑定的问题。核心代码如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/171e071fa648a7de825eedfe9ef0594c.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从上面的泄漏演示流程,我们可以看到,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"最终的结果完整的表达出了泄漏链和具体泄漏的代码行数","attrs":{}},{"type":"text","text":",开发者就可以根据具体代码行数,进行修复。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3. 帧率检测","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"帧率检测的数据来自","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SchedulerBinding","attrs":{}},{"type":"text","text":"。Flutter中包含多个Binding,每个Binding都是是Flutter的“胶水粘合剂”,而SchedulerBinding就是粘合了绘制相关的任务,比如调度帧scheduleTask、回调帧_handleBeginFrame、帧时间回调addTimingsCallback等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中addTimingsCallback方法就是关于帧时间的回调,只要有帧被绘制了(setState、动画等刷新),该回调会被执行,给我们的回调数据是数组的FrameTiming。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FrameTiming包含了这一帧的总时长、光栅时长、build时长等等,根据这些信息就可以算出帧率、耗时帧等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/be/be13e2c3537e3f9f983eeb44e637a873.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"5.Widget拾取","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家研发过程中可能会遇到:无法快速定位页面上的Widget在源码中的位置;查看某个Widget的边界范 围,必须依赖IDE;UI、UE走查时无法动态查看文本过长或过短的边界情况。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过UI拾取工具以上问题都可以很好的解决","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是UI拾取工具的功能演示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ad/adb1c01a84550da67917311af40c7541.gif","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从上面的演示,我们可以看到UI拾取工具的基本功能:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"自由拖动拾取器来标记Widget范围;获取Widget的代码文件和行数、文本组件编辑文本等等。细看功能其实能够发现,我们获取的就是某个组件的轮廓、属性及对应源码的位置信息,我们想到的是什么?没错就是WidgetInspector,对应1.2.2和1.2.3章节的内容","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根据上面的内容,该功能的思路如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然Element是Widget和Render的桥梁,那么我们先获取座标对应的Element,然后利用WidgetInspector去获取Widget的轮廓、源码等信息即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从上图看,Element是Widget和Render的桥梁。因此,只要我们找到Element,理顺了三棵树的关系,功能实现就有了突破口。实现过程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1. 找到座标选中的元素。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"层序遍历Element树,比对 拾取座标和 Element持有的Render的范围,最深层级的元素就是选中元素。查找流程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过上图的四步之后,edgeHits数组的第一个元素(最深层级)就是目标Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6ce2e8a89b966cbf65d103e8d7a39f35.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2. 获取所需控件信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WidgetInspectorService的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getSelectedSummaryWidget","attrs":{}},{"type":"text","text":"可以通过told方法返回的id获取对应的Widget信息(如下图所示),包括:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"组件类型、代码路径和行数","attrs":{}},{"type":"text","text":"等等。这两个方法配合使用就可以拿到所需信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8e/8ede08fe67d39cedf937847ffb980335.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的json结构中,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"description字段","attrs":{}},{"type":"text","text":"是Widget类型,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"creationLocation字段","attrs":{}},{"type":"text","text":"是创建Widget的代码位置。有了具体的代码行数,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"即使不是自己的代码,也可以快速定位,快速进入开发","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3. 编辑文本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文本编辑功能是和市面同类产品的创新点,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不仅能看还能编辑","attrs":{}},{"type":"text","text":"。设计的同学非常需要编辑文本功能,因为设计同学不会本地mock数据,数据是什么,走查就只能看到什么,想要查看文本过多或者过少的情况,每次都需要依赖后端同学模拟,所以经常出现走查不彻底、走查成本高的问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从实现的角度来看,该功能较为复杂,原因有:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"无直接入口","attrs":{}},{"type":"text","text":": 拿不到State对象,无法调用业务方的更新方法(setState)。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"影响节点要少","attrs":{}},{"type":"text","text":": 不能直接从根节点就开始更新,造成不必要的损耗。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更新要临时","attrs":{}},{"type":"text","text":": 不能影响真实的代码属性。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鉴于以上原因,实现方法较为巧妙:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"临时生成新文本组件,主动触发Element更新和绘制,只更新选中的Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0a/0ae47642152ae7ee084fe84ea1ef6f76.png","alt":null,"title":"处理的流程图","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左图是处理的流程图,右图是具体的核心代码。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"6.页面代码","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试过程中,我们经常对QA说,“你的包是不是没有我提交的代码啊,你的包不是最新包吧,...”。查看Flutter代码的功能,对解决上面的问题有一定的帮助。工具的效果演示如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6df9880acf54ed5677198bdc56d71c59.gif","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WidgetInspectorService的selection属性可以获取到一个Element,并使用Element绑定的Widget就可以获取文件路径,但是我们的Widget拾取功能、Android Studio等其他调试工具可能会变动到这个值。导致WidgetInspectorService的Element可能不能直接找到当前页面。FDB根据Flutter页面叠加的原理来找到当前的页面,查找方式如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter的根节点是Overlay组件,该组件是一个可以管理Widget的栈。如果将一个Widget插入这个栈上,就可以让此Widget浮在其他的Widget之上。而且这个Overlay组件是被Navigator所创建。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们开发的页面,就这样被一个个的叠加到了Navigator的Overlay上。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"所以,只要能拿到Overlay,页面代码问题就有了突破口","attrs":{}},{"type":"text","text":"。因为Overlay保存了一个个的Route,最顶层的Route就是当前页面的Route。根据Route封装的WidgetBuilder就拿到了当前页面Widget是谁。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再结合上面介绍的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getSelectedSummaryWidget","attrs":{}},{"type":"text","text":"方法,就得到了当前页面的文件名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dart虚拟机的getScripts方法可以获取所有库文件的 Id和文件名,对比文件名获得目标文件的 Id。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Dart虚拟机中眼中,文件也是Object,也可以通过Id进行getObject操作。这样最终就拿到了页面源码。流程如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7e/7ef00400d21c47f9692d781fd130937a.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"7.总结与展望","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介绍了FDB的功能,以及核心工具的实现,相信大家对FDB、Flutter工具建设有了一定的认识。FDB的每一个功能都依赖虚拟机数据,掌握Flutter运行中的每一个节点,是我们能获取所需数据的支撑。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前第一版本的Flutter调试工具已经完成,在贝壳B端已有3个APP接入,接入之初两周时间使用次数已经突破2000。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB项目已开源,后面会根据各业务方及社区内开发者的反馈进行下一步的迭代和调优,以提高大家开发需求和排查问题的效率。同时我们鼓励Flutter社区开发者们参与FDB的共建或者多提些建议、反馈。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"8.参考文献","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们的实现是站在前人的肩膀山续接探索的结果,这里特别感谢一系列开源的作者,是你们为Flutter更好的落地保驾护航。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.flutter-io.cn/packages/flutter_ume","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.dev/packages/dokit","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.dev/packages/leak_detector","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://flutter.cn/community/tutorials/memory-leak-monitoring-on-flutter","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章