目前西瓜视频作者侧 Flutter 业务场景已经覆盖了 80% (包括视频播放场景),用户侧核心场景包括我的 Tab 也已经是 Flutter,在开发过程中,暴露了一些问题,debug 调试难、离开了 IDE 后犹如抓瞎、PM 设计 QA 验收过程中拿不到有用的信息,在市面上找了一圈,也没有类似 iOS Flex 这样强大的调试工具,例如视图大小、层级的展示,实例对象属性的实时修改,网络请求抓取,log 日志打印,文件查看等,因此西瓜视频 Flutter 基础团队决定开发 UME 以解决上述问题。
UME (读音:油米~) 是一个 Flutter 调试工具包,内部集成了丰富的调试小工具,设计 UI、网络、监控、性能、logger 等,无论是研发、PM、还是 QA 均能使用。
接下来会详细介绍一些核心功能的使用效果以及核心实现
可以查看当前选中 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 的树层级,以及它 renderObject 的详细 build 链。
这个获取到选中 widget 的一个 build 链还是比较简单的,通过 InspectorSelection 获取到当前 currentElement ,然后 使用 debugGetDiagnosticChain 方法就可以获取到整个 build 链了。
RenderObject 的信息也很好得到,通过currentElement 拿到 当前的RenderObject,然后使用 toString方法就可以拿到了。
可以查看到当前页面的页面代码。
主要实现涉及到以下几个关键点:
获取文件名主要利用WidgetInspectorService实现。
而读取脚本主要使用VMService实现。
LeakDetector 用于检测 flutter 内存泄漏,总体的实现思想和 Android 平台的LeakCannary工具类似。利用Expando来弱引用持有待检测对象,并且使用 VMService 拿到泄漏对象的引用链,最终将泄漏信息本地存储并且展示出来。
Dart VM Service Dart 提供的一套 web 服务,数据传输协议是 JSON-RPC 2.0。通过它提供的接口我们能获取到 Dart 虚拟机内部的一些重要信息。下面介绍下整个过程:
由于getInstance(isolateId, classId, limit)方法存在性能和 limit 限制的问题,我们转而利用invoke(isolateId, targetId, selector, argumentIds, disableBreakpoints)方法,借助 Library 顶层函数就可以获取 libraryId 也就是 invoke 方法中的 targetId,最后我们只需要将目标对象暂存一下再通过 invoke 方法取出来就可以拿到该对象的 InstanceRef 了,进而拿到其 id 字段就是我们要找的 objectId 了。
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);
获取到截图后,我们就需要通过移动的位置来获取到图片的当前像素值了,可以通过Image的 getPixelSafe 来获取到 用 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 数据或者查看请求非常麻烦,需要连代理,使用抓包工具才可以进行这些操作,想要简单的在手机上就能完成这些操作,所以网络调试模块目前支持的功能:
看到这,你可能会问这是怎么拦截到所有的网络请求的呢?
这里通过 Dart 在编译时的插桩从而达到对特定 API 的 hook 效果(其实就是替换掉某个方法的实现从而添加自己的实现),由于篇幅问题,这里暂时不展开讲 Hook 的具体流程~ 之后也会有另外的文章来详细说这个。
Flutter 中的所有网络请求走的都是 package:http/src/base_client.dart 中 BaseClient 类中的_sendUnstreamed, 因此,我们只需要 hook _sendUnstreamed 方法便可以拦截到所有的网络请求。
会展示使用 debugprint 函数打印的日志,特别是播放器的一些日志,在没有 IDE 的情况下,查看日志还是很方便的。
拦截 print 有两种方式:
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 的 print 方法里可能会调用 print 来打印日志造成死循环,这里我们只 hook debugPrint 方法,对 package:flutter/src/foundation/print.dart 中 debugPrintThrottled 进行 hook 即可。
可以查看到所有的 channel 调用,包括方法名,时间,参数,返回结果。
hook package:flutter/src/services/platform_channel.dart 中 MethodChannel 类的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 」