iOSFlex核心功能的使用效果以及核心实现模块详解(一)

背景

目前西瓜视频作者端的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的位置,根据屏幕大小计算当前距离。下面将重点介绍自动吸附的实现。

图片[1]-iOSFlex核心功能的使用效果以及核心实现模块详解(一)-老王博客

要吸收最近的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。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论