本文介绍
tools/android-cli/工具链:如何用一条命令完成「优化前度量 → AI 改布局 → 人工确认 → 优化后验证」的完整闭环。
读者无需事先了解本仓库,具备 Android 开发与基本命令行经验即可。
1. 背景:我们在解决什么问题
1.1 布局债务从哪来
做过几年 Android 的同学都熟悉这套画面:列表 item 里套了三层 RelativeLayout,Activity 壳层再来一层,改一个 margin 要翻四个文件。层级深不仅难维护,还会拖慢 measure / layout,列表滑动时更容易掉帧。
把 RelativeLayout 换成 ConstraintLayout 是正道,但手工迁移有几个老问题:
- 说不清有没有变好:改完只能说「感觉快了」,缺少优化前后的数据对比。
-
容易改坏 UI:RL 的
below改成 CL 约束时,经常只写了marginTop,marginStart丢了,界面悄悄挤成一团。 - 改一层动全身:Activity 容器、Fragment 根布局、RecyclerView item 三层嵌套,一次改完很难定位是哪一层引入了测量问题。
-
XML 和 Kotlin 不同步:容器从
RelativeLayout换成ConstraintLayout,findViewById的类型和 import 忘了改,编译或运行时才爆。
1.2 这套工具想做什么
tools/android-cli 不是又一个 Lint 规则,而是一套 可重复执行的工程流水线:
- 在真机或模拟器上,自动装包、打开指定页面,采集 优化前的基线(静态 XML 指标、截图、运行时 View 树、滑动帧率等)。
- 根据基线生成 优化简报,驱动 Cursor Agent 在严格规约下改 XML 和绑定代码。
- 开发者 审查 diff 并确认 后,再跑一轮基线,自动生成 前后对比报告。
试点业务是 私信列表(chat-list-opt),但引擎设计为通用:新业务只要加一个目录和配置文件,就能注册进同一套 pipeline。
1.3 你需要知道的两层目录
整个工具分两块,记住这个分工就够了:
-
common/:所有业务共用的引擎——装包、采基线、调 Agent、出报告、状态机。 -
<业务-id>/(如chat-list-opt):只有这个业务才需要的东西——怎么导航到目标页、改哪些 layout、Skill 里写哪些 @id。
产物写在仓库根的 artifacts/<业务-id>/ 下,每次 before 或 after 会生成一个带时间戳的 run 目录,里面是报告、截图和 JSON 指标。
2. 技术原理:整条链路怎么串起来
2.1 四步流水线,像一次正式的 Code Review
可以把 pipeline 理解成四个必须按顺序走的关卡(optimize 和 confirm 之间可以停很久,由人来看 diff):
before → optimize → confirm → after
度量 改代码 人签字 再度量、出对比
before(第 1 步)
重新 Gradle 打包,把 APK 装到手机,自动导航到目标页面(例如底部 Tab 点进私信列表),在多个场景下截图并采集数据:互关列表、粉丝列表、滚动后等。同时分析你指定的几个 layout XML 文件,统计嵌套深度、View 数量、RelativeLayout 个数。结果写入 artifacts/.../<run_id>/REPORT.md,并钉住这次 run 的 id,供以后对比。
optimize(第 2 步)
根据 before 报告生成 OPTIMIZE_BRIEF.md(给 AI 看的任务说明),然后可选地调用 Cursor Agent。Agent 会同时读两份 Skill 文档:一份讲通用的 RL→CL 规则和间距怎么迁,一份讲这个业务允许改哪些文件、每个 @id 对应什么 Kotlin 类型。改完后 Agent 需要自己跑 compile,过不了不能交差。
confirm(第 3 步)
机器改完的代码,人要过一眼。执行 confirm 会对约定路径做 git diff --stat,并在状态文件里记下「开发者已确认」。没 confirm 就不能 after,避免未经审查的改动进入对比报告。
after(第 4 步)
再次打包、装包、导航、采基线,和 before 那次自动对比,生成 COMPARE.md:XML 深度降了多少、运行时节点少没少、janky 帧有没有改善。
设计上有三条硬原则:先度量再改、改完必须能编译、优化后必须用同一套脚本再量一次。XML 里深度数字变好看,不代表真机上 View 变少,所以静态和运行时指标都要看。
2.2 一次 before/after 里,脚本在手机上做了什么
每次采集基线,引擎内部按固定顺序执行(实现都在 pipeline_run.sh,设备相关能力在 common.sh):
检查环境(android info,没设备就起模拟器)
↓
Gradle 打 debug 包 → android run 安装并启动
↓
等业务 hook:导航到目标页(如私信 Tab)
↓
按场景循环:截图 → 尝试导出 UI 树 → uiautomator dump
↓
分析源码里的 layout XML + 写 REPORT
这里会用到两类外部工具,容易混淆,分开记:
-
adb:老面孔,装包后的am start、input tap、dumpsys、uiautomator dump都走它。 -
android(Android CLI):Cursor / Android Studio 生态里的命令行,本 pipeline 用它做 装包运行(android run)、截屏(android screen capture)、导出无障碍 UI 树(android layout)、起模拟器(android emulator start)。
狐友首页动画多,android layout 经常因为「UI 不 idle」失败。引擎默认 不因此中断:截图和 uiautomator 仍然保留,静态 XML 分析不依赖真机 idle。这是务实降级,不是偷懒——社交 App 上 layout dump 失败很常见。
2.3 AI 优化那一步:为什么用「双层 Skill」
直接把「帮我把布局拍平」扔给大模型,几乎一定会改到 ViewModel、乱删 @id、或者 CL 约束写一半。所以我们把规约写成两份 Markdown Skill:
-
通用 Skill(
common/skills/layout-opt/SKILL.md):所有业务都遵守——先改 item 再改 fragment 最后改 activity,每改一个文件 compile 一次;RL 改 CL 时 padding 壳怎么拆、margin 四个方向都要保留。 -
业务 overlay Skill(如
chat-list-layout-opt/SKILL.md):只对这个业务生效——允许改哪几个 xml、哪个 Kotlin 文件、每个 @id 的类型对照表。
prepare_optimize.py 把 before 的数字填进简报,render_optimize_prompt.py 拼出完整 Agent 提示词,invoke_optimize_agent.sh 调 agent CLI 在仓库里直接改文件。不想用 Agent 可以加 --no-agent,自己打开 Skill 和简报在 IDE 里改。
2.4 状态存在哪
artifacts/<业务>/pipeline-state.json 记录四步走到哪了、before_run_id 是多少、简报路径、Agent 日志等。跑 status 会用人话打印「下一步该执行什么命令」。after 会读 baseline-before.json 里钉住的 run id,保证对比的是同一次优化前基线。
3. 模块说明:代码怎么分工(按脉络读,不按字典查)
下面按 数据怎么流动 介绍主要文件。不必一次读完,用到再翻即可。
3.1 入口:common/run.sh 和业务里的薄 run.sh
你只需要记住一条命令形态:
./tools/android-cli/common/run.sh <业务-id> <子命令>
不带参数执行会列出已注册业务(扫描各目录下的 pipeline.metrics.json)。chat-list-opt/run.sh 只有三行,转调 common/run.sh chat-list-opt,方便老文档里的路径继续能用。
run.sh 负责找到业务目录、设置 PIPELINE_DIR,然后交给 pipeline_run.sh 干实事。
3.2 引擎核心:pipeline_run.sh
这是单文件里最厚的一块(约 700 行),实现 before / optimize / confirm / after / full / status / list / compare 等子命令。启动时会依次加载:
-
common.sh— 设备和 Gradle -
pipeline_state.sh— 读写状态 JSON -
invoke_optimize_agent.sh— 调 Agent - 业务的
config.env— 包名、Gradle task、要分析的 XML 列表等 - 业务的
lib/pipeline_nav.sh— 怎么点进目标页 - 业务的
lib/pipeline_hooks.sh— 至少实现pipeline_phase2_navigate()
full 子命令会把四步串起来并自动 confirm,适合实验;日常开发建议分步跑,方便在 confirm 前看 diff。
3.3 设备与装包:common.sh(重点)
common.sh 是 所有 pipeline 和导航脚本的公共底座。它不直接出现在命令行里,但被 source 之后提供一整套「让 App 出现在正确页面并采数据」的能力。Android 开发同学最值得花时间理解的是下面几块。
装包与启动:install_and_launch
before/after 的第一步都会调用它,逻辑可以概括成:
先用 Gradle 打出 debug APK(before/after 会 强制重新打,避免对比时包不一致)。然后调用 android run --apks=... 安装,并尽量带上 --activity 启动 Splash——狐友的 MainActivity 没有 launcher intent,必须从 Splash 进。装完后脚本还会用 adb shell am start 再拉一次前台,并自动处理华为等机型上烦人的 「安装成功」确认页(点屏幕下方 + 返回键,最多重试几次)。
如果设置了 ANDROID_RUN_DEBUG=1,android run 会带 --debug,App 会卡在 Waiting For Debugger;自动化基线默认关闭这项。
等 App 真的进到主页:wait_for_main_activity
启动后要等开屏广告、登录页过去,脚本用 dumpsys window / dumpsys activity 轮询,直到前台是目标包名且 resumed 的是 MainActivity(类名可在 config.env 里配)。这段时间也会顺手 dismiss 安装器界面。
导航点击:tap_view_by_resource_id
业务导航(例如在 pipeline_nav.sh 里「点底部私信 Tab」)最终会调到这个函数。它会在超时时间内反复尝试:先 uiautomator dump 找 @id,找不到再 android layout 导出 JSON 用文案或 id 算中心点,然后 adb shell input tap。坐标和用的哪种方法会记在 artifacts/.../runtime/nav-*.json,方便事后排查「点歪了」。
私信列表配置为优先 uiautomator(PREFER_UIAUTOMATOR_FOR_RESID=1),因为狐友首页 layout dump 常常 idle 失败。
导出 UI 树:layout_dump_to_file
对 android layout -p -o <文件> 的封装:失败会重试,可选在 dump 前临时关掉系统动画。仍失败且 LAYOUT_OPTIONAL=1 时只打日志,不让整个 pipeline 失败。输出的 JSON 是一串扁平的无障碍元素,后面 parse_tab_center.py 用来按 Tab 文案算点击位置。
截图与其它
pipeline_run.sh 里每个场景直接调 android screen capture -o ...。性能数据在采完场景后通过 collect_gfxinfo 滑几次列表再 dumpsys gfxinfo 拿到 janky 比例和帧耗时分位。
其余函数(记 Git commit、钉住 baseline run id、找 APK 路径等)都是为报告服务的辅助,需要时到 common.sh 里搜函数名即可。
Android CLI 在本项目中的用法小结
| 命令 | 干什么 |
|---|---|
android info |
流水线开头检查环境 |
android emulator start <AVD> |
没有真机时拉模拟器 |
android describe --project_dir=... |
可选,读工程信息,失败不阻断 |
android run --apks=... [--activity=...] |
安装并启动 |
android layout -p -o <path> |
导出当前页无障碍树 JSON |
android screen capture -o <path> |
截图 |
和 adb 的配合关系:装包、layout、截图走 android;点击、滑动、dumpsys、uiautomator 走 adb。
3.4 业务侧:以私信列表为例
chat-list-opt/ 里和引擎对接的只有三块:
config.env
包名、Splash/Main Activity、Gradle 任务、要分析的 layout 文件列表、底部 Tab 的 resource-id(hometab_chat)、等待秒数、Skill 路径等。改业务行为大多改这个文件。
lib/pipeline_nav.sh
实现 navigate_to_chat_tab:等 MainActivity 稳定后,只通过 @id 点私信 Tab,不用底部坐标硬点。还有互关/粉丝内层 Tab 的切换逻辑,供多场景采集使用。
lib/pipeline_hooks.sh
pipeline_phase2_navigate 在 phase2 被引擎调用;pipeline_between_scenarios 在采「互关 → 粉丝 → 滚动」时切 Tab 或滑动;pipeline_baseline_ready_check 判断关键截图是否已生成。
pipeline.metrics.json
业务的「身份证」:有它才会出现在 list-pipelines 里。里面还有报告标题、采集场景列表、confirm 时要 diff 的路径、Agent 用的 Skill 路径等。
prepare_optimize.py
把 before 的 layouts.depth.json 和业务目标写成 OPTIMIZE_BRIEF.md,是 optimize 步骤的输入。
skills/chat-list-layout-opt/SKILL.md
给 Agent 看的业务说明书:能改哪三个 xml、五个 Kotlin 文件、每个 @id 的类型、三层 layout 修改顺序和间距示例。
3.5 分析与报告:Python 脚本
这几份脚本不参与装包,只处理已经采下来的文件:
-
analyze_layout_xml.py:读源码里的 layout xml,算深度、View 数、RelativeLayout 个数、有没有 wrap_content 包 match_parent 的坏味道。 -
analyze_view_hierarchy.py:读 uiautomator 的 xml,统计整屏或某个 RecyclerView 子树下的节点数。 -
generate_report.py:拼出单次 run 的REPORT.md和metrics.json。 -
compare_runs.py:两次 run 放一起,出COMPARE.md和 delta。 -
parse_tab_center.py/parse_uiautomator_resid.py:给导航用的坐标,被pipeline_nav.sh间接调用。
3.6 扩展新业务
复制 chat-list-opt 目录,改 pipeline_id、导航、layout 列表和 overlay Skill,即可注册第二个 pipeline。更系统的步骤见 common/skills/scaffold-layout-opt-pipeline/SKILL.md。
4. 实战演练:私信列表走一遍
4.1 环境
- 连接真机或准备好
config.env里的模拟器名(默认Pixel_8_Pro_API_35)。 - 终端能执行:
adb devices、android info、python3、仓库根下./gradlew。 - 若要用自动 Agent:
agent login。
4.2 推荐流程(分步)
# 看有哪些业务
./tools/android-cli/common/run.sh
# 1. 优化前基线(约 5~15 分钟,会重新打 debug 包)
./tools/android-cli/common/run.sh chat-list-opt before
# 看进度和报告路径
./tools/android-cli/common/run.sh chat-list-opt status
cat artifacts/chat-list-opt/*/REPORT.md # 选最新的 run 目录
# 2. 生成简报并让 Agent 改布局(或 --no-agent 仅生成简报)
./tools/android-cli/common/run.sh chat-list-opt optimize
# 3. 本地看 diff,确认间距、类型没问题
git diff app/src/main/res/layout/
git diff app/src/main/java/hy/sohu/com/app/chat/view/conversation/
./tools/android-cli/common/run.sh chat-list-opt confirm
# 4. 优化后再采一轮,自动出对比
./tools/android-cli/common/run.sh chat-list-opt after
对比报告在:artifacts/chat-list-opt/<after_run_id>/COMPARE.md。
Agent 失败可重试:optimize --agent-only(简报已存在时)。
4.3 你跑的时候心里可以有个 checklist
- before 的截图里列表间距是否正常(后面优化要靠肉眼对照)。
- optimize 之后 compile 是否过,Kotlin 里容器类型是否跟 xml 一致。
- confirm 不要跳过,除非你在调试 pipeline 本身。
- after 的 COMPARE 里,静态深度和运行时节点是否 同方向 变好;如果 XML 变好但运行时没变,要怀疑 CL 约束写缺了。
5. 总结
tools/android-cli 把「布局拍平」从个人经验活变成 可复制的工程流程:脚本负责统一装包、导航、采数和对比;Skill 文档负责把 RL→CL 最容易踩的坑写死;人在 confirm 环节守住视觉和类型安全。
对 Android 同学来说,日常只需熟悉 四条命令(before → optimize → confirm → after)和 业务 config 改哪几项。底层 common.sh 和 Android CLI 的细节,在需要接新业务或排查导航失败时再深入即可。
当前适用范围是 XML View 体系的存量优化,不是 Compose 迁移;导航逻辑因 App 而异,新业务必须自己写 pipeline_nav.sh 和 hooks。
6. 参考
6.1 命令速查
./tools/android-cli/common/run.sh # 列出业务
./tools/android-cli/common/run.sh <id> before
./tools/android-cli/common/run.sh <id> optimize [--no-agent | --agent-only]
./tools/android-cli/common/run.sh <id> confirm [--message "..."]
./tools/android-cli/common/run.sh <id> after
./tools/android-cli/common/run.sh <id> status | list | compare ...
./tools/android-cli/common/run.sh <id> full # 全自动,实验用
退出码:10 环境/设备,20 构建安装,30 导航,40 基线不完整,50 未 confirm。
6.2 仓库内延伸阅读
- 通用 Agent 规约:
common/skills/layout-opt/SKILL.md - 私信列表业务规约:
chat-list-opt/skills/chat-list-layout-opt/SKILL.md - 新建业务脚手架:
common/skills/scaffold-layout-opt-pipeline/SKILL.md
6.3 目录与产物(备查)
tools/android-cli/
common/ # 引擎:run.sh, pipeline_run.sh, common.sh, *.py, skills/
chat-list-opt/ # 示例业务:config.env, metrics.json, lib/, prepare_optimize.py
artifacts/<业务-id>/
pipeline-state.json
OPTIMIZE_BRIEF.md
baseline-before.json
<run_id>/
REPORT.md, COMPARE.md, metrics.json
static/layouts.depth.json
runtime/*.png, layout-*.json, view-hierarchy-*.xml
6.4 外部链接
- ConstraintLayout 指南
- 布局性能
- Cursor 文档(Agent CLI)
文档版本:2026-07-07