我们排查 Flutter Android 项目卡住的那些瞬间,其实大多不是玄学,而是构建链路某个环节停住了。这篇文章我会按自己真实的排障习惯,一条一条带你看。
1. 先分清“这一步”卡在哪:构建进程不同,排查方向完全不同
1.1 终端里停留的最后一句话,就是最好的线索
很多人在开发者社区提问时,标题只会写“到这一步就卡住了”。“这一步”到底是哪一步,截图里往往就一行字,比如Running Gradle task 'assembleDebug'...,或者是Waiting for another flutter command to release the startup lock,又或者是连错误都没有的空白光标。面对这些情况,我最早犯过的错是看到“卡住”就到处搜教程,东改一处西换一个版本,折腾一晚上还是没解决。
后来我养成了一个习惯:不把“卡住”当整体现象,而是先看终端里最后停下的那一条提示,然后问自己三个问题:这行提示是 Flutter 命令输出的,还是 Gradle 输出的?它发生后,CPU 和网络还有没有动静?之前一次能正常跑通时,走到这一步大概花了多久?把这三个问题回答清楚,再去查资料,效率会高很多。
1.2 Flutter Android 从 Run 到上屏要走完哪六段路
Flutter 项目在 Android 上启动,并不是只靠 Dart 代码直接变成 APK。你点击 Run 之后,实际会经历六个阶段:
pub get解析 pubspec.yaml 里的依赖,并把依赖包拉到本地缓存。- 调起 Gradle Wrapper,让 Gradle 引擎读取 android 目录下的构建脚本。
- Gradle 解析并下载 Android 构建所需依赖,包括 Android Gradle Plugin、Kotlin 相关库、第三方插件等。
- 执行
assembleDebug打包 APK。 - 通过 adb 把 APK 安装到连接的设备或模拟器。
- 安装完成后启动应用,随后 Flutter 引擎 attach 到调试器。
这六步每一步都可能卡住,但屏幕上都只显示一个类似“正在构建”的状态。如果不先定位到具体阶段,很容易把所有问题混在一起乱猜。比如有人flutter run卡住,就去改 Gradle 镜像,结果问题其实出在 adb 没有识别到设备,白白耽误半天。
1.3 真卡死与假缓慢的区分方法
判断是慢还是卡,我通常会同时看三个信源:终端的输出频率、任务管理器的 CPU 占用、网络或磁盘的活动状态。
如果终端在几分钟内持续滚动新日志,而且 CPU 占用率一直有波动,那多半是还在干活,只是速度慢。这时候你不需要慌张,可以泡杯茶再等几分钟。如果最后一行已经停留了十几分钟,CPU 几乎归零,网络也没有下载流量,那才是真卡死了。
还有一类“假卡”:flutter run停在询问是否允许网络、或者 Android Studio 弹了某个授权窗口但你切到了别的窗口没看到。这种情况最搞笑,也最容易忽略。遇到卡住时,先切到 IDE 主窗口和系统通知栏看一看,也许不是进程死了,只是它在等一个永远不会被点击的对话框。
2. 最常见的卡死源头:Gradle 发行包与依赖仓库
2.1 一个 Flutter 项目为什么会卡在 Gradle 层
很多新手会问:我写的明明是 Dart 代码,为什么构建时要下载 Gradle?原因是 Flutter 的 Android 工程本质上仍然是一个 Android 工程,它使用 Gradle 作为构建工具。Gradle 负责把 Android 资源、Java/Kotlin 代码、Manifest 配置、依赖库整合起来,最终生成 APK。
Gradle 本身就好比一个建筑队的小头目,它要指挥各种材料到位。但它第一件事并不是盖楼,而是先确认自己这个版本的工具包是否已经在本地。android/gradle/wrapper/gradle-wrapper.properties文件里写着 Gradle 的发行版下载地址。如果本地缓存里没有这个版本,Gradle Wrapper 会先下载一个压缩包,这个压缩包通常几十上百 MB,下载速度一旦被网络拖累,就会长时间停在那里。于是你看到的Running Gradle task 'assembleDebug'...其实是在下载 Gradle 本身,而不是在编译代码。
2.2 三个文件定位 Gradle 下载卡点
要确认是不是 Gradle 下载的问题,我一般会先看三个地方:
android/gradle/wrapper/gradle-wrapper.properties:看distributionUrl指向哪个域名,对应哪个版本号。- 本地
~/.gradle/wrapper/dists/目录:查看对应版本目录下有没有.part或.lck文件,有就说明下载未完成。 android/settings.gradle或android/build.gradle:看仓库源的配置顺序,依赖解析是从这些仓库里拉取。
拿我自己的经验来说,卡在这种地方时,任务管理器里往往有网络流量,但非常零星,速度长期徘徊在几 KB/s 到几十 KB/s。如果你看到的正是这个状态,不用怀疑,多半是网络到默认下载域名的链路太挤了。
2.3 给发行包和依赖仓库“体检”和换源
搞清楚是下载问题之后,剩下就是换源和预下载两条路线。
换源时要改的是gradle-wrapper.properties里的distributionUrl,把原来的官方下载地址替换成能更快访问的公共镜像地址。改的时候注意版本号不能随意动,还是保持原文件里写的版本,只换域名部分最稳妥。改完拿新的 URL 在浏览器里试着下载一次,确认能被完整拉下来,再重新跑构建。
依赖仓库的配置也类似。很多项目会同时依赖 Google Maven 仓库和 Maven Central 仓库,这两个是下载依赖库的主要来源。如果卡在某个> Could not resolve或> IDLE状态,可以在仓库列表最前面插入镜像源。但要记住一个原则:不能把它放在最前面就顺手把原始仓库删了,因为有些冷门插件只发布在官方指定仓库,全删之后反而会报依赖找不到。
2.4 修改源之后必须处理的残留缓存
换完源还有一个新手特别容易踩的细节:旧缓存没清理。比如 Gradle 已经下载了一半的.part文件,或者下载完整但被标记成坏文件的目录,都会导致重新构建时仍然卡住。正确的做法是把~/.gradle/wrapper/dists/下对应版本号的目录先改个名字备份,确认新下载能用再删除;依赖仓库缓存同样可以清理~/.gradle/caches/,但这一步改动影响较大,建议优先只处理 wrapper。
清理完之后先跑一次./gradlew --stop,把之前可能残留的 Gradle 守护进程全部停掉,再重新flutter run。这一步经常被忽略,但很实用,能避免守护进程里保存的旧状态继续干扰。
3. Flutter 与 Android 工具链不咬合也会假死
3.1 第一次 flutter run 时的 Engine artifacts 下载
Gradle 下载解决之后,还有一类卡住发生在 Flutter 工具链自身。首次跑flutter run时,Flutter 会按目标平台下载对应的 Engine artifacts,包括编译好的 Flutter engine、sky_engine 相关文件、字体和 shader 等。这些文件默认放在 Flutter SDK 的bin/cache或用户级缓存目录里。
刚才那种“等了很久都没反应”,有可能最后一行是类似Downloading...的字样,但前面没有显示进度百分比。这种情况我也遇到过,嘴上说“卡住了”,其实是在大包下载。处理办法通常不是等,而是检查 Flutter SDK 的缓存目录,确认文件大小是不是在增长。如果确实在增长,就耐心等;如果完全不动,可能是下载源不通,需要看flutter config --list是否设置了镜像,或者手动把缓存目录中的临时文件清掉重新下载。
3.2 JDK 版本和 Android Gradle Plugin 需要配对
另一个容易被忽视的卡点是 Java 环境与 Android Gradle Plugin 版本不匹配。Flutter 的 Android 构建依赖 JDK,但不同版本的 Android Gradle Plugin 对 JDK 版本要求不一样。JDK 太老或太新,构建过程会在某个没有报错信息的地方僵住,或者输出一堆让新手害怕但实际只因版本不对的异常。
检查方式很简单:终端跑flutter doctor -v,看 Java 版本;再打开android/settings.gradle里的plugins块,确认 Android Gradle Plugin 和 Kotlin 版本。各版本对 JDK 的要求并不复杂,但如果你之前自己手动装过多个 JDK,很容易出现 IDE 里配置的是 JDK 17,命令行里却指向 JDK 8,构建时卡得不明不白。这时候就需要统一环境变量JAVA_HOME,保证 IDE 和命令行用的是同一个 JDK。
3.3 插件版本互相打架时的表现与排查
还有一类卡住,既不是网络问题也不是工具链版本问题,而是依赖插件冲突。具体表现是 Gradle 构建阶段输出大量日志,但最后走到某个 Task 时突然停住,或者 sync 时一直转圈不结束。
这种我来按一个“最小化排除法”处理:先把pubspec.yaml里的第三方依赖注释掉,只保留项目自带的那几个基础包,运行一次。如果构建能走通,就一个一个把插件加回来,每次只加一个并跑一次构建,很快就能定位到是哪个插件把构建卡住。定位到之后,查看它的 Android 配置要求,把相关 SDK 版本、依赖版本对齐即可。
4. 三种能帮你“看到卡点”的透视手段
4.1 flutter run --verbose 读尾行
默认的flutter run输出比较精简,很多中间细节被隐藏了。遇到疑难卡死,我会先跑一次带 verbose 的启动命令,比如flutter run --verbose。它会输出比平时多得多的时间线,要点不是看全部,而是看最后那几行。
举个例子,如果最后一行显示正在执行:app:processDebugMainManifest,卡点就在清单合并阶段;如果显示:app:mergeDebugNativeLibs,则多半和 so 库、NDK 有关。有了这一步的小标记,再去搜对应的 Gradle Task 名,能少走很多弯路。
4.2 绕过 Flutter 直接跑 Gradle
第二个更直接的透视手段是绕过 Flutter,直接进 android 目录跑 Gradle。在 Android Studio 的 terminal 里执行cd android然后./gradlew assembleDebug --stacktrace。这么做的好处是把现场从 Flutter 的整体流程里剥出来,单独验证 Gradle 本身能不能完成构建。
我自己就试过一种情况:Flutter CLI 卡在Running Gradle task...,但直接用 Gradle 命令跑时,几秒钟就失败了并给出具体异常。原来问题根本不在网络,而在某个依赖坐标格式写错,Flutter 层面吞掉了详细报错。所以在排查时分两步走很有意义:先用 Flutter 命令看表现,再用 Gradle 命令拿细节。
4.3 adb devices、系统监视器与磁盘空间
第三个手段比较朴素,却是很多人忘掉的:确认手机/模拟器状态和系统资源。
每次flutter run前,我都先执行一次adb devices,确认设备编号后面跟着的是device而不是unauthorized。如果状态是unauthorized,需要在手机上点“允许调试”,否则 Flutter 会一直等待设备授权而看起来像卡住。与此同时,打开系统监视器看磁盘占用。Android 构建过程中会生成多个临时文件,磁盘如果剩下不到几个 GB,构建可能在某一步停住,表现得和“卡死”一模一样。
5. 照着做也踩过的坑与快速自救清单
5.1 无脑删除 build 目录的代价
遇到构建卡住,很多人的第一反应是把android和根目录下的build目录删掉,以为这样可以来个“干净重来”。我当初也这么干过,结果不仅没有加速,反而让下一次构建从头开始,把之前好不容易缓存的增量编译全部扔掉了,构建时间变得更长。
正确做法是有条件地清理:只想清理编译碎片时,用flutter clean就足够;想连 Gradle 缓存一起处理时,再配合./gradlew clean。千万不要每次卡住就删build,尤其不要在项目依赖较多的情况下去删,除非你已经确认缓存文件损坏,否则这纯粹是添乱。
5.2 “全部升级到最新版”并不总能解决问题
还有一类陷阱来自“升级依赖能解决一切”的想法。我见过有朋友为了处理构建卡顿,把 Flutter SDK、Android Gradle Plugin、Kotlin 版本全部升到最新,结果卡得更厉害。原因是 Flutter 版本和 Android Gradle Plugin 之间存在理性的兼容窗口,新版本 Flutter 往往要求特定范围的 AGP,并不是越新越好。
我的原则是:能用锁定版本的,不轻易动版本;实在需要升级的,一次只升一个。验证完再动下一个,可以避免把多变量的兼容问题搅成一团乱麻。
5.3 可以固化的快速自查清单
最后把这些经验浓缩成一张速查清单,我每次遇到“flutter Android 项目卡住”都会按顺序过一遍:
| 阶段 | 检查项 | 卡住时的常见表现 | 处理建议 |
|---|---|---|---|
| 依赖解析 | pubspec.yaml 里的插件版本 | pub get长时间不动 | 查看 pub 缓存目录,确认是否在下载 |
| Gradle 发行包 | gradle-wrapper.properties 里的 distributionUrl | 卡在 Gradle task,网络流量极小 | 换成可快速访问的镜像,清理.part缓存 |
| 依赖仓库 | settings.gradle 中 repositories 顺序 | 停在某个依赖解析任务 | 镜子源前置,但保留原仓库兜底 |
| JVM/Gradle 环境 | java -version与 AGP 的兼容性 | 构建中途无报错停住 | 统一 JDK 版本,查看 AGP 兼容表 |
| 设备连接 | adb devices输出 | flutter run 等待设备 | 查看是否 authorized,检查 USB 调试授权 |
| 磁盘空间 | 系统磁盘剩余空间 | 构建写到临时文件时卡住 | 清理磁盘,保留足够空间 |
| 缓存损坏 | 本地 Gradle/Flutter 缓存 | 反复卡在同一位置 | flutter clean和./gradlew --stop后重试 |
其实每次把以上这些过一遍之后,很大概率能找到卡住的原因。之前我有过一次比较典型的经历:项目整整卡在Running Gradle task...两个小时,最后发现只是gradle-wrapper.properties里的发行包域名连接不通。改掉那一行,整个构建恢复正常。从那以后我就更坚定地认为,所谓“大神一眼看出问题”,在他们眼里从来就没有玄学,只有步骤和证据。你只要愿意把问题拆细一点,多保留几行日志,距离解决方法就不远了。