
背景
目前西瓜视频作者端的Flutter业务场景已经覆盖了80%(包括视频播放场景),用户端的核心场景包括我的Tab已经是Flutter了。开发过程中暴露了一些问题,调试困难,离开IDE后感觉自己瞎了眼,在PM设计QA验收过程中得不到有用的信息。搜了一下市场,没有像iOS Flex这样强大的调试工具,比如视图大小、关卡显示、实例对象属性等,所以西瓜视频Flutter基础团队决定开发UME来解决以上问题。
简介
UME(读音:油米~)是一个Flutter调试工具包,集成了丰富的调试小工具,设计UI、网络、监控、性能、记录器等,无论是研发、PM还是QA都可以用过。
当前实现的功能
接下来详细介绍一些核心功能的使用效果和核心实现
模块详细信息小部件信息
您可以查看当前所选小部件的大小、名称、文件路径和代码行数。有了这个工具,即使你不负责这个功能模块的开发,也可以快速找到当前的代码。
如何获取当前所选小部件的信息?大小可以通过RenderObject获取。小部件的代码位置呢?可以通过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
}
因为数据太多,省略了一部分b本地调试工具,然后根据对应的key就可以找到需要的部分了。
小部件级别
您可以查看当前选中的widget的树级及其renderObject的详细构建链。
获取所选小部件的构建链相对简单。通过InspectorSelection获取当前currentElement,然后使用debugGetDiagnosticChain方法获取整个构建链。
RenderObject的信息也很容易获取,通过currentElement获取当前的RenderObject,然后使用toString方法获取。
显示代码
可以查看当前页面的页面代码。
主要实现涉及以下几个关键点:
获取当前页面小部件的文件名,根据dart脚本的文件名查找并读取脚本
获取文件名主要使用WidgetInspectorService实现。
读取脚本主要使用VMService实现。
获取当前页面小部件文件名查找并读取脚本内存泄漏
LeakDetector 用于检测 Flutter 内存泄漏。整体实现思路类似于Android平台上的LeakCannary工具。使用Expando对待检测对象进行弱引用,使用VMService获取泄漏对象的引用链,最后在本地存储并显示泄漏信息。
Dart VM Service Dart 提供的一组 Web 服务,数据传输协议为 JSON-RPC 2.0。通过它提供的接口,我们可以得到 Dart 虚拟机内部的一些重要信息。整个过程描述如下:
获取VMService获取isolateId获取libraryId获取objectId
由于getInstance(isolateId, classId, limit)方法的性能和限制限制,我们求助于invoke(isolateId, targetId, selector, argumentIds, disableBreakpoints)方法,libraryId可以借助库顶层函数,也就是invoke方法中的targetId,最后我们只需要临时存储目标对象,然后通过invoke方法取出来获取对象的InstanceRef,然后得到它的id字段为我们正在寻找的 objectId。
泄漏判断获取参考路径触发GC触发时序内存视图
内存可用于查看 Dart VM 对象的当前占用情况。
如果需要获取 vm 内存,则必须依赖 Dart VM。如上所述,你可以通过它通过vm_service提供的接口来获取它。
当前isolate占用的信息可以通过Future getMemoryUsage获取。我们来看看 MemoryUsage 的结构。每个属性都有详细的解释b本地调试工具,这里不再赘述。
/// 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属性获取各个类占用的堆信息。
对齐标尺
对齐标尺用于测量当前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来设置,它的矩阵参数可以通过我们手势移动的位置,加上比例来计算,赋值给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);
得到截图后,我们需要通过移动位置来获取图片当前的像素值。我们可以通过Image的getPixelSafe(#AABBGGRR)得到用Uint32编码的像素颜色值,最后只需要将abgr转为argb即可。
int abgrToArgb(int argbColor) {
int r = (argbColor >> 16) & 0xFF;
int b = argbColor & 0xFF;
return (argbColor & 0xFF00FF00) | (b << 16) | r;
}
网络调试
在调试Flutter网络的时候,mock数据或者查看请求是很麻烦的。您需要连接到代理并使用数据包捕获工具来执行这些操作。你想简单的在手机上完成这些操作,所以网络调试模块目前支持的功能:
看到这里,你可能会问,这是怎么拦截所有网络请求的呢?
这里,Dart在编译时的instrumentation是用来实现特定API的hook效果(其实就是替换一个方法的实现添加自己的实现)。由于篇幅问题,我们暂时不展开Hook的具体流程。 ~ 后面会有另一篇文章详细阐述。
Flutter 中所有的网络请求都要经过 package:http/src/base_client.dart 中 BaseClient 类中的 _sendUnstreamed,所以我们只需要 hook _sendUnstreamed 方法就可以拦截所有的网络请求。
记录器
debugprint函数打印的日志会显示出来,尤其是播放器的一些日志。在没有IDE的情况下查看日志非常方便。
截取打印有两种方式:
R runZoned(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的print方法中调用print打印日志导致死循环,这里我们只hook debugPrint方法,并hook package:flutter/src/foundation/print.dart中的debugPrintThrottled。
频道监控
您可以查看所有通道调用,包括方法名称、时间、参数和返回结果。
可以使用hook包中MethodChannel类的invokeMethod方法:flutter/src/services/platform_channel.dart。
当前问题
目前只完成了初步版本,还有很多功能需要进一步完善和更多新功能;接下来,我们将继续深入一些细节;现在网络调试、通道监控、Logger 功能都依赖于 Hook 方案。 hook 方案也会考虑开源。
总结
以上介绍了UME的一些核心功能和实现。有很多丰富的功能,由于篇幅问题这里就不展开了。未来会有更多有趣的东西,一些核心功能未来会考虑开源。
加入我们
我们是负责西瓜视频客户端Flutter基础技术的研发团队。我们在 Flutter 工程、研发工具等方面深耕,同时支持业务快速迭代,同时提升 Flutter 开发和打包效率。
如果您对技术充满热情,欢迎加入 Flutter 基础技术团队或西瓜基础业务团队。目前我们在上海、北京、杭州均有招聘需求。内部推荐可联系邮箱:tech@bytedance.com;邮件主题:姓名 - 工作年限 - 西瓜 - iOS/Android。
请登录后发表评论
注册
社交帐号登录