配置一个Flutter的远程Android调试环境

本文约 2700 字,阅读需 6 分钟。

1.构建制品

之前已经构建过:Flutter源码剖析(一):源码获取与构建 - 赵裕的博客

新遇到一些错误,记录下:

$ ../flutter/bin/flutter build --local-engine-src-path ../src  --local-engine=android_debug_unopt android
......
You are using a locally built engine (--local-engine) but have not specified --local-engine-host.
You may be building with a different engine than the one you are running with. See https://github.com/flutter/flutter/issues/132245 for
details.

<–原因:现在这个参数要强行配置。

Could not execute [class org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatHandler.buildFinished]
FAILURE: Build failed with an exception.
* Where:
Build file '/data/research/flutter/my_app_1/android/app/build.gradle' line: 4
* What went wrong:
An exception occurred applying plugin request [id: 'dev.flutter.flutter-gradle-plugin', version: '1.0.0']
> Failed to apply plugin 'dev.flutter.flutter-gradle-plugin'.
   > local-engine-out must point to a local engine build
* Try:
> Run with --debug option to get more log output.
> Run with --scan to get full insights.
* Exception is:

<–原因:--local-engine-src-path不要用相对路径。

../flutter/bin/flutter build --local-engine-src-path=/data/research/flutter/src  --local-engine=android_debug_unopt --local-engine-host=host_debug_unopt apk --debug --verbose

2.远程调试

这部分基于之前的工作:在服务器上搭建Android开发调试环境的尝试 - 赵裕的博客

# 错误1:
error: attach failed: Failed to connect port

确保端口都转发且对应得上。

# 错误2:
error: attach failed: Cannot get process architecture

确定是APP的主进程,不要attach错了。

最终成功的效果:

注意要添加Debug Symbol,不然无法设置断点(PS:后面优化到脚本里去了):

"add-dsym  ${workspaceFolder}/src/out/android_debug_unopt/libflutter.so"

注意libflutter.so要加载(System.loadLibrary)过一次,不然会有这个错误:

error: symbol file '/data/research/flutter/src/out/android_debug_unopt/libflutter.so' does not match any existing module

Trick: 如果要Debug启动过程,可以加个入口Activity,进一次页面退出,再add-dsym,就可以开始调试了。

3.优化使用

之前写了个脚本,主要功能是获取目标App的Pid,启动lldb-server。然后,在launch.json作如下配置:

{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "DebugAndroid",
        "type": "lldb",
        "request": "custom",
        "initCommands": [
          "platform select remote-android",
          "platform connect connect://f6d56607:10087",
          "process attach -p xxx",
        ],
        "exitCommands": [
            "platform disconnect",
        ],
      }
    ]
  }

这样使用有个问题,就是DeviceId(f6d56607)和App的PID(xxx)是经常需要修改的,特别是后者。此外,每次使用前如果App重启了,需要执行下脚本,获取新的Pid。

作为一个“懒惰的”程序员,得想想办法。正常来说,launch.json和脚本通过preLaunchTask连接,应该可以通过env传递,试了好几种方法但没成功。我觉得本质是VSCode没有预设这么高耦合的用法,就算可以也没那么优雅。

再想想,可以利用lldb的特性,把脚本的准备工作和launch.json的调试工作通过文件(其实是两个模块的通信协议)连接。launch.json如下

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "DebugAndroid",
      "type": "lldb",
      "request": "custom",
      "preLaunchTask": "start_lldb_server", // 执行脚本
      "initCommands": [
        "version", // use custom build lldb(version > 16) for port forward
        "command source ${workspaceFolder}/launch.sh"
      ],
      "exitCommands": [
        "platform disconnect",
        "shell ${workspaceFolder}/before_lldb_quit.sh",
      ],
    }
  ]
}

start_lldb_server其实就是启动了这个脚本:

{
    "version": "2.0.0",
    "tasks": [
      {
        "label": "start_lldb_server",
        "type": "shell",
        "command": "sh ~/.power_script/android_dev/launch_debuggable_app.sh com.example.my_app 10087",
        "problemMatcher": []
      }
    ]
}

脚本内容如下(from:power_script/android_dev/launch_debuggable_app.sh at master · vimerzhao/power_script):

#!/bin/sh
# build & install & launch,比较定制化且相对低频,就不纳入脚本了
if [ "$#" -ne 2 ]; then
    echo "./launch_debuggable_app.sh \$PKG_NAME \$PORT_NUM"
    exit 1
fi
PKG_NAME="$1"
PORT_NUM="$2"
TARGET_PID=$(adb shell pidof ${PKG_NAME})
echo "--->TARGET_PID: $TARGET_PID"
if [ "${TARGET_PID}" = "" ]; then
    echo "Package: ${PKG_NAME} is not running!!!"
    exit 1
fi

function run_in_android() {
 adb shell "run-as ${PKG_NAME} sh -c '${1}'" # 解决权限问题
}

# Step 1 清理之前可能残留的lldb进程
# enable for debug
echo "--->before clear:"
run_in_android ps
echo "--->start clear lldb-server if needed"
# 可能有 lldb-server 和 [lldb-server] ,只需要命中前者并kill即可
CLIENT_LLDB_ID=$(run_in_android ps| grep 'lldb-server$' | awk '{print $2}')
if [ "${CLIENT_LLDB_ID}" != "" ]; then
    echo "--->Kill LLDB: ${CLIENT_LLDB_ID} <---"
    run_in_android "kill -9 ${CLIENT_LLDB_ID}"
fi
echo "--->current process info(after clear):"
run_in_android ps
# Step 2 准备Server
LLDB_DIR="/data/data/${PKG_NAME}/lldb"
SERVER_DIR="${LLDB_DIR}/bin/"
run_in_android "mkdir -p ${SERVER_DIR}"
SERVER_PATH=${SERVER_DIR}/lldb-server
TMP_SERVER="/data/local/tmp/lldb-server"
run_in_android "cp -F ${TMP_SERVER} ${SERVER_PATH} && chmod 700 ${SERVER_PATH}"
# Step 3(如果是还命令行调用,则有必要设置环境变量,VSCode有专门的配置入口: Lldb->Adapter Env)
export ANDROID_PLATFORM_LOCAL_PORT=50012
export ANDROID_PLATFORM_LOCAL_GDB_PORT=50013
#echo "ANDROID_PLATFORM_LOCAL_PORT=${ANDROID_PLATFORM_LOCAL_PORT}"
#echo "ANDROID_PLATFORM_LOCAL_GDB_PORT=${ANDROID_PLATFORM_LOCAL_GDB_PORT}"

# Step 4 开始监听
echo "--->start listen to ${PORT_NUM}"
run_in_android "${SERVER_PATH} platform --listen \"*:${PORT_NUM}\" --server" &
echo "--->current process info(after launch listening):"
run_in_android ps # verbose info
gen=".generated"
if [ ! -d "$gen" ]; then
  mkdir ${gen}
else
  echo "Directory exists: $directory"
fi
CLIENT_LLDB_ID=$(run_in_android ps| grep lldb | awk '{print $2}')
if [ "${CLIENT_LLDB_ID}" != "" ]; then
    # launch.json的exitCommand中调用,每次使用完及时清理lldb server进程
    echo "--->new client lldb id: ${CLIENT_LLDB_ID}"
    echo "#Generated at $(date)" > ${gen}/before_lldb_quit.sh
    echo "adb shell \"run-as ${PKG_NAME} sh -c 'kill -9 ${CLIENT_LLDB_ID}'\"" >> ${gen}/before_lldb_quit.sh
    chmod +x ${gen}/before_lldb_quit.sh
fi

# Step 5 生成lldb命令,让VSCode加载,因为DeviceId / 端口 / 进程id 都在脚本生成,在`launch.json`直接运行lldb其实不方便,
#        这里利用 temp 文件传递了这些信息
launch_file=${gen}/launch.sh
device_id=$(adb devices | head -2 |tail -1 | cut -f 1)
echo "platform select remote-android" > ${launch_file}
echo "platform connect connect://${device_id}:${PORT_NUM}" >> ${launch_file}
echo "process attach -p ${TARGET_PID}" >> ${launch_file}
current_path=$(pwd)
# 这一句衔接在attach后面不会生效,需要研究下......
echo "add-dsym ${current_path}/src/out/android_debug_unopt/libflutter.so" >> ${launch_file}
# from terminal
# lldb -s temp
echo "--->Try Kill in this place"

这样,每次启动App后,只要点一下Button就可以开始调试了。

4.代码跳转

这里有个深坑,之前在工作中曾经想用VSCode开发,也是由于代码跳转一直没整明白,就搁置了。

按照Flutter官方文档的VSCode with C/C++ Intellisense [C/C++] 所说,其实有两种配置compile_command.json的方法:

  1. 其中一种是依赖官方的C/C++插件,在c_cpp_properties.json这个文件配置"compileCommands": "/path/to/compile_commands.json",,详见(c_cpp_properties.json reference
  2. 另外一种是安装clangd插件,在settings.json配置,但好像也没有文档讲的特别清楚(clangd/vscode-clangd: Visual Studio Code extension for clangd

如果用第1种,问题很多,建立索引的过程又慢又卡,基本会Crash,如下:

对于方法2,和Vim的体验很类似,基本不会卡UI,另外可以加以下配置,体验更好:

# VSCode settings -> Clangd: Arguments
--background-index --clang-tidy

在Mac平台,如果用第一种,会导致内核态CPU占用非常高,甚至重启(所以我之前基本没有重度使用VSCode,因此就没有深入研究这个问题,死锁了….)

5.遗留问题

5.1 启动过程无法调试

发现在调试时,如果在启动那块逻辑打断点,会莫名其妙Crash,如下:

I  Timeline: Activity_launch_request time:10635159384
D  initViewContentFetcherClass
I  ViewContentFetcher : ViewContentFetcher
D  createInterceptor took 0ms
E  [ERROR:flutter/shell/platform/android/android_egl_surface.cc(52)] EGL
   Error: EGL_SUCCESS (12288)
A  [FATAL:flutter/shell/platform/android/platform_view_android.cc(143)] Check

    failed: android_context_->IsValid(). Could not create surface from invalid Android context.

是什么原因?

  • 远程调试引入的问题?
  • 代码问题?一设置断点(造成时序改变)就触发了Crash?

其实是因为用的是默认arm架构的原因,如果用arm64架构就好了,围绕这个原因的发现,自己大概零散花了两个周末,因为是远程调试,多踩了不少坑(也因此对这个调试过程加深了不少理解,顺便验证了脚本的通用性),后面会单独一篇文章详细记录。

TODO1->补齐文章链接.

5.2 调试非常卡

这个有两方面:

  1. 因为是远程调试,本机只是跳板,所以网络延迟,必然体现在调试速度上。周末还试了下把符号表+要调试的代码下载到本地,用Android Studio去调试(路径映射),速度是快了不少的
  2. 对比了下,VSCode/AS调试Android确实比XCode调试iOS要慢一些,可能和内部实现有关(比如通信机制)

附录

  • flutter commit id: 9f985ad755d9fd7a7fb11ef8a7fbbf76927f9ad3 (3.16版本附近)
  • engine commit id: 452e75f2a6b6ce3ee6ef71e5e1686ce4cfef91b4
总阅读量次。