UME - 丰富的Flutter调试工具

2024-01-29 06:20
文章标签 工具 调试 flutter 丰富 ume

本文主要是介绍UME - 丰富的Flutter调试工具,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

目前西瓜视频作者侧 Flutter 业务场景已经覆盖了 40多个页面 (包括视频播放场景),用户侧核心场景包括我的 Tab 也已经是 Flutter,在开发过程中,暴露了一些问题,debug 调试难、离开了 IDE 后犹如抓瞎、PM 设计 QA 验收过程中拿不到有用的信息,在市面上找了一圈,也没有类似 iOS Flex 这样强大的调试工具,例如视图大小、层级的展示,实例对象属性的实时修改,网络请求抓取,log 日志打印,文件查看等,所以西瓜视频 Flutter 基础团队决定开发 UME。

介绍

UME (读音:油米~) 是一个 Flutter 调试工具包,内部集成了丰富的调试小工具,设计UI、网络、监控、性能、logger 等,无论是研发、PM、还是 QA 均能使用。

目前已实现的功能

接下来会详细介绍一些核心功能的使用效果以及核心实现:

模块详解

Widget 信息

可以查看当前选中 widget 的大小、名称,文件路径以及代码所在行数,有了这工具,即使你不负责这个功能模块的开发,你也能迅速找到当前代码。

那如何能获取到选中当前 widget 的信息呢,大小通过RenderObject 就能拿到,那 widget 的代码位置呢?通过WidgetInspectorService 中的 getSelectedSummaryWidget 便可以获取到一个json字符串,我们来看下它的结构:

{"description":"Text","type":"_ElementDiagnosticableTreeNode","style":"dense","hasChildren":true,"allowWrap":false,"locationId":0,"creationLocation":{"file":"file:///Users/.../example/lib/home/widgets/category_card.dart","line":69,"column":15,"parameterLocations":[{"file":null,"line":70,"column":24,"name":"data"},... ]},"createdByLocalProject":true,"children":[{"description":"RichText","type":"_ElementDiagnosticableTreeNode","style":"dense","allowWrap":false,"locationId":1,"creationLocation":{"file":"file://../packages/flutter/lib/src/widgets/text.dart","line":425,"column":21,"parameterLocations":[{"file":null,"line":426,"column":7,"name":"textAlign"},...]},"children":[],"widgetRuntimeType":"RichText","stateful":false}],"widgetRuntimeType":"Text","stateful":false
}

由于数据太多了,省略了一部分, 然后根据对应的key即可找到需要的部分。

Widget层级

可以查看当前选中 widget 的树层级,以及它 renderObject 的详细 build 链。

这个获取到选中 widget 的一个 build 链还是比较简单的,通过 InspectorSelection 获取到当前 currentElement ,然后 使用 debugGetDiagnosticChain 方法就可以获取到整个build 链了。

RenderObject 的信息也很好得到,通过currentElement 拿到 当前的RenderObject,然后使用 toString方法就可以拿到了。

ShowCode

可以查看到当前页面的页面代码。

主要实现涉及到以下几个关键点:

  1. 获取到当前页面 widget 所属的文件名。

  2. 根据 dart 脚本的文件名来找到并读取脚本。

获取文件名主要利用WidgetInspectorService实现。

而读取脚本主要使用VMService实现。

获取当前页面widget文件名
  • 我们通过遍历获得当前页面的renderObject列表,按照大小筛选出我们想要的目标 widget。

  • Widget 信息中讲解到过,我们可以通过WidgetInspectorServicegetSelectedSummaryWidget 方法获取到 json 字符串。

  • 提取 "creationLocation" 的值即是当前 widget 的在开发过程中的文件地址。

  • 我们截取出来地址字符串的最后一部分就是当前页面代码所在的文件名了。

找到并读取脚本
  • VMService中的getScripts方法可以获取当前线程下的所有库文件的 ID和文件名。

  • 我们通过比对文件名可以获得目标库文件 id。

  • 通过VMServicegetObject方法可以获取到当前id对应的对象,我们传入刚刚获取的库文件id即可获得这个库对象,读取对象的source属性,里面就是我们的源码了。

内存泄露

LeakDetector 用于检测 flutter 内存泄漏,总体的实现思想和 Android 平台的LeakCannary工具类似。利用Expando来弱引用持有待检测对象,并且使用 VMService 拿到泄漏对象的引用链,最终将泄漏信息本地存储并且展示出来。

Dart VM Service 是 Dart 提供的一套 web 服务,数据传输协议是 JSON-RPC 2.0。通过它提供的接口我们能获取到 Dart 虚拟机内部的一些重要信息。下面介绍下整个过程:

  1. 获取 VMService 服务
  • 获取 ObservatoryUri

    • 通过Service.``getInfo``()获取ServiceProtocolInfo,从中取出serverUri

    • 通过vm_service中的util工具方法convertToWebSocketUrl()将上面的http格式的uri格式转为ws://格式。

    • 获取VmService服务对象, vm_service_io文件中有个vmServiceConnectUri()方法,传入一个observatoryUri就可以获取一个VmService对象。

  1. 获取 isolateId
  • 通过 VmService 的 getVM 方法拿到 VM 对象,VM 对象中存储着所有的IsolateRef。

    • 通过Service.getIsolateID(Isolate.current)拿到,只有 debug 下有效,release 下会返回null。

  1. 获取 libraryId
  • 通过第2步拿到 isolateId 之后,然后调用 VmService 的getIsolate拿到对应的 Isolate 对象。

  • 遍历 Isolate 的 libraries 字段,这是一个 LibraryRef 的 List,然后拿当前 Library 的 uri 去List中匹配LibraryRef的 uri ,就可以获取 LibraryRef 的 id 。

  • 拿着 isolateId 和 LibraryRef 的 id,调用 VmService 的 getObject 方法就可以获取 Library,取其 id 字段就是我们要找的libraryId(其实LibraryRef的 id 应该就是了,实际可以测试)。

  1. 获取 objectId

由于getInstance(isolateId, classId, limit)方法存在性能和limit限制的问题,我们转而利用invoke(isolateId, targetId, selector, argumentIds, disableBreakpoints)方法,借助 Library 顶层函数就可以获取 libraryId 也就是 invoke 方法中的 targetId,最后我们只需要将目标对象暂存一下再通过 invoke 方法取出来就可以拿到该对象的 InstanceRef 了,进而拿到其 id 字段就是我们要找的 objectId 了。

  1. 泄漏判断
  • 通过 getObject(isolateId, objectId) 方法拿到 Expando 的对象的 Obj 实例,它的真实类型其实是一个 Instance。

  • 遍历 Instance 的 fields 字段找到 _data(_data的类型是 ObjRef,可以拿到它对应的 Instance 实例)字段(怎么找_data?可以通过 BoundField 的 FieldRef 字段,然后匹配 FieldRef 的 name 为 ‘_data’),在expando_path.dart中我们可以看到 Expando 的具体实现,_data 字段是一个 List。

  • 遍历 _data 字段,如果都为 null,表明我们观察的 key 对象都释放了;如果元素不为 null,则将该该元素转为 Instance 对象(其实就是一个 WeakProperty),取其 propertyKey 字段就是我们实际的没被回收的对象了。

  1. 获取引用路径
  • VmService 有一个getRetainingPath方法可以直接拿到一个对象的引用链,但是只会拿一条。

  • 需要注意在前面使用 Expando 检测完内存泄漏之后,就释放 Expando 对原始对象的引用。

  • Instance 的 id 会过期,VmService 对它的缓存最大是8192,所以不要保存 id而要保存对象。

  1. 触发GC
  • VmService 有一个getAllocationProfile(isolateId, gc=true)方法,通过它来触发 dart vm 进行 gc,这个也是 Dev Tools 工具上触发 gc 按钮最终调用的方法。据测试触发的都是 FULL GC。

  1. 触发时机
  • Route 检测

    • 借助 framework 提供的NavigatorObserver机制,可以很轻松的监听到页面的进出栈,在 didPop、didRemove、didReplace 方法中触发对route的泄漏检测。

  • Widget/State 检测

    • 一般的页内 Widget/State 不检测,而只检测真正页面对应的 Widget 和State,framework 并没有提供一个全局监听页面销毁的机制。这里我们借助hook_annotation(这个后面会解释)来hook两个点:RouteRootState 的 initState 方法,记录要检测的页面对象;State 的 dispose 方法,如果是我们已记录的页面,则触发检测流程。

内存查看

Memory 可用于查看当前Dart VM 对象所占用情况。

需要拿到 vm 内存的话就必须得依赖 Dart VM ,上文说到,通过 vm_service 就可通过它提供的接口拿到。

通过 Future<MemoryUsage> getMemoryUsage 就能获取到当前 isolate 所占用的信息,来看下 MemoryUsage 的结构,  每个属性都有详细的解释,这里就不再赘述了。

/// The amount of non-Dart memory that is retained by Dart objects. For
/// example, memory associated with Dart objects through APIs such as
/// Dart_NewWeakPersistentHandle and Dart_NewExternalTypedData.  This usage is
/// only as accurate as the values supplied to these APIs from the VM embedder
/// or native extensions. This external memory applies GC pressure, but is
/// separate from heapUsage and heapCapacity.
int externalUsage;/// The total capacity of the heap in bytes. This is the amount of memory used
/// by the Dart heap from the perspective of the operating system.
int heapCapacity;/// The current heap memory usage in bytes. Heap usage is always less than or
/// equal to the heap capacity.
int heapUsage;

那如何获取到每个类对象的内存信息呢?

通过 getAllocationProfile 获取分配对象的信息,通过members属性来获取到每个 class 所占用的堆信息。

对齐标尺

对齐标尺用来测量当前 widget 所在屏幕的一个坐标位置,开启吸附开关后可以自动吸附最近 widget。

标尺显示当前坐标还是非常简单的,通过手势移动的坐标,来改变Positioned的位置即可,并通过屏幕的大小来计算出当前的距离,下面会着重讲一下自动吸附的实现。

要吸附最近的 widget ,就必须找到当前位置的所在的 widget ,然后并画出当前 widget 的一个大小范围,最后设置标尺的位置即可,那么如何找到当前坐标的 widget 呢?

通过globalKey我们可以获取到当前页面的一个RenderObject,然后通过它的debugDescribeChildren 获取到它的所有子节点,然后通过describeApproximatePaintClip获取到当前对象坐标系中的Rect,之后在根据一些坐标转换,判断是不是在当前坐标范围,最后根据RenderObject 的大小做一个排序,这样我们就能知道最小的那个一定是当前坐标位置中最近的 widget 了,得到最近的 widget 之后,我们只需要将标尺的中心位置设置成离 widget 最近的四个角即可。

颜色吸管

可以查看到当前页面任何像素的颜色,方便调试 UI。

这个功能首先分为两步,1、背景放大  2、获取当前像素的颜色值。

如何放大图片

在Flutter中,要想给图片加一些效果,我们可以用到 BackdropFilter,  其实就是加上一层滤镜效果,发现参数其实并不多,通过 ImageFilter就能添加具体的滤镜,想要做一个放大的效果,我们可以使用 ImageFilter.matrix ,它能够放大背景图片, filterQuality 参数可以用来设置放大效果的质量,那如何放大对应的位置以及放大的倍数呢?

通过Matrix4便可以设置,通过我们手势移动的位置,加上 scale 就能计算出它的矩阵参数,并赋值给ImageFilter.matrix就能得到放大效果。

如何获取图片像素及颜色值

在Flutter中想要截图的话就必须借助RepaintBoundary了,配合globalKey我们就能获取当屏幕的当前截图了。

RenderRepaintBoundary boundary = rootKey.currentContext.findRenderObject();
Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
snapshot = img.decodeImage(pngBytes);

获取到截图后,我们就需要通过移动的位置来获取到图片的当前像素值了,可以通过ImagegetPixelSafe 来获取到 用 Uint32 编码过的像素颜色值了(#AABBGGRR),最后我们只需要把abgr转换成 argb 就好了。

int abgrToArgb(int argbColor) {int r = (argbColor >> 16) & 0xFF;int b = argbColor & 0xFF;return (argbColor & 0xFF00FF00) | (b << 16) | r;
}

网络调试

在调试 Flutter 网络的时候,要 mock 数据或者查看请求非常麻烦,需要连代理,使用抓包工具才可以进行这些操作,想要简单的在手机上就能完成这些操作,所以网络调试模块目前支持的功能:

  • 支持所有网络请求抓取

  • 数据支持结构化展示,长按可以复制到剪贴板

  • 收藏请求,单独展示;清空非收藏列表

  • 请求过滤与搜索(支持部分匹配、正则匹配)

  • 请求导出 curl

  • 持久化与导出 HAR

  • mock 响应内容

    • 完整har文件映射

    • 修改单个字段

  • 结构化信息长按复制

看到这,你可能会问这是怎么拦截到所有的网络请求的呢?

这里通过 Dart 在编译时的插桩从而达到对特定 API 的 Hook 效果(其实就是替换掉某个方法的实现从而添加自己的实现),由于篇幅问题,这里暂时不展开讲 Hook 的具体流程,之后也会有另外的文章来详细说这个。

Flutter 中的所有网络请求走的都是 package:http/src/base_client.dartBaseClient 类中的_sendUnstreamed, 因此,我们只需要 hook _sendUnstreamed 方法便可以拦截到所有的网络请求。

Logger

会展示使用 debugprint 函数打印的日志,特别是播放器的一些日志,在没有 IDE 的情况下,查看日志还是很方便的。

拦截 print 有两种方式:

  • Dart 中有一个runZoned方法,可以给执行对象指定一个 Zone,Zone 表示一个代码执行的环境范围,Zone 类似一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如 Zone 中可以捕获日志输出、Timer 创建、微任务调度的行为,同时 Zone 也可以捕获所有未处理的异常。runZoned(...)方法定义:

R runZoned<R>(R body(), {Map zoneValues, ZoneSpecification zoneSpecification,Function onError
}) 
zoneValues: Zone 的私有数据,可以通过实例zone[key]获取

zoneSpecification:Zone 的一些配置,可以自定义一些代码行为,比如拦截日志输出行为等。

这样所有调用 print 方法输出日志的行为都会被拦截。

runZoned(() => runApp(MyApp()), zoneSpecification: new ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {print(line);
}));
  • 通 hook 的方式

由于在 hook 的 print 方法里可能会调用 print 来打印日志造成死循环,这里我们只 hook debugPrint 方法,对 package:flutter/src/foundation/print.dartdebugPrintThrottled 进行 hook 即可。

Channel Monitor

可以查看到所有的 channel 调用,包括方法名,时间,参数,返回结果。


hook package:flutter/src/services/platform_channel.dartMethodChannel 类的invokeMethod方法即可。

目前存在的问题

目前只是完成了初步的版本,很多功能还需要继续完善以及更多的新功能;接下来会从一些细节上继续深入;现在网络调试、channel 监控、Logger这些功能依赖于Hook方案,后续 Hook方案也会考虑开源。

总结

以上介绍了一些 UME 的核心功能以及实现,还有很多丰富的功能由于篇幅问题在这里就不继续展开了,之后还会有更多有趣的东西出现,未来会考虑开源一些核心功能。

加入我们

我们是负责西瓜视频客户端 Flutter 基础技术研发团队。我们在 Flutter 工程,研发工具等方向深耕,支撑业务快速迭代的同时,提高 Flutter 开发调式打包效率。

如果你对技术充满热情,欢迎加入西瓜视频 Flutter 基础技术团队或者西瓜基础业务团队。目前我们在上海、北京、杭州、均有招聘需求,内推可以联系邮箱: tech@bytedance.com  ;邮件标题: 姓名 - 工作年限 - 西瓜 - iOS/Android 

更多分享

一例 Go 编译器代码优化 bug 定位和修复解析

字节跳动破局联邦学习:开源Fedlearner框架,广告投放增效209%

抖音品质建设 - iOS启动优化《原理篇》

iOS 性能优化实践:头条抖音如何实现 OOM 崩溃率下降50%+


欢迎关注「 字节跳动技术团队 」

简历投递联系邮箱「 tech@bytedance.com 」

 点击阅读原文,快来加入我们吧!

这篇关于UME - 丰富的Flutter调试工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/656010

相关文章

SQLite3命令行工具最佳实践指南

《SQLite3命令行工具最佳实践指南》SQLite3是轻量级嵌入式数据库,无需服务器支持,具备ACID事务与跨平台特性,适用于小型项目和学习,sqlite3.exe作为命令行工具,支持SQL执行、数... 目录1. SQLite3简介和特点2. sqlite3.exe使用概述2.1 sqlite3.exe

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

使用jenv工具管理多个JDK版本的方法步骤

《使用jenv工具管理多个JDK版本的方法步骤》jenv是一个开源的Java环境管理工具,旨在帮助开发者在同一台机器上轻松管理和切换多个Java版本,:本文主要介绍使用jenv工具管理多个JD... 目录一、jenv到底是干啥的?二、jenv的核心功能(一)管理多个Java版本(二)支持插件扩展(三)环境隔

Python使用smtplib库开发一个邮件自动发送工具

《Python使用smtplib库开发一个邮件自动发送工具》在现代软件开发中,自动化邮件发送是一个非常实用的功能,无论是系统通知、营销邮件、还是日常工作报告,Python的smtplib库都能帮助我们... 目录代码实现与知识点解析1. 导入必要的库2. 配置邮件服务器参数3. 创建邮件发送类4. 实现邮件

CnPlugin是PL/SQL Developer工具插件使用教程

《CnPlugin是PL/SQLDeveloper工具插件使用教程》:本文主要介绍CnPlugin是PL/SQLDeveloper工具插件使用教程,具有很好的参考价值,希望对大家有所帮助,如有错... 目录PL/SQL Developer工具插件使用安装拷贝文件配置总结PL/SQL Developer工具插

IDEA如何实现远程断点调试jar包

《IDEA如何实现远程断点调试jar包》:本文主要介绍IDEA如何实现远程断点调试jar包的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录问题步骤总结问题以jar包的形式运行Spring Boot项目时报错,但是在IDEA开发环境javascript下编译

Python使用FFmpeg实现高效音频格式转换工具

《Python使用FFmpeg实现高效音频格式转换工具》在数字音频处理领域,音频格式转换是一项基础但至关重要的功能,本文主要为大家介绍了Python如何使用FFmpeg实现强大功能的图形化音频转换工具... 目录概述功能详解软件效果展示主界面布局转换过程截图完成提示开发步骤详解1. 环境准备2. 项目功能结

Linux系统之stress-ng测压工具的使用

《Linux系统之stress-ng测压工具的使用》:本文主要介绍Linux系统之stress-ng测压工具的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、理论1.stress工具简介与安装2.语法及参数3.具体安装二、实验1.运行8 cpu, 4 fo

Maven项目中集成数据库文档生成工具的操作步骤

《Maven项目中集成数据库文档生成工具的操作步骤》在Maven项目中,可以通过集成数据库文档生成工具来自动生成数据库文档,本文为大家整理了使用screw-maven-plugin(推荐)的完... 目录1. 添加插件配置到 pom.XML2. 配置数据库信息3. 执行生成命令4. 高级配置选项5. 注意事

Python使用pynput模拟实现键盘自动输入工具

《Python使用pynput模拟实现键盘自动输入工具》在日常办公和软件开发中,我们经常需要处理大量重复的文本输入工作,所以本文就来和大家介绍一款使用Python的PyQt5库结合pynput键盘控制... 目录概述:当自动化遇上可视化功能全景图核心功能矩阵技术栈深度效果展示使用教程四步操作指南核心代码解析