flutter attach時候經(jīng)常出現(xiàn)下面這種錯誤:
Rerun this command with one of the following passed in as the appId:
flutter attach --app-id com.test.1
flutter attach --app-id com.test.1 (2)
flutter attach --app-id com.test.1 (3)


通過在運行時添加參數(shù)就能解決此問題。
基于此探索一下與flutter attach相關(guān)的內(nèi)容。
Flutter是一個跨平臺的移動應用程序開發(fā)框架,F(xiàn)lutter attach是Flutter命令行工具提供的一個命令,用于將開發(fā)者的編輯器(如VSCode、Android Studio)連接到正在運行的Flutter應用程序,以便于進行調(diào)試。Flutter attach的原理是利用了Dart VM的一個調(diào)試協(xié)議——VM服務協(xié)議,它允許開發(fā)者以REST風格的API與Dart VM進行通信。
Flutter attach的連接流程可以大致分為以下幾步:
啟動Flutter應用程序:開發(fā)者使用Flutter run命令啟動Flutter應用程序,該命令將啟動Dart VM并加載應用程序代碼。
啟用VM服務:Dart VM支持一個VM服務,用于向外部應用程序提供調(diào)試和診斷功能。Flutter run命令會自動啟用VM服務,并監(jiān)聽一個默認的端口號(默認為“8181”)。
連接編輯器:開發(fā)者使用Flutter attach命令連接編輯器。Flutter attach命令會嘗試連接到運行中的Flutter應用程序的VM服務,連接成功后,將在編輯器中打開一個調(diào)試會話。
交互調(diào)試:在編輯器中,開發(fā)者可以設置斷點、單步執(zhí)行代碼、查看變量等,通過與Dart VM服務的交互進行調(diào)試。
需要注意的是,F(xiàn)lutter attach命令要求開發(fā)者在啟動Flutter應用程序時啟用了VM服務。如果在啟動應用程序時未啟用VM服務,則無法使用Flutter attach命令進行連接。此外,F(xiàn)lutter attach命令還要求運行中的Flutter應用程序與編輯器在同一臺計算機上,或者在通過網(wǎng)絡進行通信時,必須通過安全的通道進行連接。
另外flutter attach 命令需要 flutter 應用程序?qū)脑创a,否則報錯:
Target file "lib/main.dart" not found.
因為需要熱重載和熱重啟時,需要比對源代碼的修改,做出文件同步,這是可以理解的。
1、attach
連接到 Flutter 應用程序并啟動開發(fā)工具和調(diào)試服務
attach-》 _attachToDevice-》getObservatoryUri-》 _client.start(); -》
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance = false,
bool enableDevTools = false,
}) async {
_didAttach = true;
try {
await connectToServiceProtocol(
reloadSources: _reloadSourcesService,
restart: _restartService,
compileExpression: _compileExpressionService,
getSkSLMethod: writeSkSL,
allowExistingDdsInstance: allowExistingDdsInstance,
);
// Catches all exceptions, non-Exception objects are rethrown.
} catch (error) { // ignore: avoid_catches_without_on_clauses
if (error is! Exception && error is! String) {
rethrow;
}
globals.printError('Error connecting to the service protocol: $error');
return 2;
}
if (enableDevTools) {
// The method below is guaranteed never to return a failing future.
unawaited(residentDevtoolsHandler.serveAndAnnounceDevTools(
devToolsServerAddress: debuggingOptions.devToolsServerAddress,
flutterDevices: flutterDevices,
));
}
for (final FlutterDevice device in flutterDevices) {
await device.initLogReader();
}
try {
final List<Uri> baseUris = await _initDevFS();
if (connectionInfoCompleter != null) {
// Only handle one debugger connection.
connectionInfoCompleter.complete(
DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress,
baseUri: baseUris.first.toString(),
),
);
}
} on DevFSException catch (error) {
globals.printError('Error initializing DevFS: $error');
return 3;
}
final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
_addBenchmarkData(
'hotReloadInitialDevFSSyncMilliseconds',
initialUpdateDevFSsTimer.elapsed.inMilliseconds,
);
if (!devfsResult.success) {
return 3;
}
for (final FlutterDevice device in flutterDevices) {
// VM must have accepted the kernel binary, there will be no reload
// report, so we let incremental compiler know that source code was accepted.
if (device.generator != null) {
device.generator.accept();
}
final List<FlutterView> views = await device.vmService.getFlutterViews();
for (final FlutterView view in views) {
globals.printTrace('Connected to $view.');
}
}
// In fast-start mode, apps are initialized from a placeholder splashscreen
// app. We must do a restart here to load the program and assets for the
// real app.
if (debuggingOptions.fastStart) {
await restart(
fullRestart: true,
reason: 'restart',
silent: true,
);
}
appStartedCompleter?.complete();
if (benchmarkMode) {
// Wait multiple seconds for the isolate to have fully started.
await Future<void>.delayed(const Duration(seconds: 10));
// We are running in benchmark mode.
globals.printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
globals.printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
// Wait multiple seconds to stabilize benchmark on slower device lab hardware.
// Hot restart finishes when the new isolate is started, not when the new isolate
// is ready. This process can actually take multiple seconds.
await Future<void>.delayed(const Duration(seconds: 10));
globals.printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart();
if (stayResident) {
await waitForAppToFinish();
} else {
globals.printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await exitApp();
}
final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
return 0;
}
writeVmServiceFile();
int result = 0;
if (stayResident) {
result = await waitForAppToFinish();
}
await cleanupAtFinish();
return result;
}
上面的代碼是 Flutter 開發(fā)框架中的一個函數(shù),它在調(diào)試模式下連接到 Flutter 應用程序并啟動開發(fā)工具和調(diào)試服務。它有幾個參數(shù),用于控制連接和初始化的行為。
函數(shù)首先將 _didAttach 標記設置為 true,以表示已經(jīng)連接到調(diào)試服務。然后,它通過調(diào)用 connectToServiceProtocol 函數(shù)來連接到服務協(xié)議,并通過傳遞幾個服務對象來注冊服務。
如果連接過程中出現(xiàn)錯誤,則函數(shù)會打印錯誤消息并返回 2。
如果 enableDevTools 參數(shù)設置為 true,則函數(shù)會啟動開發(fā)工具,并在開發(fā)工具服務器地址上向客戶端廣播 DevTools 的可用性。
接下來,函數(shù)將對每個 Flutter 設備調(diào)用 initLogReader 方法以初始化日志讀取器。然后,它將調(diào)用 _initDevFS 方法來初始化開發(fā)文件系統(tǒng)(DevFS)并獲取基本 URI。如果 connectionInfoCompleter 參數(shù)不為空,則函數(shù)將使用第一個 Flutter 設備的 VM 服務地址和基本 URI 完成 DebugConnectionInfo 對象。
如果在初始化 DevFS 過程中出現(xiàn)錯誤,則函數(shù)會打印錯誤消息并返回 3。
接下來,函數(shù)將調(diào)用 _updateDevFS 方法來更新開發(fā)文件系統(tǒng),并將 fullRestart 參數(shù)設置為 true。如果更新失敗,則函數(shù)將返回 3。
然后,函數(shù)將對每個 Flutter 設備調(diào)用 getFlutterViews 方法以獲取 Flutter 視圖,并打印連接成功的消息。
如果 debuggingOptions.fastStart 參數(shù)設置為 true,則函數(shù)將調(diào)用 restart 方法以進行全面重啟,并在靜默模式下重新啟動應用程序。
如果 benchmarkMode 參數(shù)設置為 true,則函數(shù)將測量性能并記錄測試結(jié)果。首先,函數(shù)將等待 10 秒鐘以確保隔離環(huán)境完全啟動。然后,函數(shù)將打印開始基準測試的消息,并調(diào)用 restart 方法以進行全面重啟。然后,函數(shù)將再次等待 10 秒鐘,以穩(wěn)定基準測試結(jié)果。接下來,函數(shù)將打印開始基準測試熱重載的消息,并調(diào)用 restart 方法以進行熱重載。如果 stayResident 參數(shù)設置為 true,則函數(shù)將等待應用程序運行完成,否則函數(shù)將清理 DevFS、停止日志記錄并退出應用程序。最后,函數(shù)將使用 toPrettyJson 函數(shù)將基準測試結(jié)果寫入文件,并返回 0。
最后,函數(shù)將調(diào)用 writeVmServiceFile 方法以將 VM 服務地址寫入文件。如果 stayResident 參數(shù)設置為 true,則函數(shù)將調(diào)用 waitForAppToFinish 方法并返回其結(jié)果。否則,函數(shù)將調(diào)用 cleanupAtFinish 方法以清理資源,并返回 0。
2、_attachToDevice
Future<void> _attachToDevice(Device device) async {
final FlutterProject flutterProject = FlutterProject.current();
final Daemon daemon = boolArg('machine')
? Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
logger: globals.logger,
),
notifyingLogger: (globals.logger is NotifyingLogger)
? globals.logger as NotifyingLogger
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
logToStdout: true,
)
: null;
Stream<Uri> observatoryUri;
bool usesIpv6 = ipv6;
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
if (debugPort == null && debugUri == null) {
if (device is FuchsiaDevice) {
final String module = stringArg('module');
if (module == null) {
throwToolExit("'--module' is required for attaching to a Fuchsia device");
}
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
} on Exception {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
rethrow;
}
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
final Uri uriFromMdns =
await MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
);
observatoryUri = uriFromMdns == null
? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
}
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) {
final ProtocolDiscovery observatoryDiscovery =
ProtocolDiscovery.observatory(
// If it's an Android device, attaching relies on past log searching
// to find the service protocol.
await device.getLogReader(includePastLogs: device is AndroidDevice),
portForwarder: device.portForwarder,
ipv6: ipv6,
devicePort: deviceVmservicePort,
hostPort: hostVmservicePort,
logger: globals.logger,
);
globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
observatoryUri = observatoryDiscovery.uris;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
}
} else {
observatoryUri = Stream<Uri>
.fromFuture(
buildObservatoryUri(
device,
debugUri?.host ?? hostname,
debugPort ?? debugUri.port,
hostVmservicePort,
debugUri?.path,
)
).asBroadcastStream();
}
globals.terminal.usesTerminalUi = daemon == null;
try {
int result;
if (daemon != null) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
AppInstance app;
try {
app = await daemon.appDomain.launch(
runner,
({Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter}) {
return runner.attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
},
device,
null,
true,
globals.fs.currentDirectory,
LaunchMode.attach,
globals.logger as AppRunLogger,
);
} on Exception catch (error) {
throwToolExit(error.toString());
}
result = await app.runner.waitForAppToFinish();
assert(result != null);
return;
}
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
throwToolExit('Lost connection to device.');
}
rethrow;
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
}
}
- 這是一段 Flutter 命令行工具的 Dart 代碼,具體功能是將一個 Flutter 應用程序附加到特定設備的調(diào)試器上,以便進行調(diào)試。
- 在這段代碼中,根據(jù)設備類型選擇不同的附加方式。例如,如果是 Fuchsia 設備,則使用 FuchsiaIsolateDiscoveryProtocol 協(xié)議來查找應用程序,如果是 iOS 設備,則使用 MDnsObservatoryDiscovery 協(xié)議查找。如果以上兩種方法都失敗,則使用 ProtocolDiscovery 協(xié)議查找。
- 在找到應用程序的 Uri 后,該應用程序會使用運行中的 daemon 或創(chuàng)建新的 daemon 與設備進行通信。
找到uri http://127.0.0.1:55177/RXKA2jepV60=/
運行while循環(huán)接收指令:
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
3、getObservatoryUri
@visibleForTesting
Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
_logger.printTrace('Checking for advertised Dart observatories...');
try {
await _client.start();
final List<PtrResourceRecord> pointerRecords = await _client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
_logger.printTrace('No pointer records found.');
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet();
String? domainName;
if (applicationId != null) {
for (final String name in uniqueDomainNames) {
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = name;
break;
}
}
if (domainName == null) {
throwToolExit('Did not find a observatory port advertised for $applicationId.');
}
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple observatory ports available.');
buffer.writeln('Rerun this command with one of the following passed in as the appId:');
buffer.writeln();
for (final String uniqueDomainName in uniqueDomainNames) {
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
}
throwToolExit(buffer.toString());
} else {
domainName = pointerRecords[0].domainName;
}
_logger.printTrace('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate.
final List<SrvResourceRecord> srv = await _client
.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName),
)
.toList();
if (srv.isEmpty) {
return null;
}
if (srv.length > 1) {
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
_logger.printTrace('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await _client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
const String authCodePrefix = 'authCode=';
String? raw;
for (final String record in txt.first.text.split('\n')) {
if (record.startsWith(authCodePrefix)) {
raw = record;
break;
}
}
if (raw == null) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) {
authCode += '/';
}
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
} finally {
_client.stop();
}
}
代碼流程如下:
- 打印日志,開始查找已經(jīng)廣告的Dart Observatory。
- 啟動MDNS客戶端。
- 通過客戶端查詢指向Dart Observatory的指針記錄(PtrResourceRecord)。
- 如果找不到指針記錄,打印日志并返回null。
- 如果找到指針記錄,將其唯一的域名添加到集合中。
- 如果提供了應用程序ID,則在集合中查找以該ID開頭的唯一域名。如果找不到,則拋出異常。
- 如果未提供應用程序ID,并且集合中有多個唯一的域名,則打印建議的應用程序ID并拋出異常。
- 如果未提供應用程序ID,并且集合中只有一個唯一的域名,則使用該唯一的域名。
- 檢查所選域名上是否有可用端口。
- 如果有多個服務記錄(SrvResourceRecord),則使用第一個記錄的端口。
- 檢查所選域名上是否有身份驗證代碼(authCode)。
- 如果沒有身份驗證代碼,則返回使用第一個服務記錄的端口和空的身份驗證代碼的MDnsObservatoryDiscoveryResult。
- 如果有身份驗證代碼,則從TXT資源記錄中提取該代碼。
- 如果找不到身份驗證代碼,則返回使用第一個服務記錄的端口和空的身份驗證代碼的MDnsObservatoryDiscoveryResult。
- 如果找到了身份驗證代碼,則將其分配給MDnsObservatoryDiscoveryResult,同時確保代碼以"/"結(jié)尾。
- 停止MDNS客戶端。
- 返回使用所選域名的第一個服務記錄的端口和身份驗證代碼的MDnsObservatoryDiscoveryResult。
4、 await _client.start();
Future<void> start({
InternetAddress? listenAddress,
NetworkInterfacesFactory? interfacesFactory,
int mDnsPort = mDnsPort,
InternetAddress? mDnsAddress,
}) async {
listenAddress ??= InternetAddress.anyIPv4;
interfacesFactory ??= allInterfacesFactory;
assert(listenAddress.address == InternetAddress.anyIPv4.address ||
listenAddress.address == InternetAddress.anyIPv6.address);
if (_started || _starting) {
return;
}
_starting = true;
final int selectedMDnsPort = _mDnsPort = mDnsPort;
_mDnsAddress = mDnsAddress;
// Listen on all addresses.
final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
listenAddress.address,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
// Can't send to IPv6 any address.
if (incoming.address != InternetAddress.anyIPv6) {
_sockets.add(incoming);
} else {
_toBeClosed.add(incoming);
}
_mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
? mDnsAddressIPv4
: mDnsAddressIPv6;
final List<NetworkInterface> interfaces =
(await interfacesFactory(listenAddress.type)).toList();
for (final NetworkInterface interface in interfaces) {
// Create a socket for sending on each adapter.
final InternetAddress targetAddress = interface.addresses[0];
final RawDatagramSocket socket = await _rawDatagramSocketFactory(
targetAddress,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
_sockets.add(socket);
// Ensure that we're using this address/interface for multicast.
if (targetAddress.type == InternetAddressType.IPv4) {
socket.setRawOption(RawSocketOption(
RawSocketOption.levelIPv4,
RawSocketOption.IPv4MulticastInterface,
targetAddress.rawAddress,
));
} else {
socket.setRawOption(RawSocketOption.fromInt(
RawSocketOption.levelIPv6,
RawSocketOption.IPv6MulticastInterface,
interface.index,
));
}
// Join multicast on this interface.
incoming.joinMulticast(_mDnsAddress!, interface);
}
incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
_started = true;
_starting = false;
}
檢查是否已經(jīng)啟動或正在啟動,如果是則直接返回。
初始化網(wǎng)絡地址、接口工廠等參數(shù)。
創(chuàng)建一個 RawDatagramSocket 對象,用于接收網(wǎng)絡數(shù)據(jù)。通過 _rawDatagramSocketFactory 方法創(chuàng)建并設置監(jiān)聽地址、端口、地址重用、端口重用等選項。
將創(chuàng)建的 RawDatagramSocket 對象添加到 _sockets 列表中,如果地址為 InternetAddress.anyIPv6,則添加到 _toBeClosed 列表中。
確定 mDNS 地址,如果沒有傳入 mDNS 地址,則根據(jù)監(jiān)聽地址類型選擇 IPv4 或 IPv6 的默認 mDNS 地址。
獲取本地網(wǎng)絡接口列表,并對每個接口創(chuàng)建一個 RawDatagramSocket 對象,用于發(fā)送網(wǎng)絡數(shù)據(jù)。對每個接口設置監(jiān)聽地址、端口、地址重用、端口重用等選項,并添加到 _sockets 列表中。對于 IPv4 接口,使用 setRawOption 方法設置 IPv4 組播接口,對于 IPv6 接口,使用 setRawOption 方法設置 IPv6 組播接口。
_sockets.add(socket);會發(fā)現(xiàn)有3個sockets
0.0.0.0,127.0.0.1,253.53.111.111 這三個ip地址應該對應是同一個主機。
- 對接收 RawDatagramSocket 對象調(diào)用 joinMulticast 方法,加入 mDNS 組播地址和本地網(wǎng)絡接口。
- 對接收 RawDatagramSocket 對象調(diào)用 listen 方法,監(jiān)聽網(wǎng)絡事件并調(diào)用 _handleIncoming 方法處理網(wǎng)絡數(shù)據(jù)。
// Process incoming datagrams.
void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
if (event == RawSocketEvent.read) {
final Datagram? datagram = incoming.receive();
if (datagram == null) {
return;
}
// Check for published responses.
final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
if (response != null) {
_cache.updateRecords(response);
_resolver.handleResponse(response);
return;
}
// TODO(dnfield): Support queries coming in for published entries.
}
}
在_handleIncoming的數(shù)據(jù)回調(diào)中,可看到數(shù)據(jù)長這樣:

- 設置 _started 標志表示已啟動,設置 _starting 標志表示正在啟動。
總的來說,在Flutter中,mdnsclient.start是啟動一個mDNS客戶端的方法,用于在本地網(wǎng)絡上發(fā)現(xiàn)可用的服務。
mDNS是一種廣泛使用的服務發(fā)現(xiàn)協(xié)議,可以通過在本地網(wǎng)絡中進行廣播和響應來發(fā)現(xiàn)可用的服務。mDNS客戶端使用查詢報文向本地網(wǎng)絡中的所有設備發(fā)送請求,以查找可用的服務。一旦某個設備響應了請求,mDNS客戶端就會接收到包含服務信息的響應報文。
mdnsclient.start方法會啟動一個mDNS客戶端,并開始向本地網(wǎng)絡中發(fā)送查詢報文。當發(fā)現(xiàn)可用的服務時,客戶端將回調(diào)一個提供服務信息的回調(diào)函數(shù),以便應用程序可以處理這些信息。通過這種方式,應用程序可以在本地網(wǎng)絡中發(fā)現(xiàn)可用的服務,并使用這些服務進行網(wǎng)絡通信。
5、 MDnsObservatoryDiscoveryResult(srv.first.port, authCode)

6、buildObservatoryUri
- MDnsObservatoryDiscovery.instance.getObservatoryUri
final Uri uriFromMdns =
await MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
);
observatoryUri = uriFromMdns == null
? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
- query(
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
);
final MDnsObservatoryDiscoveryResult? result = await query(
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
);
- buildObservatoryUri
Future<Uri> buildObservatoryUri(
Device device,
String host,
int devicePort, [
int? hostVmservicePort,
String? authCode,
]) async {
String path = '/';
if (authCode != null) {
path = authCode;
}
// Not having a trailing slash can cause problems in some situations.
// Ensure that there's one present.
if (!path.endsWith('/')) {
path += '/';
}
hostVmservicePort ??= 0;
final int? actualHostPort = hostVmservicePort == 0 ?
await device.portForwarder?.forward(devicePort) :
hostVmservicePort;
return Uri(scheme: 'http', host: host, port: actualHostPort, path: path);
}
最終是得到了一個這樣的 Uri:
http://127.0.0.1:55612/tJgiq9vKwN0=/
7、問題回歸:
mDNS代表“多播DNS”,是一種網(wǎng)絡協(xié)議,用于在局域網(wǎng)上發(fā)現(xiàn)可用的設備和服務。它使用了組播IP地址(224.0.0.251)和標準的DNS編解碼格式,通過局域網(wǎng)內(nèi)廣播查詢和響應的方式來實現(xiàn)設備的發(fā)現(xiàn)和服務的注冊。mDNS協(xié)議的一個重要用途是使Apple的Bonjour技術(shù)能夠在Mac OS X和其他操作系統(tǒng)上自動發(fā)現(xiàn)可用的網(wǎng)絡服務和設備。mDNS還可以用于智能家居和物聯(lián)網(wǎng)設備之間的通信,因為它可以通過在局域網(wǎng)上廣播和接收信息來簡化設備之間的連接和交互。由于mDNS使用組播地址,因此它可以在不需要集中式服務器的情況下工作,這使得它非常適合于在家庭和小型辦公室網(wǎng)絡中使用。
flutter tools attach上面getObservatoryUri 這個方法做的就是跟使用dns-sd 命令差不多的事情:
- 發(fā)現(xiàn)可用服務:使用以下命令可以列出網(wǎng)絡中可用的所有服務:
dns-sd -B _services._dns-sd._udp

- 發(fā)現(xiàn)特定服務:使用以下命令可以列出特定服務的所有實例:
dns-sd -B _dartobservatory

- 查找網(wǎng)絡上運行的 Dart Observatory 實例的命令
Dart Observatory 是一種用于調(diào)試和分析 Dart 代碼的工具,它運行在特定的端口上并提供了一系列的調(diào)試功能。在調(diào)試 Dart 應用程序時,您可以使用 Dart Observatory 監(jiān)視運行時性能指標,跟蹤內(nèi)存使用情況,以及診斷和修復其他問題。
命令中的 "-Z" 選項告訴命令行實用程序 "dns-sd"(也稱為 Bonjour)執(zhí)行零配置服務發(fā)現(xiàn),并查找名稱為 "_dartobservatory" 的服務。如果在本地網(wǎng)絡上運行了 Dart Observatory 實例,它應該會在終端中輸出有關(guān)該實例的詳細信息,例如IP地址、端口等。
dns-sd -Z _dartobservatory

最后我們可以使用在flutter attach 時添加參數(shù):

- 最后用網(wǎng)絡博主的文案做一個問題解決的總結(jié):https://juejin.cn/post/7175493950856232997
Flutter通過將更新的源代碼文件注入到正在運行的Dart 虛擬機(VM)來實現(xiàn)熱重載。在虛擬機使用新的字段和函數(shù)更新類之后, Flutter 框架會自動重新構(gòu)建 widget 樹,以便快速查看更改的效果。
而attach的過程就是一個連接VM的過程,應用以Debug模式運行后,會啟動一個VM服務,并且使用mDNS協(xié)議( mDNS/DNS-SD 是用于本地局域網(wǎng)服務發(fā)現(xiàn)的協(xié)議)廣播。執(zhí)行attach操作時,會通過mDNS協(xié)議去查找當前應用所匹配的VM服務,再通過WS協(xié)議進行連接,這個時候就可能出現(xiàn)如下幾種情況,導致連接失敗:
找到的VM服務太多,需要選擇連接哪一個。
在flutter attach參數(shù)上添加-app-id 指定appid調(diào)度問題,沒有去連正確的 VM 服務。
在attach時拔掉網(wǎng)線,斷開網(wǎng)絡連接,讓mDNS找不到局域網(wǎng)內(nèi)其它設備的VM服務,每次運行前執(zhí)行 dns-sd -Z _dartobservatory._tcp 查看當前的VM服務,根據(jù)上面的設備名和端口選擇正確的app-id進行配置。mDNS緩存沒有刷新
mDNS查找問題
- 關(guān)閉個人熱點
- Mac -> 設置 -> 網(wǎng)絡 -> iPhone USB -> 不勾選“除非需要,否則請停用”
- 重試