前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter调试工具devTools是如何工作的

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

原创
作者头像
brzhang
修改2020-02-20 13:21:14
3.8K1
修改2020-02-20 13:21:14
举报
文章被收录于专栏:玩转全栈玩转全栈

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

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

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

flutter pub global activate devtools

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

代码语言:javascript
复制
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,翻到最后,我们发现了这个。

代码语言:javascript
复制
// Now run the app.
  runApp(
    DevToolsApp(),
  );

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

代码语言:javascript
复制
 /// 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来查看数据,然后看看代码。

代码语言:javascript
复制
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

代码语言:javascript
复制
class InspectorController extends DisposableController
    with AutoDisposeControllerMixin
    implements InspectorServiceClient

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

代码语言:javascript
复制
  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。但是它最终触发他创建的地方在这里:

代码语言:javascript
复制
 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中有这样一个方法:

代码语言:javascript
复制
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,而这个东西实际是:

代码语言:javascript
复制
// 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的实现:

代码语言:javascript
复制
 /// 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框架中的方法了。

代码语言:javascript
复制
@protected
  bool isWidgetTreeReady([ String groupName ]) {
    return WidgetsBinding.instance != null &&
           WidgetsBinding.instance.debugDidSendFirstFrameEvent;
  }

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

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

代码语言:javascript
复制
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>的地址,目次是可以恢复的,就没有必要往下追踪了。

之间使用什么数据互通

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

代码语言:javascript
复制
 /// 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发送触发那边的方法调用。

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

发现是可以对应上的。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 下载源码,自己动手编译,把devTools跑起来
  • 从何处来,到何处去
  • 之间使用什么数据互通
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档