专栏首页玩转全栈Flutter调试工具devTools是如何工作的
原创

Flutter调试工具devTools是如何工作的

Flutter的devTools是flutter中开发不可或缺的一个工具。

常用的功能就有性能调优布局查看函数调用栈等。

安装这个工具可以直接在命令行下执行,用命令行安装是一个比较好的习惯:

flutter pub global activate devtools

然后,这不,你就会安装一下这些依赖库,如是,就可以对这个devtools的原理进行一个初步的分析。

Package devtools is currently active at version 0.1.14.
Resolving dependencies...
+ args 1.5.2
+ async 2.4.0
+ browser_launcher 0.1.5
+ charcode 1.1.3
+ collection 1.14.12
+ convert 2.1.1
+ crypto 2.1.4
+ devtools 0.1.15
+ devtools_server 0.1.14
+ devtools_shared 0.2.0
+ http 0.12.0+4
+ http_multi_server 2.2.0
+ http_parser 3.1.3
+ intl 0.16.1
+ logging 0.11.4
+ meta 1.1.8
+ mime 0.9.6+3
+ path 1.6.4
+ pedantic 1.9.0
+ shelf 0.7.5
+ shelf_static 0.2.8
+ source_span 1.6.0
+ sse 3.1.2
+ stack_trace 1.9.3
+ stream_channel 2.0.0
+ string_scanner 1.0.5
+ term_glyph 1.1.0
+ typed_data 1.1.6
+ usage 3.4.1
+ uuid 2.0.4
+ vm_service 2.3.1
+ webkit_inspection_protocol 0.5.0
Downloading devtools 0.1.15...

从这些依赖库中,我们发现有以下三个库,也是最值得我们关注的。

  • devtools 0.1.15
  • devtools_server 0.1.14
  • devtools_shared 0.2.0

本文的主要目的是了解清楚devtools是如何从app中拿到数据并且将数据展示给用户的。

下载源码,自己动手编译,把devTools跑起来

要了解这个工具的原理,最好的办法就是下载他的源码,调试它

  • git clone https://github.com/flutter/devtools
  • cd devtools/packages/devtools_app
  • flutter pub get

以上源码就把源码下载好,而且相关库都准备好了,应该可以可以开车了。

1、随便找一个flutter的项目,把他跑起来,用做我们debug的数据源,都说这个调试工具要采集数据的,那数据当然是从一个flutter项目来啊。

2、运行这个项目

  • cd devtools/packages/devtools_app
  • alias build_runner="flutter pub run build_runner"
  • build_runner serve web

3、你就能够看到这个界面了

需要我们输入一个url,其实就是http://127.0.0.1:49288/GG5v1Ot9kKQ=类似这样的一个鬼东西,莫要惊慌失措,这个会在你跑你flutter项目的时候在日志中给出,一定会有,没有你找我。

把url填入进去,连接,就可以看到这个界面了:

从何处来,到何处去

既然已经跑起来了,那么,入口在哪里,很显然,我们发现devtools既然是一个用dart写的项目,那么或许会有一个main.dart,果不其然,在devtools_app/lib下面就找到了main.dart,翻到最后,我们发现了这个。

// Now run the app.
  runApp(
    DevToolsApp(),
  );

继续跟踪,还是顶一个目标呢?要不,我们就看看Flutter Inspector是如何把我们 flutter app的树结构显示到devTools上的把,随着深挖下去,我们在app.dart中找到这样一段代码

 /// The routes that the app exposes.
  final Map<String, UrlParametersBuilder> _routes = {
    '/': (_, params) => Initializer(
          url: params['uri'],
          builder: (_) => DevToolsScaffold(
            tabs: const [
              InspectorScreen(),
              TimelineScreen(),
              MemoryScreen(),
              PerformanceScreen(),
              // TODO(https://github.com/flutter/flutter/issues/43783): Put back
              // the debugger screen.
              if (showNetworkPage)
                NetworkScreen(),
              LoggingScreen(),
              InfoScreen(),
            ],

很显然这个排布就够就是我们devtools上面的tab

因此,我们毫不犹豫的点进InspectorScreen这个类中去一看究竟。我们看到他的initState方法中有一个_handleConnectionStart,从名字上看应该是开始连接之后干些啥事,不急,我们先猜一下,我猜,会启动一个service来收集数据,然后启动一个client来查看数据,然后看看代码。

setState(() {
      inspectorController?.dispose();
      summaryTreeController = InspectorTreeControllerFlutter();
      detailsTreeController = InspectorTreeControllerFlutter();
      inspectorController = InspectorController(
        inspectorTree: summaryTreeController,
        detailsTree: detailsTreeController,
        inspectorService: inspectorService,
        treeType: FlutterTreeType.widget,
        onExpandCollapseSupported: _onExpandCollapseSupported,
        onLayoutExplorerSupported: _onLayoutExplorerSupported,
      );

我们这里只看到了一个inspectorService,不急,跟到InspectorController里面瞄一瞄。结果我们发现这货其实就是实现了InspectorServiceClient

class InspectorController extends DisposableController
    with AutoDisposeControllerMixin
    implements InspectorServiceClient

所以,很显然,这种就是cs架构无疑了。然后,我们深入看一看这个InspectorService,这货肯定就是采集数据的了。然后他是如何创建的,以下是创建它的方法

  static Future<InspectorService> create(VmService vmService) async {
    assert(_inspectorDependenciesLoaded);
    assert(serviceManager.hasConnection);
    assert(serviceManager.service != null);
    final inspectorLibrary = EvalOnDartLibrary(
      inspectorLibraryUriCandidates,
      vmService,
    );

    final libraryRef = await inspectorLibrary.libraryRef.catchError(
      (_) => throw FlutterInspectorLibraryNotFound(),
      test: (e) => e is LibraryNotFound,
    );
    final libraryFuture = inspectorLibrary.getLibrary(libraryRef, null);
    final library = await libraryFuture;
    Future<Set<String>> lookupFunctionNames() async {
      for (ClassRef classRef in library.classes) {
        if ('WidgetInspectorService' == classRef.name) {
          final classObj = await inspectorLibrary.getClass(classRef, null);
          final functionNames = <String>{};
          for (FuncRef funcRef in classObj.functions) {
            functionNames.add(funcRef.name);
          }
          return functionNames;
        }
      }
      // WidgetInspectorService is not available. Either this is not a Flutter
      // application or it is running in profile mode.
      return null;
    }

    final supportedServiceMethods = await lookupFunctionNames();
    if (supportedServiceMethods == null) return null;
    return InspectorService(
      vmService,
      inspectorLibrary,
      supportedServiceMethods,
    );
  }

这里,接收一个vm参数,这个参数是哪里来的呢,他是来自一个全局的servermanger,叫做ServiceConnectionManager。但是它最终触发他创建的地方在这里:

 Future<void> _attemptUrlConnection() async {
    final uri = normalizeVmServiceUri(widget.url);
    final connected = await FrameworkCore.initVmService(
      '',
      explicitUri: uri,
      errorReporter: (message, error) =>
          Notifications.of(context).push('$message, $error'),
    );
    if (!connected) {
      _navigateToConnectPage();
    }
  }

你应该还记得你填入的那个进入调试页面,然后填了一个url,回车,没错,就是在这个时候initVmService的。

service创建好了,不是用来放着供着的,那是要干活的。我们主要到controller中有这样一个方法:

Future<void> maybeLoadUI() async {
    if (!visibleToUser || !isActive) {
      return;
    }

    if (flutterAppFrameReady) {
      // We need to start by querying the inspector service to find out the
      // current state of the UI.
      await inspectorService.inferPubRootDirectoryIfNeeded();
      await updateSelectionFromService(firstFrame: true);
    } else {
      final ready = await inspectorService.isWidgetTreeReady();
      flutterAppFrameReady = ready;
      if (isActive && ready) {
        await maybeLoadUI();
      }
    }
  }

而这个方法的调用时机就是在 onFlutterFrame就是第一帧绘制好的时候,代码就不细贴了,然后,我们注意到有这样一个调用

await inspectorService.isWidgetTreeReady();

那么这个Service肯定是去问我们那个app是否应准备好了,那么,他的源码在哪里呢?我看到前面InspectorService创建的时候时候,有一个参数是inspectorLibraryUriCandidates,而这个东西实际是:

// TODO(jacobr): remove flutter_web entry once flutter_web and flutter are
// unified.
const inspectorLibraryUriCandidates = [
  'package:flutter/src/widgets/widget_inspector.dart',
  'package:flutter_web/src/widgets/widget_inspector.dart',
];

稍微追踪一下代码,就能够发现isWidgetTreeReady,就是去问package:flutter/src/widgets/widget_inspector.dart这个类中的方法。然后我们看一看isWidgetTreeReady的实现:

 /// If the widget tree is not ready, the application should wait for the next
  /// Flutter.Frame event before attempting to display the widget tree. If the
  /// application is ready, the next Flutter.Frame event may never come as no
  /// new frames will be triggered to draw unless something changes in the UI.
  Future<bool> isWidgetTreeReady() {
    return invokeBoolServiceMethodNoArgs('isWidgetTreeReady');
  }

很加单,就是一个方法调用,这应该就是调用flutter框架中的方法了。

@protected
  bool isWidgetTreeReady([ String groupName ]) {
    return WidgetsBinding.instance != null &&
           WidgetsBinding.instance.debugDidSendFirstFrameEvent;
  }

实际上还不是直接调这个方法,还经过了一个映射,最后映射到这个方法上来了。

所以,我们要去第一帧的数据的化,那么,我们就要去看看maybeLoadUI这个方法中这个调用了。inspectorService.inferPubRootDirectoryIfNeeded()

Future<String> inferPubRootDirectoryIfNeeded() async {
    final group = createObjectGroup('temp');
    final root = await group.getRoot(FlutterTreeType.widget);

    if (root == null) {
      // No need to do anything as there isn't a valid tree (yet?).
      await group.dispose();
      return null;
    }
    final children = await root.children;
    if (children?.isNotEmpty == true) {
      // There are already widgets identified as being from the summary tree so
      // no need to guess the pub root directory.
      return null;
    }

    final List<RemoteDiagnosticsNode> allChildren =
        await group.getChildren(root.dartDiagnosticRef, false, null);
    final path = allChildren.first.creationLocation?.path;
    if (path == null) {
      await group.dispose();
      return null;
    }

    // this directory rather than guessing based on url structure.
    final parts = path.split('/');
    String pubRootDirectory;
    for (int i = parts.length - 1; i >= 0; i--) {
      String part;
      if (part == 'lib' || part == 'web') {
        pubRootDirectory = parts.sublist(0, i).join('/');
        break;
      }

      if (part == 'packages') {
        pubRootDirectory = parts.sublist(0, i + 1).join('/');
        break;
      }
    }
    pubRootDirectory ??= (parts..removeLast()).join('/');

    await setPubRootDirectories([pubRootDirectory]);
    await group.dispose();
    return pubRootDirectory;
  }

所以,至此就拿到了flutter页渲染的那个树,返回的信息是一个string,其实是存放那个树对应的List<RemoteDiagnosticsNode>的地址,目次是可以恢复的,就没有必要往下追踪了。

之间使用什么数据互通

通过具体的方法,我们可以看到:

 /// Returns a JSON representation of the subtree rooted at the
  /// [DiagnosticsNode] object that `diagnosticsNodeId` references providing
  /// information needed for the details subtree view.
  ///
  /// The number of levels of the subtree that should be returned is specified
  /// by the [subtreeDepth] parameter. This value defaults to 2 for backwards
  /// compatibility.
  ///
  /// See also:
  ///
  ///  * [getChildrenDetailsSubtree], a method to get children of a node
  ///    in the details subtree.
  String getDetailsSubtree(
    String id,
    String groupName, {
    int subtreeDepth = 2,
  }) {
    return _safeJsonEncode(_getDetailsSubtree( id, groupName, subtreeDepth));
  }

最终方法的调用将会回调会一个json数据,举个例子,大概是:

然后更具这些信息,devTools上呈现出树状接口的ui,然后devTools其实还可以反过来控制app上显示debug标志等其他操作,其实这都是通过service发送触发那边的方法调用。

下图是我验证了一下,这些数据是否和工具展示的对得上,验证结果是可以对上的:

发现是可以对应上的。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • flutter插件开发需要了解的EventChannel与MethodChannel

    在flutter插件开发中,EventChannel与MethodChannel是两个不可避免的类。我们要了解它,最好先记住它通常用来干嘛。

    brzhang
  • 现有项目集成flutter排坑指南

    1、如果选择,stable,我们遇到的情况是,IOS上接入之后是跑不了的。切到master上就OK了。

    brzhang
  • 移动端性能数搜集及上报系统

    github项目地址https://github.com/bravekingzhang/statis-report-framwork-android

    brzhang
  • Codeforce 712A Memory and Crow

    A. Memory and Crow time limit per test:2 seconds memory limit per test:256 megab...

    Angel_Kitty
  • Java 使用 QQ 实现第三方登录

    个人网站最近增加了评论功能,为了方便用户不用注册就可以评论,对接了 QQ 的一键登录,总的来说其实都挺简单的,可能会有一点小坑,但不算多,完整记录下来方便后来人...

    江南一点雨
  • 让Visio2007/2003支持UML2.2

    Visio2007虽然不错,但画UML图总觉得支持不是很完美。在这里,可以通过安装模板包的方式,让它支持UML最新版本2.2。

    williamwong
  • HDUOJ---What Are You Talking About

    What Are You Talking About Time Limit: 10000/5000 MS (Java/Others)    Memory Lim...

    Gxjun
  • [享学Netflix] 三十六、Hystrix请求命令:HystrixCommand和HystrixObservableCommand

    Hystrix内部使用了大量的RxJava代码来书写,使得把其代码精简到了极致,性能也提升了很多。虽说Hystrix的源代码难啃,但是它面向使用者提供的API是...

    YourBatman
  • DAY65:阅读Device-Side Kernel Launch

    我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第65天,我们正在讲解编程接口,希望在接下来的35天里,您可以学习到原汁...

    GPUS Lady
  • 数据资源常识(3.6)数据标准(Data Standards)

    三、行业数据资源概念(Industry Data Resources Concept)

    秦陇纪

扫码关注云+社区

领取腾讯云代金券