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是如何从app中拿到数据并且将数据展示给用户的。
要了解这个工具的原理,最好的办法就是下载他的源码,调试它
:
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发送触发那边的方法调用。
下图是我验证了一下,这些数据是否和工具展示的对得上,验证结果是可以对上的:
发现是可以对应上的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。