告别 Play Console 手动上传:从模拟器截图到自动发版的完整流水线

TrendingAI 做完一次整体 UI 改版后,商店截图、官网演示视频、Play 商品详情全都要跟着换一遍。以前这套流程的每一步都是手工活:在 Android Studio 的模拟器面板里一张张点「带外壳截图」,然后打开 Play Console 一张张拖进上传框。这次我把整条链路自动化了,最终的状态是:

  • 改版换截图:跑一条命令,双语 12 张带官方外壳的商店图全部生成,push 到 main 后 CI 自动同步到 Play 商品详情------Play Console 一次都不用打开;
  • 发新版本:打一个 git tag,CI 构建 AAB、生成发版说明、上传 production 轨道送审,过审自动上线。

这篇按操作顺序拆解整条流水线:准备截图环境 → 命令行套外壳 → 录演示视频 → 接入 fastlane supply → CI 挂两条链路。每个阶段给出要做什么、怎么验证做对了,以及实测撞过的坑。其中「命令行给截图套官方设备外壳」这一步,网上几乎没有现成资料,是全文最值得带走的部分。文末照例有操作前 Checklist 和坑速查表。

全局:流水线长什么样

bash 复制代码
UI 改版完成
  ├─ 截图流水线:模拟器截图(demo mode 清状态栏)
  │     → Pillow 套官方设备外壳 → TinyPNG 压缩
  │     → 分发到 fastlane/metadata + README assets + 官网 OG 图
  │
  ├─ 视频流水线:scrcpy 录屏(慢手势)
  │     → ffmpeg 压制 + 抽首帧 poster → 官网 <video> 自动循环播放
  │
  └─ Git 触发 CI(两条独立链路)
        ├─ push 到 main 且 fastlane/metadata 有变更
        │     → play_listing.yml 同步 Play 商品详情(文案 + 截图)
        └─ 打正式版 tag
              → android_release.yml 上传 AAB + 发版说明到 production 轨道

三条核心机制,理解了它们,后面每一步都是顺理成章:

  1. 设备外壳是本地素材 + 简单图层合成。Android Studio「带外壳截图」用的素材就在它的安装目录里,每台设备三个文件(机身底图、屏幕遮罩、偏移量描述),用 Pillow 十行代码就能做出和 IDE 手动截图完全同规格的输出。
  2. 仓库里的 fastlane/metadata 是商品详情的唯一事实源 。fastlane 的这套目录结构(title.txtfull_description.txtimages/phoneScreenshots/......)同时被 F-Droid 和 Google Play 消费------F-Droid 直接从仓库读,Play 靠 fastlane supply 推送。文案和截图只在仓库改,改完自动同步,Play 后台从此只读。
  3. 发版和商店物料是两个生命周期。版本随 tag 发布,截图文案偶尔才改。CI 也应该照这个现实拆成两条链路,而不是把 listing 同步塞进发版流程里做条件判断。

一、准备干净的截图环境

商店截图的状态栏必须干净统一:固定时钟、满电、满信号、无通知图标。五张图各有各的时间电量,是最容易被忽略的廉价感来源;对录屏来说更致命------循环播放的视频里状态栏时间跳一下,立刻穿帮。

Android 的 demo mode 一组广播就能搞定(模拟器和多数真机通用):

bash 复制代码
adb shell settings put global sysui_demo_allowed 1
adb shell am broadcast -a com.android.systemui.demo -e command enter
adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 0917
adb shell am broadcast -a com.android.systemui.demo -e command battery -e level 100 -e plugged false
adb shell am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level 4 -e fully true
adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile hide
adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false
adb shell am broadcast -a com.android.systemui.demo -e command status -e volume hide -e bluetooth hide
adb shell svc power stayon true   # 顺手防熄屏

几个细节:

  • network -e mobile hide 别漏------模拟器默认会在状态栏挂一个「3G」图标,第一批截图出来才发现的话,全部重截;
  • 时钟随便定,我定成 09:17 是因为 TrendingAI 的每日精选在北京时间 9 点生成,算个彩蛋;
  • 双语截图不需要切系统语言。如果你的 App 有应用内语言设置(per-app language 或自研的都行),直接在 App 里切,省掉系统 locale 折腾和它带来的进程重启。

验证:截一张,确认状态栏只有「9:17 + WiFi + 满电」,没有时间流动、没有通知点、没有移动网络图标。

⚠️ 坑:demo mode 只管状态栏,管不了 App 内的时间。列表里的「3 分钟前」之类相对时间照样在走,同一批截图尽量一次截完,别隔天补。

二、命令行给截图套官方设备外壳

这是全文的核心。adb exec-out screencap 拿到的是裸屏幕图,而商店图要的是带手机边框、圆角、挖孔摄像头的效果。过去我的做法是在 Android Studio 的模拟器工具栏里手动点「带外壳截图」------它没有对应的命令行接口,于是双语 × 多场景 × 手机平板的矩阵全靠手点。

其实 Android Studio 的外壳素材就躺在本机安装目录里:

bash 复制代码
/Applications/Android Studio.app/Contents/plugins/android/resources/device-art-resources/<设备名>/
├── back.webp   # 机身底图(含边框、按键、阴影),比屏幕大一圈
├── mask.webp   # 前景遮罩:屏幕区域透明,圆角边缘和挖孔摄像头不透明
└── layout      # 文本描述文件:屏幕分辨率、在机身上的偏移量、成品尺寸

pixel_9 为例,layout 里写着:屏幕 1080×2424,贴在机身的 (55, 58) 位置,成品 1198×2531。合成算法就三步:

css 复制代码
back.webp 打底 → 裸截图按偏移量方形贴入 → mask.webp 前景叠加(盖出圆角和摄像头)

完整脚本(Pillow 实现,支持手机和平板):

python 复制代码
#!/usr/bin/env python3
"""给模拟器裸截图套 Android Studio 官方设备外壳。"""
import argparse
from pathlib import Path
from PIL import Image

ART_ROOT = Path(
    "/Applications/Android Studio.app/Contents/plugins/android/resources/device-art-resources"
)

# 屏幕分辨率与偏移量取自各设备 device-art 的 layout 描述文件
DEVICES = {
    "pixel_9": {"screen": (1080, 2424), "offset": (55, 58)},
    "pixel_tablet": {"screen": (2560, 1600), "offset": (119, 117)},
}

def frame(shot_path: Path, device: str, out_dir: Path) -> Path:
    spec = DEVICES[device]
    art = ART_ROOT / device
    shot = Image.open(shot_path).convert("RGBA")
    if shot.size != spec["screen"]:
        raise SystemExit(f"{shot_path.name}: 分辨率 {shot.size} 与 {device} 不符")
    back = Image.open(art / "back.webp").convert("RGBA")
    mask = Image.open(art / "mask.webp").convert("RGBA")
    back.paste(shot, spec["offset"])          # 方形贴入
    back.alpha_composite(mask, spec["offset"])  # 前景盖出圆角 + 挖孔
    out = out_dir / shot_path.name
    back.save(out)
    return out

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("shots", nargs="+", type=Path)
    ap.add_argument("-d", "--device", choices=DEVICES, default="pixel_9")
    ap.add_argument("-o", "--out", type=Path, default=Path("framed"))
    args = ap.parse_args()
    args.out.mkdir(parents=True, exist_ok=True)
    for shot in args.shots:
        print("framed:", frame(shot, args.device, args.out))

if __name__ == "__main__":
    main()

配合 adb 驱动导航(input tap / input swipe 走到目标页面再 screencap),整个截图矩阵就是一个 shell 循环的事。

验证:输出尺寸应与 Android Studio 手动「带外壳截图」完全一致(pixel_9 → 1198×2531,pixel_tablet → 2798×1837)。如果你以前手动截过,新旧文件分辨率对得上就说明套壳无损。

⚠️ 坑:mask.webp 是前景遮罩,不是抠图蒙版 。我第一版把它的 alpha 当作截图的粘贴蒙版用(paste(shot, offset, mask_alpha)),结果四角冒出白色方块、挖孔位置露出灰色圆斑。看一眼像素值就明白了:mask 在屏幕中心区域是全透明(让截图透出来),在圆角边缘和摄像头位置是不透明的机身色(要盖在截图上面 )。正确用法是先方形贴图、再 alpha_composite 叠 mask。
⚠️ 坑:截图入库前先压缩。Pillow 直出的 PNG 一张 500-800KB,TinyPNG 过一遍普遍降 60% 到 150-250KB,肉眼无差。商店图会进 git 仓库和 F-Droid 构建,体积值得省。

三、录一段能上官网的演示视频

官网的应用展示,动图远比静态截图有说服力。现代做法不是 GIF,而是静音自动循环的 MP4:

html 复制代码
<video
  src="https://harlon.wang/app-demo.mp4"
  poster="/app-demo-poster.jpg"
  autoplay loop muted playsinline
  preload="metadata"
  class="w-64 rounded-3xl border-8 border-neutral-900 shadow-2xl"
></video>

手机壳直接用 CSS 画(粗深色边框 + 大圆角 + 阴影),视频只录纯屏幕内容------比把壳烧进视频灵活得多,改设计不用重录。

录制。如果有真机,用系统自带录屏最省事(注意关掉「显示点按位置」和声音采集)。我这次用模拟器全自动录,把三种方式都趟了一遍,帧率差距巨大:

方式 实测帧率 结论
adb shell screenrecord ~3fps 幻灯片,不可用
adb emu screenrecord(宿主侧 WebM) ~10fps 勉强,仍卡顿
scrcpy --record 15-22fps 可用
bash 复制代码
scrcpy -s emulator-5554 --no-playback --no-audio \
  --record=demo.mp4 --max-fps=60 --video-bit-rate=12M --time-limit=19 &
# 同时用 adb input swipe/tap 驱动演示脚本......
wait

20fps 对滚动画面来说仍偏低,但有个简单技巧能把观感拉回来:放慢手势input swipe 的持续时间拉到 1800ms、单次位移控制在 650px 左右,内容移动速度慢了,每帧位移只有几个像素,感知上就顺滑了。快速甩动 + 低帧率才是卡顿感的来源。

压制。录完用 ffmpeg 出片 + 抽首帧当 poster:

bash 复制代码
ffmpeg -ss 0.3 -to 17.8 -i demo.mp4 \
  -vf "scale=720:-2" -c:v libx264 -crf 28 -preset slow \
  -pix_fmt yuv420p -an -movflags +faststart app-demo.mp4
ffmpeg -ss 0.4 -i demo.mp4 -frames:v 1 -vf "scale=720:-2" app-demo-poster.jpg

17 秒的成片压到 750KB 左右,直接进网站仓库走 CDN,不需要对象存储。

验证ffprobe -select_streams v -show_entries frame=pts_time 数一下滚动段的每秒帧数;成片在 240-300px 显示宽度下播放流畅、状态栏时间不跳。

⚠️ 坑:scrcpy 不能强杀kill -INT 时机不对会来不及写 MP4 的 moov atom,文件直接报 moov atom not found 作废。用 --time-limit 让它自然结束。
⚠️ 坑:内容脚本一镜一事。每段 5-8 秒只讲一个卖点,录之前把演示数据准备好(列表有新鲜内容、对话有预设问题),录完检查有没有把录屏指示器、悬浮窗之类的穿帮元素录进去。

四、接入 fastlane supply

supply 是 fastlane 里专管 Play 上架物料的子工具,能上传商品详情文案、截图、AAB 和发版说明。你的仓库如果已经为 F-Droid 维护了 fastlane/metadata/android/ 目录,那一份数据两个商店直接通用,连 changelog 的 versionCode 命名都是同一套约定。

服务账号与授权(一次性,约 15 分钟):

  1. Google Cloud 控制台任意项目里启用 Google Play Android Developer API,创建服务账号(无需任何项目角色),下载 JSON 密钥;
  2. Play Console →「用户和权限」→ 邀请新用户,填服务账号邮箱,应用权限只勾目标 App 的「管理商店发布」(只同步物料的话不要给发版权限,最小化起步);
  3. 后续要 CI 自动发版时,再补一项「发布为正式版、排除设备以及使用 Google Play 应用签名功能」。

验证:分两层,缺一不可------

bash 复制代码
# 第一层:密钥能连上 API
fastlane run validate_play_store_json_key json_key:key.json
# 第二层:对目标 App 真有权限(拉一次线上物料)
fastlane supply init --package_name <你的包名> --json_key key.json

⚠️ 坑:validate 成功 ≠ 有应用权限validate_play_store_json_key 只验证 API 连通性;服务账号没被邀请进对应开发者账号时,它照样报成功,而 supply init 一跑就是 Google Api Error: The caller does not have permission。以 init 能拉到数据为准。

首次接入必做:先 init 对齐,再上传supply init 会把线上现有物料完整拉下来,逐文件和仓库 diff。这一步救了我一次------我仓库里的文案和 Play 后台的不一致(后台是老定位的存量文案,仓库是新口径),而 supply 的截图上传是先删线上再传本地 的整体替换,文案同理。如果不先对齐就上传,分叉的那一边会被静默覆盖。动手前先定好唯一事实源的方向(我的选择:一律以仓库为准,Play 后台从此只读),把另一边回写对齐,再开始同步。

⚠️ 坑:平板截图的目录位置别猜 。fastlane 有 sevenInchScreenshotstenInchScreenshots 两个平板位,你后台实际用的是哪个,supply init 拉下来一看便知------我的在 7 寸位,直觉放 10 寸就白传了。

日常同步命令(先加 --validate_only true 干跑):

bash 复制代码
fastlane supply --package_name <你的包名> \
  --json_key key.json \
  --metadata_path fastlane/metadata/android \
  --skip_upload_apk true --skip_upload_aab true \
  --skip_upload_changelogs true

五、CI 挂两条链路

本地能跑通后,把它交给 GitHub Actions。密钥内容存进 repo secret(如 PLAY_SERVICE_ACCOUNT_JSON),运行时通过 SUPPLY_JSON_KEY_DATA 环境变量传给 fastlane------它原生识别这个变量,密钥全程不落盘、不出现在命令行参数和日志里。

链路一:商品详情同步。文案截图偶尔才改,改了就该立即生效,不该等发版:

yaml 复制代码
name: Play Listing Sync
on:
  push:
    branches: [main]
    paths:
      - 'fastlane/metadata/android/**'
  workflow_dispatch: # 手动补跑入口

jobs:
  sync-listing:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
      - name: Sync listing to Google Play
        env:
          SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
        run: |
          gem install fastlane --no-document
          fastlane supply --package_name <你的包名> \
            --metadata_path fastlane/metadata/android \
            --skip_upload_apk true --skip_upload_aab true \
            --skip_upload_changelogs true

⚠️ 坑:gem install fastlane 在 runner 上直接跑会挂 ------You don't have write permissions for the /var/lib/gems/3.2.0 directory。ubuntu-latest 的系统 Ruby 装 gem 需要 sudo,加一步 ruby/setup-ruby 用工具链 Ruby 即可。这个雷我是在 listing 链路首跑时踩的,发版链路里同款命令还没来得及炸就顺手修了。

链路二:发版。在现有 release 工作流(构建 + 签名 + GitHub Release)的末尾追加一步,正式版 tag 上传 AAB 到 production 轨道:

yaml 复制代码
      - name: Publish to Google Play
        env:
          SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
        run: |
          # 预发布不上 Play,灰度走 GitHub Release 附件
          if [[ "${{ github.ref_name }}" =~ (alpha|beta|rc) ]]; then
            echo "Prerelease tag,跳过 Play 发布"; exit 0
          fi

          AAB_PATH=$(find androidApp/build/outputs/bundle/playRelease -name "*.aab" | head -n 1)
          gem install fastlane --no-document
          fastlane supply --package_name <你的包名> \
            --aab "$AAB_PATH" \
            --track production \
            --metadata_path fastlane/metadata/android \
            --skip_upload_apk true \
            --skip_upload_metadata true \
            --skip_upload_images true \
            --skip_upload_screenshots true

注意两条链路的 skip 参数正好互补:listing 链路跳过包和 changelog,发版链路跳过文案和截图。changelog(changelogs/<versionCode>.txt)跟着 AAB 走,因为发版说明必须挂在具体 versionCode 上------listing 链路没有包,传了也挂不上。

发版说明我加了一层兜底:优先读仓库里人工写好的 changelog 文件,不存在时从 App 内置的更新说明 JSON 就地生成(逐条累加、超过 Play 的 500 字符上限就整条丢弃,不截半句),只喂给 supply、不回写仓库。人工写的质量优先,兜底只防漏。

验证:listing 链路推一次 metadata 变更看全绿;发版链路等下一个正式版 tag 实战(重点看 AAB 上传和 changelog 挂载两行日志)。

操作前 Checklist

  • demo mode 已开:固定时钟、满电、隐藏通知、隐藏移动网络图标
  • App 语言已切到目标 locale(优先应用内切换)
  • 裸截图分辨率与设备原生一致(套壳脚本会校验)
  • 套壳输出与旧商店图同规格(有历史图可对照)
  • PNG 已过 TinyPNG 再入库
  • 录屏用 scrcpy + --time-limit,手势放慢,状态栏时间不跳
  • 首次接 supply:先 supply init 拉线上对齐,定好文案唯一事实源的方向
  • supply 正式上传前先 --validate_only true 干跑
  • 服务账号权限最小化:只同步物料就别勾发版
  • CI 密钥走 secret + SUPPLY_JSON_KEY_DATA 环境变量,不落盘

坑速查表

症状 根因 修复 章节
套壳后四角白色方块、挖孔位置灰斑 把 mask.webp 当抠图蒙版用了 先方形贴图,再 alpha_composite 叠 mask(它是前景)
状态栏出现 3G 图标 / 时间电量不统一 没开 demo mode 或漏了 mobile hide 补全 demo mode 广播组
模拟器录屏像幻灯片(~3fps) adb screenrecord 在模拟器上只按画面更新出帧 换 scrcpy --record
录制文件报 moov atom not found scrcpy 被强杀,没来得及 finalize --time-limit 自然结束
密钥 validate 成功但 supply 报 caller does not have permission 服务账号没被邀请进开发者账号 / 缺应用权限 Play Console 用户和权限里邀请并勾「管理商店发布」
平板截图上传了但商店没变化 放错目录(sevenInch vs tenInch) supply init 拉线上确认实际使用的目录
上传后线上文案被换成旧版 仓库与后台分叉,supply 是整体替换 先 init 对齐、定唯一事实源,再开始同步
CI 报 You don't have write permissions for /var/lib/gems runner 系统 Ruby 装 gem 需 sudo ruby/setup-ruby 步骤

这条流水线搭完后,商店物料从「改版后最不想碰的杂活」变成了流程的自然末端:截图脚本一跑、push、tag,剩下的交给 CI。如果你的项目也同时上架 F-Droid 和 Play,fastlane/metadata 这份一鱼两吃的元数据会让收益翻倍。


本文首发于 harlon.wang,转载请注明出处。

相关推荐
weixin_440784112 小时前
【IntentSeivice实现原理】
android·java·开发语言·intentservice
delta_hell2 小时前
【阅读源码--Android】动画之辅助类
android·源码·animator
沫璃染墨2 小时前
《从零入门Linux系统篇(十四):系统工具篇·五——Git版本控制:从版本管理到协同开发》
linux·运维·服务器·git·gitee·github
fthux3 小时前
装闭 RenoPit 源码解析(02):AI装修闭坑系统数据库与模型设计
人工智能·ai·开源·github·open source·renopit
消失的旧时光-19436 小时前
第 2 篇:Android 控制屏为什么与机器人主控使用 TCP 长连接?
android·tcp/ip·机器人
小Ti客栈6 小时前
MySQL查询原理:从Server到InnoDB
android·mysql·adb
本地化文档6 小时前
skbuild-docs-l10n
python·github·gitcode·sphinx
minhuan6 小时前
大模型应用CI/CD全流程解析:打通模型训练、评估、部署自动化,应用持续迭代实践23.7
ci/cd·自动化·大模型应用·大模型应用ci/cd全流程解析·大模型应用工程体系
XS0301066 小时前
GitHub/Gitee 团队协作笔记
笔记·gitee·github