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

相关文章

高效录音转文字:2024年四大工具精选!

在快节奏的工作生活中,能够快速将录音转换成文字是一项非常实用的能力。特别是在需要记录会议纪要、讲座内容或者是采访素材的时候,一款优秀的在线录音转文字工具能派上大用场。以下推荐几个好用的录音转文字工具! 365在线转文字 直达链接:https://www.pdf365.cn/ 365在线转文字是一款提供在线录音转文字服务的工具,它以其高效、便捷的特点受到用户的青睐。用户无需下载安装任何软件,只

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

vscode中文乱码问题,注释,终端,调试乱码一劳永逸版

忘记咋回事突然出现了乱码问题,很多方法都试了,注释乱码解决了,终端又乱码,调试窗口也乱码,最后经过本人不懈努力,终于全部解决了,现在分享给大家我的方法。 乱码的原因是各个地方用的编码格式不统一,所以把他们设成统一的utf8. 1.电脑的编码格式 开始-设置-时间和语言-语言和区域 管理语言设置-更改系统区域设置-勾选Bata版:使用utf8-确定-然后按指示重启 2.vscode

超强的截图工具:PixPin

你是否还在为寻找一款功能强大、操作简便的截图工具而烦恼?市面上那么多工具,常常让人无从选择。今天,想给大家安利一款神器——PixPin,一款真正解放双手的截图工具。 想象一下,你只需要按下快捷键就能轻松完成多种截图任务,还能快速编辑、标注甚至保存多种格式的图片。这款工具能满足这些需求吗? PixPin不仅支持全屏、窗口、区域截图等基础功能,它还可以进行延时截图,让你捕捉到每个关键画面。不仅如此

PR曲线——一个更敏感的性能评估工具

在不均衡数据集的情况下,精确率-召回率(Precision-Recall, PR)曲线是一种非常有用的工具,因为它提供了比传统的ROC曲线更准确的性能评估。以下是PR曲线在不均衡数据情况下的一些作用: 关注少数类:在不均衡数据集中,少数类的样本数量远少于多数类。PR曲线通过关注少数类(通常是正类)的性能来弥补这一点,因为它直接评估模型在识别正类方面的能力。 精确率与召回率的平衡:精确率(Pr