Flutter - 我给官方提PR,解决run命令卡住问题 😃

3,759 阅读7分钟

欢迎关注微信公众号:FSA全栈行动 👋

一、背景

Flutter 项目在 iPhone 真机上运行会白屏,VSCode 右下角一直卡在 Installing and launching...AndroidStudio 亦是如此。

1.png

其它人的电脑又是正常的,遂着手进行问题的定位,并在解决问题后向 Flutter 官方提交了我的第一份 PR

PR 链接:github.com/flutter/flu…

二、问题

使用命令 flutter run -d xxxx -v 运行项目,日志如下:

[        ] -------------------------
[ +187 ms] [LinXunFeng]$command source -s 0
'/tmp/3EB9B732-D866-46D2-88CB-513C2CF7455E/fruitstrap-lldb-prep-cmds-00008120_0019784E2690C01E'
[        ] Executing commands in
'/tmp/3EB9B732-D866-46D2-88CB-513C2CF7455E/fruitstrap-lldb-prep-cmds-00008120_0019784E2690C01E'.
[        ] [LinXunFeng]$    platform select remote-'ios' --sysroot '/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols'
[        ]   Platform: remote-ios
[        ]  Connected: no
[        ]   SDK Path: "/Users/lxf/Library/Developer/Xcode/iOS DeviceSupport/16.1.1 (20B101) arm64e/Symbols"
[        ] [LinXunFeng]$    target create
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos/Runner.app"
[+3544 ms] Current executable set to
'/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos/Runner.app' (arm64).
[        ] [LinXunFeng]$    script
fruitstrap_device_app="/private/var/containers/Bundle/Application/1915BC3A-57D0-41A5-8121-1769CF3F6777/Runner.app"
[ +124 ms] [LinXunFeng]$    script fruitstrap_connect_url="connect://127.0.0.1:65222"
[        ] [LinXunFeng]$    script fruitstrap_output_path=""
[        ] [LinXunFeng]$    script fruitstrap_error_path=""
[        ] [LinXunFeng]$    target modules search-paths add /usr "/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols/usr" /System "/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols/System"
"/private/var/containers/Bundle/Application/1915BC3A-57D0-41A5-8121-1769CF3F6777"
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos"
"/var/containers/Bundle/Application/1915BC3A-57D0-41A5-8121-1769CF3F6777"
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos" /Developer
"/Users/lxf/Library/Developer/Xcode/iOS DeviceSupport/16.1.1 (20B101) arm64e/Symbols/Developer"
[  +15 ms] [LinXunFeng]$    command script import
"/tmp/3EB9B732-D866-46D2-88CB-513C2CF7455E/fruitstrap_00008120_0019784E2690C01E.py"
[   +3 ms] [LinXunFeng]$    command script add -f fruitstrap_00008120_0019784E2690C01E.connect_command connect
[        ] [LinXunFeng]$    command script add -s asynchronous -f fruitstrap_00008120_0019784E2690C01E.run_command
run
[        ] [LinXunFeng]$    command script add -s asynchronous -f
fruitstrap_00008120_0019784E2690C01E.autoexit_command autoexit
[        ] [LinXunFeng]$    command script add -s asynchronous -f
fruitstrap_00008120_0019784E2690C01E.safequit_command safequit
[        ] [LinXunFeng]$    connect
[  +29 ms] [LinXunFeng]$    run
[ +109 ms] success
[+4740 ms] [LinXunFeng]$2023-02-02 08:57:41.425078+0800 Runner[5278:1703137] flutter: The Dart VM service is
listening on http://127.0.0.1:61063/

而正常运行的日志是这样的:

[        ] -------------------------
[ +359 ms] (lldb) command source -s 0
'/tmp/70CB9E5A-3BA1-4299-8F38-BE5135178035/fruitstrap-lldb-prep-cmds-00008120_0019784E2690C01E'
[        ] Executing commands in
'/tmp/70CB9E5A-3BA1-4299-8F38-BE5135178035/fruitstrap-lldb-prep-cmds-00008120_0019784E2690C01E'.
[        ] (lldb)     platform select remote-'ios' --sysroot '/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols'
[        ]   Platform: remote-ios
[        ]  Connected: no
[        ]   SDK Path: "/Users/lxf/Library/Developer/Xcode/iOS DeviceSupport/16.1.1 (20B101) arm64e/Symbols"
[        ] (lldb)     target create
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos/Runner.app"
[+3984 ms] Current executable set to
'/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos/Runner.app' (arm64).
[        ] (lldb)     script
fruitstrap_device_app="/private/var/containers/Bundle/Application/ABD16CE1-675A-44C3-BE9C-18EC156B6499/Runner.app"
[ +136 ms] (lldb)     script fruitstrap_connect_url="connect://127.0.0.1:65102"
[        ] (lldb)     script fruitstrap_output_path=""
[        ] (lldb)     script fruitstrap_error_path=""
[        ] (lldb)     target modules search-paths add /usr "/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols/usr" /System "/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols/System"
"/private/var/containers/Bundle/Application/ABD16CE1-675A-44C3-BE9C-18EC156B6499"
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos"
"/var/containers/Bundle/Application/ABD16CE1-675A-44C3-BE9C-18EC156B6499"
"/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example/build/ios/iphoneos" /Developer
"/Users/lxf/Library/Developer/Xcode/iOS DeviceSupport/16.1.1 (20B101) arm64e/Symbols/Developer"
[  +17 ms] (lldb)     command script import
"/tmp/70CB9E5A-3BA1-4299-8F38-BE5135178035/fruitstrap_00008120_0019784E2690C01E.py"
[   +3 ms] (lldb)     command script add -f fruitstrap_00008120_0019784E2690C01E.connect_command connect
[        ] (lldb)     command script add -s asynchronous -f fruitstrap_00008120_0019784E2690C01E.run_command run
[        ] (lldb)     command script add -s asynchronous -f fruitstrap_00008120_0019784E2690C01E.autoexit_command
autoexit
[        ] (lldb)     command script add -s asynchronous -f fruitstrap_00008120_0019784E2690C01E.safequit_command
safequit
[        ] (lldb)     connect
[  +38 ms] (lldb)     run
[ +118 ms] success
[        ] Application launched on the device. Waiting for observatory url.
[+4521 ms] (lldb) 2023-02-02 08:55:53.841520+0800 Runner[5272:1701483] Warning: Unable to create restoration in
progress marker file
[  +45 ms] Observatory URL on device: http://127.0.0.1:60477/
[   +3 ms] Attempting to forward device port 60477 to host port 65119
[        ] executing: /Users/lxf/fvm/versions/3.3.7/bin/cache/artifacts/usbmuxd/iproxy 65119:60477 --udid
00008120-0019784E2690C01E --debug
[ +433 ms] fopen failed for data file: errno = 2 (No such file or directory)
[        ] Errors found! Invalidating cache...
[  +13 ms] fopen failed for data file: errno = 2 (No such file or directory)
[        ] Errors found! Invalidating cache...
[ +560 ms] Forwarded port ForwardedPort HOST:65119 to DEVICE:60477
[        ] Forwarded host port 65119 to device port 60477 for Observatory
[        ] Installing and launching... (completed in 19.1s)
[        ] Caching compiled dill
[  +16 ms] Connecting to service protocol: http://127.0.0.1:65119/
[  +92 ms] Launching a Dart Developer Service (DDS) instance at http://127.0.0.1:0, connecting to VM service at
http://127.0.0.1:65119/.
[  +45 ms] DDS is listening at http://127.0.0.1:65124/MCCddjLRlQk=/.
[  +22 ms] Successfully connected to service protocol: http://127.0.0.1:65119/
[  +13 ms] DevFS: Creating new filesystem on the device (null)
[  +10 ms] DevFS: Created new filesystem on the device
(file:///private/var/mobile/Containers/Data/Application/9066D3F0-F8FB-489D-99BD-FCAF266B118F/tmp/examplePN73Pp/exa
mple/)
[        ] Updating assets
[  +34 ms] Syncing files to device lxf的iPhone...
[        ] Compiling dart to kernel with 0 updated files
[        ] Processing bundle.
[        ] <- recompile package:scrollview_observer_example/main.dart ef8dbbad-3cff-4e06-9043-63987bdc88a2
[        ] <- ef8dbbad-3cff-4e06-9043-63987bdc88a2
[        ] Bundle processing done.
[  +30 ms] Updating files.
[        ] DevFS: Sync finished
[        ] Syncing files to device lxf的iPhone... (completed in 33ms)
[        ] Synced 0.0MB.
[        ] <- accept
[   +7 ms] Connected to _flutterView/0x10881a420.
[   +1 ms] Flutter run key commands.
[        ] r Hot reload. 🔥🔥🔥
[        ] R Hot restart.
[        ] h List all available interactive commands.
[        ] d Detach (terminate "flutter run" but leave application running).
[        ] c Clear the screen
[        ] q Quit (terminate the application on the device).
[        ] 💪 Running with sound null safety 💪
[        ] An Observatory debugger and profiler on lxf的iPhone is available at:
http://127.0.0.1:65124/MCCddjLRlQk=/
[  +29 ms] The Flutter DevTools debugger and profiler on lxf的iPhone is available at:
           http://127.0.0.1:9102?uri=http://127.0.0.1:65124/MCCddjLRlQk=/

在不断谷歌 Flutter Installing and launching... 并按解决步骤操作后但问题依旧时,我认真对比了上述的命令执行日志,发现有一处可疑的地方,那就是:

[  +29 ms] [LinXunFeng]$    run
[ +109 ms] success


[  +38 ms] (lldb)     run
[ +118 ms] success

为什么我会觉得它可疑呢?因为我之前学 iOS 逆向的时候自定义过 lldb 提示符,然后差不多在那个时间段后 Flutter 项目就不再能正常运行,而且从日志中可以看出到这里就不再往下执行了~

个性化提示符:

# ~/.lldbinit 文件的内容
settings set prompt "\[LinXunFeng]$"

将其注释掉,竟完美解决了这个问题~

三、探索

改回原来的 lldb 提示符就好了,那说明 Flutter 内部八成是根据这个固定的输出来做判断是否进入下一步,打开 Flutter 源码进行全局搜索 (lldb) run,果不其然,在 flutter_tools 下可以搜到关键代码

// (lldb)     run
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');

具体文件定位:flutter_tools/ios_deploy.dart#L285-L287

在文件下搜索 _lldbRun 定位到 launchAndAttach 方法,如下

/// Launch the app on the device, and attach the debugger.
///
/// Returns whether or not the debugger successfully attached.
Future<bool> launchAndAttach() async {
  // Return when the debugger attaches, or the ios-deploy process exits.
  final Completer<bool> debuggerCompleter = Completer<bool>();
  try {
    _iosDeployProcess = await _processUtils.start(
      _launchCommand,
      environment: _iosDeployEnv,
    );
    String? lastLineFromDebugger;
    final StreamSubscription<String> stdoutSubscription = _iosDeployProcess!.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .listen((String line) {
      _monitorIOSDeployFailure(line, _logger);
      // (lldb)     run
      // success
      // 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: The Dart VM service is listening on http://127.0.0.1:57782/
      if (_lldbRun.hasMatch(line)) {
        _logger.printTrace(line);
        _debuggerState = _IOSDeployDebuggerState.launching;
        return;
      }
      // Next line after "run" must be "success", or the attach failed.
      // Example: "error: process launch failed"
      if (_debuggerState == _IOSDeployDebuggerState.launching) {
        _logger.printTrace(line);
        final bool attachSuccess = line == 'success';
        _debuggerState = attachSuccess ? _IOSDeployDebuggerState.attached : _IOSDeployDebuggerState.detached;
        if (!debuggerCompleter.isCompleted) {
          debuggerCompleter.complete(attachSuccess);
        }
        return;
      }
      ...
    });
    ...
  }
  return debuggerCompleter.future;
}

具体文件定位:flutter_tools/ios_deploy.dart#L319-L351

见名识义,将项目运行起来后进行附加调试,这段代码的执行流程如下:

  1. 调用 _processUtilsstart 方法执行 _launchCommand,然后监听其输出内容。
  2. 在监听到 (lldb) run 后将 _debuggerState 赋值为 _IOSDeployDebuggerState.launching,以做为判断紧跟其后的 success 是目标输出内容的依据
  3. 接着进行了最关键的一步 debuggerCompleter.complete(attachSuccess),告诉外部该功能已成功执行

而我的电脑因为自定义 LLDB prompt 导致这个小流程中的 23 无法顺利执行~~

四、修复

知道导致问题的原因后,我那就来尝试修复一下,思路很简单,就是先获取当前的 lldb 提示符然后将 _lldbRun 中的 (lldb) 字符串给替换掉,那如何获取当前的 lldb 提示符?

还记得上面提到的常量 _lldbRun 吗?定义处有相应的链接

// (lldb)     run
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');

打开链接后可以找到上面日志里出现过的 platform select remote-'ios' --sysroot,定义如下:

/*
 * Startup script passed to lldb.
 * To see how xcode interacts with lldb, put this into .lldbinit:
 * log enable -v -f /Users/vargaz/lldb.log lldb all
 * log enable -v -f /Users/vargaz/gdb-remote.log gdb-remote all
 */
#define LLDB_PREP_CMDS CFSTR("\
    platform select remote-ios --sysroot '{symbols_path}'\n\
    ...
    connect\n\
")

代码摘自:ios-control/ios-deploy.m#L33

那就好办了,这个 platform select remote-'ios' --sysroot 的输出比 run 要早,所以只需要监听其被输出后进行字符串截取,就可以取到当前的 lldb 提示符

[        ] [LinXunFeng]$    platform select remote-'ios' --sysroot '/Users/lxf/Library/Developer/Xcode/iOS
DeviceSupport/16.1.1 (20B101) arm64e/Symbols'

具体代码:

1、定义 _lldbPlatformSelect 常量

-  // (lldb)     run
-  // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
-  static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');

+  // (lldb) platform select remote-'ios' --sysroot
+  // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L33
+  // This regex is to get the configurable lldb prompt. By default this prompt will be "lldb".
+  static final RegExp _lldbPlatformSelect = RegExp(r"\s*platform select remote-'ios' --sysroot");

2、调整 launchAndAttach 方法

Future<bool> launchAndAttach() async {
    // Return when the debugger attaches, or the ios-deploy process exits.

+    // (lldb)     run
+    // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
+    RegExp lldbRun = RegExp(r'\(lldb\)\s*run');

    final Completer<bool> debuggerCompleter = Completer<bool>();
    try {
      _iosDeployProcess = await _processUtils.start(
        _launchCommand,
        environment: _iosDeployEnv,
      );
      String? lastLineFromDebugger;
      final StreamSubscription<String> stdoutSubscription = _iosDeployProcess!.stdout
          .transform<String>(utf8.decoder)
          .transform<String>(const LineSplitter())
          .listen((String line) {
        _monitorIOSDeployFailure(line, _logger);
+
+        // (lldb)    platform select remote-'ios' --sysroot
+        // Use the configurable custom lldb prompt in the regex. The developer can set this prompt to anything.
+        // For example `settings set prompt "(mylldb)"` in ~/.lldbinit results in:
+        // "(mylldb)    platform select remote-'ios' --sysroot"
+        if (_lldbPlatformSelect.hasMatch(line)) {
+          final String platformSelect = _lldbPlatformSelect.stringMatch(line) ?? '';
+          if (platformSelect.isEmpty) {
+            return;
+          }
+          final int promptEndIndex = line.indexOf(platformSelect);
+          if (promptEndIndex == -1) {
+            return;
+          }
+          final String prompt = line.substring(0, promptEndIndex);
+          lldbRun = RegExp(RegExp.escape(prompt) + r'\s*run');
+          _logger.printTrace(line);
+          return;
+        }
+
        // (lldb)     run
        // success
        // 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: The Dart VM service is listening on http://127.0.0.1:57782/
-        if (_lldbRun.hasMatch(line)) {
+        if (lldbRun.hasMatch(line)) {
          ...

修改完进行测试,确认没有问题后向 Flutter 官方提交了 PR

五、调试

这里主要记录关于调试的一些准备和注意点。

准备工作

使用 VSCode 打开 flutter_tools 项目,调试前需要做的就两个操作:

1、调整 .vscode/launch.json

{
  "name": "flutter_tools",
  "request": "launch",
  "type": "dart",
  "args": [
    "run",
    "-v",
    "-d",
    "设备id"
  ],
  "program": "${workspaceFolder}/bin/flutter_tools.dart",
  "env": {
    "FLUTTER_ROOT": "${workspaceFolder}/../../"
  },
},

2、指定运行的 flutter 项目

上面只是设置了打印日志和指定运行的设备id,还差指定运行的 flutter 项目。

由于 flutter 命令里没有提供相关参数,所以我在 lib/executable.dart 文件中 Cache.flutterRoot 的上一行添加如下代码,强行修改当前执行 flutter 命令的目录路径

...

// 设置当前执行flutter命令的目录路径
globals.fs.currentDirectory = '/Users/lxf/Desktop/LXF/github/flutter_scrollview_observer/example';

Cache.flutterRoot = Cache.defaultFlutterRoot(
...

不过该方式过于粗暴,后面经过摸索,发现在 .vscode/launch.json 配置 cwd 即可达到效果,完整配置如下:

{
  "name": "flutter_tools",
  "request": "launch",
  "type": "dart",
  "args": [
    "run",
    "-v",
    "-d",
    "设备id"
  ],
  "program": "${workspaceFolder}/bin/flutter_tools.dart",
  "env": {
    "FLUTTER_ROOT": "${workspaceFolder}/../../"
  },
  "cwd": "/Users/lxf/Desktop/gitHub/flutter_scrollview_observer/example",
},

注意项

当你调试完 flutter_tools 的源码后,高高兴兴去到自己的 flutter 工程目录下执行 flutter run 时,你会发现改动不生效,这是因为存在如下缓存文件

flutter/bin/cache/flutter_tools.snapshot

解决方式:将该文件删除即可,当执行任意的 flutter 命令时会自动再次生成。

六、最后

这次提 PR 的过程也学到了很多,过程中比较令人记忆深刻的是以下几点:

  1. 每一行的内容不可以以空格结尾,否则 Linux analyze 这个 Check 会失败。
  2. 100+Check 执行时间是真的久,好像是 40+分钟,每次提交都会重新执行。
  3. 有的 Check 时不时会抽风,像 Google testing 有时会一直卡住,luci-flutter 会经常失败,这些可以不用理它,因为其它人也会,当官方调整完会自动重新执行。
  4. 提交 PR 后到开始审核的时间好久,2023.1.29 提交,2023.2.3 审核,5天,你知道我这5天是怎么过来的吗? 😂

但不管怎么说,功夫不负有心人,最终于 2023.2.7 成功 Merge,这是一次不错的开始 😃

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~