Android CLI · 布局层级优化 Pipeline

本文介绍 tools/android-cli/ 工具链:如何用一条命令完成「优化前度量 → AI 改布局 → 人工确认 → 优化后验证」的完整闭环。
读者无需事先了解本仓库,具备 Android 开发与基本命令行经验即可。


1. 背景:我们在解决什么问题

1.1 布局债务从哪来

做过几年 Android 的同学都熟悉这套画面:列表 item 里套了三层 RelativeLayout,Activity 壳层再来一层,改一个 margin 要翻四个文件。层级深不仅难维护,还会拖慢 measure / layout,列表滑动时更容易掉帧。

RelativeLayout 换成 ConstraintLayout 是正道,但手工迁移有几个老问题:

  • 说不清有没有变好:改完只能说「感觉快了」,缺少优化前后的数据对比。
  • 容易改坏 UI:RL 的 below 改成 CL 约束时,经常只写了 marginTopmarginStart 丢了,界面悄悄挤成一团。
  • 改一层动全身:Activity 容器、Fragment 根布局、RecyclerView item 三层嵌套,一次改完很难定位是哪一层引入了测量问题。
  • XML 和 Kotlin 不同步:容器从 RelativeLayout 换成 ConstraintLayoutfindViewById 的类型和 import 忘了改,编译或运行时才爆。

1.2 这套工具想做什么

tools/android-cli 不是又一个 Lint 规则,而是一套 可重复执行的工程流水线

  1. 在真机或模拟器上,自动装包、打开指定页面,采集 优化前的基线(静态 XML 指标、截图、运行时 View 树、滑动帧率等)。
  2. 根据基线生成 优化简报,驱动 Cursor Agent 在严格规约下改 XML 和绑定代码。
  3. 开发者 审查 diff 并确认 后,再跑一轮基线,自动生成 前后对比报告

试点业务是 私信列表chat-list-opt),但引擎设计为通用:新业务只要加一个目录和配置文件,就能注册进同一套 pipeline。

1.3 你需要知道的两层目录

整个工具分两块,记住这个分工就够了:

  • common/:所有业务共用的引擎——装包、采基线、调 Agent、出报告、状态机。
  • <业务-id>/(如 chat-list-opt:只有这个业务才需要的东西——怎么导航到目标页、改哪些 layout、Skill 里写哪些 @id。

产物写在仓库根的 artifacts/<业务-id>/ 下,每次 beforeafter 会生成一个带时间戳的 run 目录,里面是报告、截图和 JSON 指标。


2. 技术原理:整条链路怎么串起来

2.1 四步流水线,像一次正式的 Code Review

可以把 pipeline 理解成四个必须按顺序走的关卡(optimizeconfirm 之间可以停很久,由人来看 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 startinput tapdumpsysuiautomator 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:

  • 通用 Skillcommon/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.shagent 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 等子命令。启动时会依次加载:

  1. common.sh — 设备和 Gradle
  2. pipeline_state.sh — 读写状态 JSON
  3. invoke_optimize_agent.sh — 调 Agent
  4. 业务的 config.env — 包名、Gradle task、要分析的 XML 列表等
  5. 业务的 lib/pipeline_nav.sh — 怎么点进目标页
  6. 业务的 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=1android 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.mdmetrics.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 devicesandroid infopython3、仓库根下 ./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 同学来说,日常只需熟悉 四条命令beforeoptimizeconfirmafter)和 业务 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 外部链接


文档版本:2026-07-07

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容