自己装 Google Play 商店
ModAVD 用的系统镜像是 Google 的「Google APIs」版 Android 14,自带 Google Play 服务,但没有 Play 商店。要从 Play 商店下载游戏、在商店里登录 Google 账号的,可以照这一页,把 Google 官方「Google Play」版镜像里的商店装进自己的实例。管理器以后打算做成新建实例时可选,在那之前先用这个办法。
先看风险
见本页最后的风险。建议新建一台实例专门来装,不要先在存着重要游戏数据的实例上试。
装的是什么
从 Google 官方的 system-images;android-34;google_apis_playstore;x86_64 r14 里取五个文件,放进实例自己的系统覆盖层。这个镜像和 ModAVD 现在用的镜像是同一个安卓版本、同一期构建。
| 文件 | 是什么 |
|---|---|
/product/priv-app/Phonesky/Phonesky.apk | Play 商店 |
/product/priv-app/PrebuiltGmsCore/PrebuiltGmsCore.apk | Google Play 服务,换成和商店同一个签名的那份(版本不变,都是 23.18.18) |
/system_ext/priv-app/GoogleServicesFramework/GoogleServicesFramework.apk | Google 服务框架(GSF),同上 |
/product/priv-app/PartnerSetupPrebuilt/PartnerSetupPrebuilt.apk | Google 合作伙伴设置,同上;不换的话它在后台反复崩溃 |
/product/etc/security/fsverity/play_store_fsi_cert.der | 商店用的 fs-verity 证书 |
另外把镜像自带的许可证检查占位应用 /product/app/LicenseChecker 藏起来:它的包名也是 com.android.vending,不藏会和商店撞。
为什么要连 Play 服务一起换、为什么不能直接 adb install,见为什么要这样装。
开始前确认
- ModAVD 0.36.0 或更新,实例带 arm64 执行桥(新建时没加
--no-bridge就是带的)。只有带桥的实例按可写系统镜像开机,商店才放得进去。 - 电脑上要有约 5 GB 临时空间(压缩包 1.5 GB,解出来的
system.img3.1 GB);实例的数据盘要有约 3.5 GB 空间放临时文件。装完都可以删。 - 下面的命令都在 PowerShell 里运行。先进 ModAVD 文件夹,设好三个变量,后面的命令直接复制就行:
cd D:\ModAVD # 换成你的 ModAVD 文件夹
$i = 1 # 换成实例编号
$dl = 'D:\gplay' # 放下载文件的文件夹,路径只用英文
New-Item -ItemType Directory -Force $dl | Out-Null1. 下载 Play 版镜像并核对
curl.exe -L -o "$dl\x86_64-34_r14.zip" https://dl.google.com/android/repository/sys-img/google_apis_playstore/x86_64-34_r14.zip
(Get-FileHash "$dl\x86_64-34_r14.zip" -Algorithm SHA1).Hash- 文件 1,510,752,654 字节,SHA-1 要是
01D32617FD1937E540FAF8731D70CF50E35AF854。不一致就删掉重下。 - 把
dl.google.com/android/repository/换成googledownloads.cn/android/repository/或mirrors.cloud.tencent.com/AndroidSDK/,也能下到同一个文件。 - 这个镜像受 Google《Android 软件开发套件许可协议》约束,就是补全组件时显示、存在
downloads\android-sdk-license.txt里的那一份。
2. 解出 system.img
tar -xf "$dl\x86_64-34_r14.zip" -C $dl x86_64/system.img得到 $dl\x86_64\system.img,3,101,687,808 字节。
3. 生成安装脚本
整段复制运行,会在 $dl 里生成 playstore.sh。它在实例里用 root 运行:只读挂上 system.img 里的两个分区,核对五个文件的 SHA-256,对上了才放进实例。
$sh = @'
#!/system/bin/sh
# Put the Google Play Store of Google's "Google Play" system image (android-34 x86_64 r14) into a ModAVD instance.
# Run as root in the instance: su 0 sh /data/local/tmp/playstore/playstore.sh install|status|gsfid|uninstall
W=/data/local/tmp/playstore
IMG=$W/system.img
U=/mnt/scratch/overlay
# file sha256 in the Play image r14
FILES="
product/priv-app/Phonesky/Phonesky.apk 2616771999b1cde77fd000ae17a43784dded1f76b21ec12881137c84d5313dd2
product/priv-app/PrebuiltGmsCore/PrebuiltGmsCore.apk 63aaf087cdfad32fd9eca45f36966afe2f44db7e125e099f3e344ca99cee13c2
system_ext/priv-app/GoogleServicesFramework/GoogleServicesFramework.apk 58999d24e4dbdde61f90b59a89c86edabc69e0908ddaaa2742091493d7e3b975
product/priv-app/PartnerSetupPrebuilt/PartnerSetupPrebuilt.apk 20190a60924f36baae9ef9b7687691c445dd9a764b42c4751474293baf5ea15c
product/etc/security/fsverity/play_store_fsi_cert.der 37aabd485044e3af6d84755268faf8392e226ca0d188be6a80335525ce0ec6ff
"
STUB=product/app/LicenseChecker
die() { echo "FAIL: $*"; exit 1; }
writable() {
remount >/dev/null 2>&1
for p in product system_ext; do
touch /$p/.modavd-w 2>/dev/null && rm /$p/.modavd-w || die "/$p is not writable. Only instances with the arm64 bridge boot with a writable system; see the guide."
done
}
mount_img() {
[ -f "$IMG" ] || die "$IMG not found: push system.img first"
mkdir -p $W/product $W/system_ext
lp=$(losetup -f -s -r -o 1134559232 -S 1955037184 "$IMG") && mount -t ext4 -o ro "$lp" $W/product ||
die "cannot mount the product partition of $IMG (is it the r14 Play image?)"
le=$(losetup -f -s -r -o 970981376 -S 162566144 "$IMG") && mount -t ext4 -o ro "$le" $W/system_ext ||
{ umount $W/product; die "cannot mount the system_ext partition of $IMG"; }
}
umount_img() { umount $W/product 2>/dev/null; umount $W/system_ext 2>/dev/null; }
install() {
writable
mount_img
echo "$FILES" | while read -r f sum; do
[ -n "$f" ] || continue
got=$(sha256sum "$W/$f" 2>/dev/null | cut -d' ' -f1)
[ "$got" = "$sum" ] || { umount_img; die "/$f in $IMG does not match (got ${got:-nothing})"; }
done || exit 1
echo "$FILES" | while read -r f sum; do
[ -n "$f" ] || continue
d=/${f%/*}
if [ ! -d "$d" ]; then mkdir -p "$d" && chmod 755 "$d" && chcon u:object_r:system_file:s0 "$d" || exit 1; fi
cp "$W/$f" "/$f.new" && chown root:root "/$f.new" && chmod 644 "/$f.new" &&
chcon u:object_r:system_file:s0 "/$f.new" && mv "/$f.new" "/$f" || exit 1
echo "copied /$f"
done || { umount_img; die "copying failed"; }
umount_img
# The image's own com.android.vending is a licensing stub; hide it so the store takes its place.
rm -rf /$STUB && echo "hid /$STUB"
pm enable com.android.vending >/dev/null 2>&1
sync
echo "OK: installed. Shut the instance down and start it again (cold boot)."
}
status() {
echo "$FILES" | while read -r f sum; do
[ -n "$f" ] || continue
got=$(sha256sum "/$f" 2>/dev/null | cut -d' ' -f1)
if [ "$got" = "$sum" ]; then echo "play /$f"; elif [ -n "$got" ]; then echo "image /$f"; else echo "absent /$f"; fi
done
if [ -e /$STUB ]; then echo "present /$STUB (store not installed)"; else echo "hidden /$STUB"; fi
echo "com.android.vending: $(pm path com.android.vending | head -1)"
}
gsfid() {
id=$(sqlite3 /data/data/com.google.android.gsf/databases/gservices.db "select value from main where name='android_id';")
[ -n "$id" ] || die "no GSF ID yet: wait a minute with the network on, then try again"
echo "GSF ID (decimal): $id"
printf 'GSF ID (hex): %x\n' "$id"
}
uninstall() {
# The store is disabled first so it cannot update itself again before the shutdown; the second run, after the
# cold boot, enables com.android.vending again (by then the image's licensing stub).
am force-stop com.android.vending >/dev/null 2>&1
pm disable com.android.vending >/dev/null 2>&1
for p in com.android.vending com.google.android.gms com.google.android.gsf; do
case "$(pm uninstall-system-updates $p 2>&1)" in *Success*) echo "removed updates of $p" ;; esac
done
writable
[ -d $U ] || die "$U not found"
left=0
for f in $(echo "$FILES" | cut -d' ' -f1); do
part=${f%%/*}; rest=${f#*/}
if [ -e "$U/$part/upper/$rest" ]; then rm -f "$U/$part/upper/$rest" && echo "removed /$f"; left=1; fi
done
rmdir $U/product/upper/priv-app/Phonesky 2>/dev/null
if [ -e $U/product/upper/app/LicenseChecker ]; then rm -f $U/product/upper/app/LicenseChecker && echo "restored /$STUB"; left=1; fi
sync
if [ $left = 1 ]; then
echo "OK: files removed. Cold boot the instance (stop, then start), then run uninstall once more."
elif [ "$(pm path com.android.vending | head -1)" = "package:/$STUB/LicenseChecker.apk" ]; then
pm enable com.android.vending >/dev/null 2>&1
echo "OK: the store is gone; the instance is back to the image's own files."
else
echo "Not done yet: cold boot the instance, then run uninstall again."
fi
}
case "$1" in
install) install ;;
status) status ;;
gsfid) gsfid ;;
uninstall) uninstall ;;
*) echo "usage: su 0 sh $0 install|status|gsfid|uninstall"; exit 2 ;;
esac
'@
[IO.File]::WriteAllText("$dl\playstore.sh", $sh.Replace("`r`n", "`n"))4. 装进实例
.\tools\modavd.exe start $i
.\tools\modavd.exe shell $i "mkdir -p /data/local/tmp/playstore"
.\tools\modavd.exe adb $i -- push "$dl\x86_64\system.img" /data/local/tmp/playstore/system.img
.\tools\modavd.exe adb $i -- push "$dl\playstore.sh" /data/local/tmp/playstore/playstore.sh
.\tools\modavd.exe shell $i "su 0 sh /data/local/tmp/playstore/playstore.sh install"- 实例已经开着时,
start只报告一下状态,不会重开。 - 最后一行是
OK: installed.才算放好了。是FAIL:的,按提示处理以后重跑这一行就行。
然后关机再开机,新放的文件冷启动以后才生效。不要在安卓里点重启:
.\tools\modavd.exe stop $i
.\tools\modavd.exe start $i --window5. 确认装好了
.\tools\modavd.exe shell $i "su 0 sh /data/local/tmp/playstore/playstore.sh status"装好时显示:
play /product/priv-app/Phonesky/Phonesky.apk
play /product/priv-app/PrebuiltGmsCore/PrebuiltGmsCore.apk
play /system_ext/priv-app/GoogleServicesFramework/GoogleServicesFramework.apk
play /product/priv-app/PartnerSetupPrebuilt/PartnerSetupPrebuilt.apk
play /product/etc/security/fsverity/play_store_fsi_cert.der
hidden /product/app/LicenseChecker
com.android.vending: package:/product/priv-app/Phonesky/Phonesky.apk最后一行过一会儿可能变成 /data/app/...:那是商店联网后自己更新了,正常。
然后在实例窗口的应用列表里打开「Play Store」(中文界面叫「Play 商店」),点「Sign in」,能进到 Google 的登录页就装好了。登录前先看下一节。
- 开机联网后,商店会马上把 Play 服务更新到最新版。更新装上的那一下商店会自己关掉,过一两分钟再打开就好。
6. 登记设备
ModAVD 的实例不是 Google 认证过的设备。登录时或登录后提示「设备未经 Play 保护机制认证」、不让用 Google 应用的,要把这台实例的 GSF ID 登记到你的 Google 账号:
取 GSF ID:
powershell.\tools\modavd.exe shell $i "su 0 sh /data/local/tmp/playstore/playstore.sh gsfid"会打出十进制和十六进制两种写法,登记页要哪种就填哪种。提示还没有 ID 的,开着网等一两分钟再试。
在电脑的浏览器里打开 https://www.google.com/android/uncertified/,用要在实例里登录的那个 Google 账号登录,填入 GSF ID,提交。
等一会儿让登记生效,把实例关机再开机,再去登录。
- 每台实例的 GSF ID 不一样,每台都要登记。清除「Google 服务框架」的数据或者重建实例以后 ID 会变,要重新登记。
- 登记只是让 Google 应用能用,商店里照样显示设备未认证。
7. 删掉临时文件
.\tools\modavd.exe shell $i "rm /data/local/tmp/playstore/system.img"
Remove-Item "$dl\x86_64" -Recurse- 实例里的
playstore.sh留着,卸载时还要用,它很小。 - 压缩包要给别的实例装就留着,不要了就删掉。
和执行桥、宿主输入法的关系
- 宿主输入法:是普通应用,不受影响。
- 执行桥:商店和桥都在实例的系统覆盖层里,各放各的文件。装了商店以后
bridge <编号> verify照常通过,开机后的设置也照常做。 - 商店离不开桥带来的可写系统镜像开机:
- 不要对装了商店的实例运行
bridge <编号> uninstall。卸了桥,实例就改回用 Google 的原始镜像开机,商店没了,系统里还会留下不配套的更新。要卸桥,先照下一节卸掉商店。 - 用
--no-bridge新建的实例装不了。
- 不要对装了商店的实例运行
- 升级 ModAVD:系统镜像版本不变时,实例的覆盖层不动,商店和执行桥一样留着,不用重装。
- 换系统镜像版本:这一页的文件、偏移和校验值只对 r14 有效。以后 ModAVD 换了镜像版本,以那时的文档为准。
卸载
.\tools\modavd.exe shell $i "su 0 sh /data/local/tmp/playstore/playstore.sh uninstall"
.\tools\modavd.exe stop $i
.\tools\modavd.exe start $i
.\tools\modavd.exe shell $i "su 0 sh /data/local/tmp/playstore/playstore.sh uninstall"- 第一次运行先停用商店(免得它关机前又给自己装更新),去掉商店、Play 服务、GSF 从商店装的更新,删掉放进去的文件,恢复占位应用。第二次运行在冷启动以后,把占位应用重新启用。看到
OK: the store is gone就卸干净了。 - 卸完以后 Play 服务回到镜像自带的 23.18,数据保留。商店给别的应用装过的更新(比如 Android System WebView)会留着。
- 实例里的
playstore.sh已经删了的,先照第 3、4 步重新生成、推进去,system.img不用推。 - 已经登录的 Google 账号会留在实例里,不想留就在安卓设置的「Passwords & accounts」(中文界面叫「密码和帐号」)里删掉。
为什么要这样装
- 直接
adb install商店会失败:镜像里已经有一个包名同为com.android.vending的系统应用,就是上面说的占位应用。签名不同,系统不让用商店去「更新」它。就算装上了,商店也要作为系统特权应用,才有装应用这些权限。 - 只放商店不够:镜像自带的 Play 服务和 GSF 是用 Android 测试证书签的,商店是 Google 正式签名的。Google 的组件之间会互相查签名,对不上就不工作,日志里是
requires Google Play Store, but its signature doesn't match that of Google Play services。所以要把 Play 服务和 GSF 也换成 Play 版镜像里同版本、Google 签名的那两份。合作伙伴设置要写 GSF 的设置,签名也得跟着换。 - 网上的 GApps 刷机包(MindTheGapps、NikGapps、OpenGApps 等):
- 没找到 Android 14 x86_64 的正式版本;
- 它们是给 recovery 刷的,ModAVD 的实例没有 recovery,最后还是得照上面这样手动放文件;
- 里面的 Play 服务和镜像自带的版本、签名都不一样,一样要换。
- 从 Google 官方镜像里取,来源能核对,版本也和镜像是同一期。
- 当普通应用装的「谷歌三件套」:撞上的就是第一条。
风险
- 这样装会改实例的系统分区。改的是实例自己的覆盖层,不动 Google 的原始镜像文件。出了问题可以照上面卸载;实例起不来的,只能删掉重建,游戏数据会丢。所以建议用新实例装。
- 设备没有通过 Play 保护机制认证。登记 GSF ID 以后 Google 应用能用,但 Play Integrity(完整性检查)多半过不了:镜像是 userdebug、测试密钥构建,还带 root。查完整性的游戏和应用可能不让运行或不让登录。
- 镜像里预装的其他 Google 应用(Gmail、YouTube、地图等)还是测试证书签名。合作伙伴设置就是因为这个在后台崩溃,所以一起换了;其他几个没测,换了 Play 服务以后可能用不了。
- 开机联网以后,商店会把自己和 Play 服务更新到最新版(实测 Play 服务从 23.18 更新到了 26.43),要下载几百 MB。
- Google《Android 软件开发套件许可协议》第 3.4 条不允许复制、修改 SDK 的组成部分,系统镜像也算在内。这样装商店不在这份协议允许的范围内,装不装由你自己决定。