在手机上跑 Linux,过去的标准答案是 Termux 加 proot:装包、写脚本、跑个 Python 服务都没问题,但它和安卓共用一个内核,没有完整的 cgroup 和 namespace,Docker 基本不用想。
Podroid 换了个思路:不翻译、不 chroot,直接在应用里启动一台带独立内核的 Alpine Linux 虚拟机。客户机看到的是干净的 Linux 环境,于是 Podman、Docker、LXC 的行为和服务器上完全一致。
这个项目 2026 年 3 月才建仓,到 11 月已经拿到 3.8k star,迭代节奏基本是每月一版。本文依据 v1.3.0(2026-11-09 发布) 的仓库、发布说明和官方文档整理;文中的性能数字来自作者在两台手机上的实测,不是我的跑分,也不是对你手机的承诺。
一、基本信息
| 项目 | 内容 |
|---|---|
| 仓库 | ExTV/Podroid |
| 官网与文档 | extv.github.io/Podroid |
| 许可证 | GPLv2 |
| 安卓端语言 | Kotlin |
| 最新版本 | v1.3.0(2026-11-09) |
| 系统要求 | Android 8(API 26)及以上,仅 arm64-v8a |
| 分发方式 | GitHub Releases 侧载 APK,没有 Play 商店版本 |
| APK 体积 | 约 310 MiB(325 MB,内含系统镜像) |
| 客户机 | Alpine Linux 3.24 + 定制 Linux 7.3 内核,QEMU 11.1.2 |
| 默认凭据 | root / podroid,SSH 端口 9922 |
| 界面语言 | 英文、中文 |
两个门槛要先看清:只有 arm64,32 位 ARM 和 x86 设备装不上(我那台玩客云就是 32 位 ARM,无缘);Android 8 起步,因为虚拟机依赖的 SELinux/seccomp 策略和原生 ABI 从 API 26 才齐。
二、它凭什么是”真”虚拟机
Podroid 的文件系统分两层,这也是它和 proot 方案最根本的区别:
/dev/vdb:只读 squashfs,完整的 Alpine 基础系统(软件包、OpenRC 服务、配置)压缩后打包在 APK 里,永不写入,升级基础系统靠发新 APK。/dev/vda:可写 ext4 镜像,也就是安装向导里让你选大小的那个持久化 overlay。apk add装的东西、容器镜像、SSH 主机密钥、home 目录全落在这里,重启和覆盖安装都还在。
一个极简 initramfs 先启动,把两块设备用 overlayfs 叠起来(squashfs 作 lower、ext4 作 upper),然后 switch_root 交给真正的 Alpine 根。
为什么是 switch_root 而不是 chroot? 早期版本用过 chroot,结果 podman exec -it 坏了:crun 调 setns(MNT) 进容器命名空间时会重置根,exec 进去的进程看到的是 /mnt/overlay/proc 这种原始内核路径而不是 /proc。switch_root 直接重排内核挂载树,命名空间分叉后才能始终看到干净的 /。这个细节很能说明作者是真在跑容器,而不是只做了个能开机的壳。
客户机里 OpenRC 管服务,几个关键服务:
| 服务 | 作用 |
|---|---|
podroid-bootstrap | 启用 cgroup v2、加载内核模块、挂载 devpts/shm/mqueue、配置 ZRAM swap、设置 sysctl 与主机名,把持久化 ext4 绑到 Podman/Docker/LXC 的数据目录 |
podroid-network | 拉起网卡(QEMU/UML 是 eth0/vec0,10.0.2.15/24;AVF 走 DHCP),写 /etc/resolv.conf |
podroid-hostd | 客户机到安卓的桥接守护进程,podroid-notify、podroid-forward 等命令靠它回传 |
podroid-x11 | 启动 Xvnc(display :0,端口 5900)和 PulseAudio(4713),支撑内置桌面查看器 |
podroid-ready | 最后运行,打出 Starting SSH... / Almost ready... / Ready! 标记,App 靠它判断启动成功 |
容器运行时是预装且开机即用的:Podman 无守护进程,直接能跑;Docker 和 LXC 已加入 default runlevel,每次开机自动启动。用 rc-status 确认,只用 Podman 的话可以 rc-service docker stop 或 rc-update del docker default 把内存省下来。
安卓侧则用一个前台服务持有 partial WakeLock,保证切到后台时 CPU 不被调度器停掉——通知栏那条常驻通知就是它的状态显示和停止入口。
三、三种后端:决定快慢的关键
同一套客户机镜像,底下可以换三种虚拟化层。这是 Podroid 最需要理解的一件事,体验差异几乎全在这里。
| QEMU (TCG) | AVF (pKVM) | UML(实验性) | |
|---|---|---|---|
| 原理 | 软件逐条模拟客户机指令 | 安卓虚拟化框架,跑在物理核上 | 客户机内核作为一个普通安卓进程运行 |
| 设备要求 | 任意 arm64 + Android 8+ | 厂商带 pKVM(目前主要是 Pixel 8 及以后) | 任意 arm64 + Android 8+ |
| 额外授权 | 无 | 两条一次性 pm grant | 无 |
| CPU 速度 | 比原生慢数倍 | 接近原生 | 明显快于 QEMU,仍不及 AVF |
| USB 直通 | 支持(需开机前启用) | 不支持 | 不支持 |
| 端口转发 | TCP + UDP | TCP + UDP | TCP + UDP |
| 默认选中 | Auto 的兜底 | Auto 优先选它 | Auto 永不选它 |
Auto 的判定有四个条件:存在 android.software.virtualization_framework 特性、MANAGE_VIRTUAL_MACHINE 已授权、USE_CUSTOM_VIRTUAL_MACHINE 已授权、设备声明支持非受保护虚拟机(non-protected VM)。四个全过才用 AVF,否则回落 QEMU,并在首页、Status 页和设置里说明原因。
第四个条件最容易被忽略:pKVM 有 protected 和 non-protected 两种模式,Podroid 的客户机是普通 Alpine squashfs,满足不了受保护虚拟机的签名与度量要求。只支持 protected VM 的设备用不了 AVF,而且没有应用侧的绕法——pvmfw 只引导带签名验证启动镜像的载荷。剩下的路只有 root 后直接打开 /dev/kvm(高通上是 /dev/gunyah),而 Podroid 坚持免 root,所以这类设备就老老实实跑 QEMU。
启用 AVF
先确认设备支持:
adb shell pm list features | grep virtualization_framework
打印出 android.software.virtualization_framework 才有戏。然后授权这两个开发模式权限(系统 UI 里不暴露):
adb shell pm grant com.excp.podroid android.permission.MANAGE_VIRTUAL_MACHINE
adb shell pm grant com.excp.podroid android.permission.USE_CUSTOM_VIRTUAL_MACHINE
手边没电脑可以用 Shizuku:启动后在它的 rish shell 里直接跑同样的 pm grant 命令。最后在 App 里走一遍 Settings → About → AVF (pKVM) diagnostic,它会检查特性标志、两个权限、/apex/com.android.virt、AVF 系统服务和内核上报的 hypervisor 能力。
这两条授权跨重启和覆盖安装保留,但卸载重装后失效,需要重新 grant。
启用 UML
v1.3.0 新增的第三条路,思路很巧:把客户机 Linux 内核当成一个普通安卓进程跑,客户机代码就直接在 CPU 上执行,不再逐条模拟。不需要 root,不需要 adb 授权,任意 arm64 + Android 8+ 都能试。
开启方式:停止虚拟机 → Settings → Show advanced → Backend → UML (experimental) → 启动。首次启动会跑一个几秒钟的兼容性测试(含 DNS 查询,必须联网),通过后正常引导。首页会多出一行 Backend 显示 UML (user-mode),终端里 uname -r 也会变成另一个内核版本。
测试失败、UML 崩溃或 120 秒内没启动完,Podroid 会在这台设备上关掉 UML,改用 Auto 会选的后端启动,并弹通知说明原因,设置里给出 Try UML again 按钮。数据在所有后端共用同一块磁盘,来回切换不丢东西。
代价也要清楚:
- 没有 USB 直通,需要 USB 设备就得回 QEMU。
- 客户机里每个进程都是真实的安卓进程,包括每个容器进程。Android 12+ 的 phantom-process 限制会杀进程,容器一多就容易中招;Android 14+ 打开开发者选项里的 Disable child process restrictions,12/13 见下文排错。
- 它不是安全边界。客户机内核以应用自身权限运行,别指望 VM 级隔离,不要在里面跑不可信代码。
- 关机更慢,干净关闭约 15–30 秒;网卡名是
vec0而不是eth0;APK 体积增加约 13 MB。 - 作者只在两台手机上测过,16 KB 内存页设备和 Android 8–11 的老内核完全没测。
性能预期
作者在 v1.3.0 发布说明里给了两组实测。第一组是 Nothing Phone (1)、Android 15、无 AVF,同一构建、同一磁盘、4 CPU / 2 GB RAM,三次取中位数:
| 测试 | QEMU | UML |
|---|---|---|
| 启动到 Ready | 33–41 s | 13–18 s |
npm install express(热缓存) | 49.5 s | 24.8 s |
| Node.js CPU 循环(3 亿次) | 18.8 s | 4.7 s |
docker run 内含 shell 循环 | 19.7 s | 7.4 s |
apk add nodejs npm(含下载) | 21.2 s | 12.4 s |
docker run --rm alpine true(仅容器启动) | 7.6 s | 8.2 s |
第二组是 Pixel 10 Pro XL、Android 17,三种后端同场对比:
| 测试 | QEMU | UML | AVF |
|---|---|---|---|
| 启动到 Ready | 44 s | 36 s | 10 s |
npm install express(热缓存) | 25.7 s | 20.6 s | 1.9 s |
| Node.js CPU 循环 | 10.5 s | 3.4 s | 0.9 s |
docker run --rm alpine true | 9.1 s | 8.4 s | 0.85 s |
读出来的结论:算力密集的活收益最大(约 4x),装包约 2x,容器启动本身没变快;手机越快,UML 相对 QEMU 的差距越小;有 AVF 就继续用 AVF,它比另外两个快大约十倍。
官方文档另给的经验值也值得记住:QEMU TCG 对 CPU 密集任务比原生慢 5–20 倍,JIT 型负载(Node、JVM、编译器)因为双重 JIT 更惨;而在 arm64 客户机里拉 x86 镜像会套两层模拟,慢 50–100 倍——所以镜像一定要选 linux/arm64。
四、五分钟上手
1. 装 APK。 从 Releases 下 Podroid-v1.3.0-release.apk,在系统设置里给浏览器或文件管理器放开”安装未知应用”,然后打开安装。
2. 走完四步向导。
| 步骤 | 要选什么 | 注意 |
|---|---|---|
| 持久化存储 | 2 GB – 512 GB,受手机剩余空间限制 | 以后只能扩大,永远不能缩小,宁可选大点 |
| 虚拟机配置 | vCPU 核数、RAM(默认 512 MB)、是否开 SSH | 打算跑桌面就把 RAM 提到 1 GB 以上 |
| Downloads 共享 | 可选,让安卓和虚拟机互传文件 | QEMU 后端在部分设备上不稳,出问题先关它 |
| 权限 | 通知、无限制后台使用 | 都能跳过,缺了会在首页顶部提示补授 |
3. 点 Start VM,等 Ready!。 首次启动约 50–60 秒,明显慢于之后的 10–45 秒,因为要做两件一次性的事:Dropbear 生成 RSA/ECDSA/ED25519 三套主机密钥(典型手机上 25–35 秒),以及按你选的大小格式化 ext4 镜像。通知栏会实时显示启动阶段。
4. 打开终端,跑第一条容器命令。
# rootless Podman,无需守护进程,立刻可用
podman run --rm alpine echo "hello from a container"
# Docker 服务已开机自启
docker run -d -p 8080:80 nginx
5. 把服务暴露到手机和局域网。 两条路等价:Settings → Port forwards → Add,或者直接在客户机 shell 里:
podroid-forward add 8080 8080 tcp # 手机 8080 -> 客户机 8080
podroid-forward list
podroid-forward remove 8080 tcp
podroid-forward clean # 一次清掉所有用户规则
规则实时生效(QEMU 上走 QMP,不用重启虚拟机)、跨重启保留,也会出现在设置列表里。三个限制:宿主端口必须大于 1024(非特权安卓应用绑不了低端口);尽量避开 32768 以上,安卓给普通应用分配端口从这开始,最容易撞;最多 2048 条规则。
6. 从笔记本 SSH 进去。 向导或设置里开了 SSH 后,同一 Wi-Fi 下:
ssh root@<phone-ip> -p 9922 # 密码 podroid
默认密码务必改掉,或者建个普通用户:adduser -G wheel <username>,wheel 组成员可以用 doas(装了 sudo 也行)。
一个完整的例子,从容器一路验证到手机浏览器:
mkdir -p ~/site && echo '<h1>Hello from Podroid</h1>' > ~/site/index.html
podman run -d --name hello \
-p 8080:80 \
-v ~/site:/usr/share/nginx/html:ro \
docker.io/library/nginx:alpine
wget -qO- http://localhost:8080 # 先在客户机内部确认
podroid-forward add 8080 8080 tcp # 再暴露到手机
然后访问 http://<phone-ip>:8080。手机 IP 可以在 Status 页看,或在客户机里执行 podroid-phone-ip。
顺带一提,整套文档也打包在客户机里,podroid-forward add 9980 9980 tcp 后执行 podroid-docs,就能在离线状态下用浏览器读 http://127.0.0.1:9980。
五、能拿它做什么
自托管小服务。 这是最直接的用途,注意挑 arm64 镜像:
# Gitea:自建 Git
podman run -d --name gitea -p 3000:3000 -p 2222:22 \
-v gitea-data:/data docker.io/gitea/gitea:latest-linux-arm64
# code-server:浏览器里的 VS Code
podman run -d --name code-server -p 8443:8443 \
-v "$PWD:/home/coder/project" \
ghcr.io/coder/code-server:latest \
--bind-addr 0.0.0.0:8443 /home/coder/project
Pi-hole、Vaultwarden 同理。JVM 系服务(Jenkins、Gradle、Gitea runner)在 QEMU 上会很难受,光 JVM 启动就要 10–30 秒。
随身 Linux 开发机。 工具链装一次就永久留在 overlay 上:
apk add git nodejs npm python3 py3-pip go gcc musl-dev make
SSH 通了之后,笔记本上的 VS Code Remote-SSH 和 JetBrains Gateway 直接连,手机对它们来说就是一台普通远程 Linux 服务器。文本编辑、shell 脚本、Git 操作、短 Python 脚本在 TCG 上都算舒服——这些活主要耗在 IO 和内存上,模拟开销不明显。
学 Linux 和容器。 真内核、真 cgroup v2、真 network namespace、真 nftables,生产服务器上有的特性这里都能摸:
ls /sys/fs/cgroup/
podman run --rm -it alpine sh -c 'ls -la /proc/1/ns/'
nft list ruleset
而且搞坏了不心疼:恢复出厂一分钟内就回到干净的 Alpine。故意把内核搞崩、把磁盘填满、把网络配错,都能立刻重来。
GUI 桌面。 内置 X11 查看器连客户机的 Xvnc,支持触控、外接键鼠和音频转发:
apk add xfce4 xfce4-terminal mousepad ristretto xfce4-taskmanager
apk add firefox gimp
只支持 X11 会话,Wayland 桌面(GNOME Wayland、Plasma Wayland、sway、Hyprland)不渲染。
定时任务和小 bot。 rc-update add crond default && rc-service crond start 之后正常用 crontab,长任务结束时用 podroid-notify "build finished" 直接在安卓通知栏弹一条,点开就回到 Podroid。
六、已知限制与排错
| 现象 | 原因 | 处理 |
|---|---|---|
切到后台虚拟机就死,日志 QEMU crashed (SIGKILL)、exit 137 | Android 12+ 的 phantom-process killer | 见下方三条 adb 命令;Android 14+ 可开开发者选项里的 Disable child process restrictions |
| 开机后不久崩溃,提示 Downloads sharing 不稳 | QEMU 的 virtio-9p 在部分 ARMv9.2 设备(含 Pixel 10)上会崩 | 设置里关掉 Downloads 共享,AVF 路径不受影响 |
| 容器慢到离谱 | 拉了 linux/amd64 镜像,套两层模拟 | 换 linux/arm64 镜像 |
| 桌面装了但查看器一片空白 | Wayland 会话不向 X display 渲染 | 用 XFCE/LXDE,或把 GNOME 强制到 Xorg 会话 |
| 改了 RAM 或 CPU 核数没变化 | 这两个是启动参数,不能热改 | 停止虚拟机再启动;端口转发和 SSH 开关则即时生效 |
| 磁盘不够用 | overlay 镜像只能扩不能缩 | 设置里扩大,下次启动客户机自动 resize 文件系统 |
ping 8.8.8.8 通但域名解析失败 | 某些 Wi-Fi 下的已知问题(#93) | 暂用移动数据 |
| USB Wi-Fi 网卡不工作 | 客户机里没有对应固件 | apk add linux-firmware-rtlwifi 或对应芯片组的固件包 |
| AVF 多核启动时重启 | Tensor G3/G4 的 Pixel 8/8a/9 系列副核 bringup 问题 | v1.2.5 起自动降核重试并记住可用核数,无需手动干预 |
| 外接鼠标右键退出全屏 | 安卓全屏手势拦截了右键 | 用双指触摸代替右键 |
| X11 画面模糊或卡顿 | VNC 客户端只用 Raw 编码,ZRLE 当前关闭 | 查看器设置里选 720p / 900p 预设 |
| 首次启用 UML 就失败 | 兼容性测试含 DNS 查询,离线必失败 | 联网后在设置里点 Try UML again |
| 虚拟机卡在 Starting | socket 10 秒超时(多为上次崩溃留下的陈旧 socket)或 60 秒引导超时 | 强制停止 App 再开;导出诊断日志看 console.log 末尾 |
phantom-process 那三条命令值得单独贴出来,一次执行、跨重启保留:
adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put activity_manager max_phantom_processes 2147483647
adb shell settings put global settings_enable_monitor_phantom_procs false
第一条让后续 OTA 推送的标志不会把你的值改回去,后两条把上限抬到 Integer.MAX_VALUE 并关掉执行它的监控器。
排查任何问题的统一入口是 Settings → Export Diagnostic Log,作者收 bug 报告时要的就是这个文件,附上机型和安卓版本。
七、值不值得装
适合这些人:想在手机上跑真容器和自托管小服务;需要一台随身的 Linux 开发机,让笔记本 Remote-SSH 连过来;想认真学 cgroup、namespace、nftables 而不怕搞坏环境;手上有 Pixel 8 及以后的设备,能吃到接近原生的 AVF。
不适合这些场景:32 位 ARM 或 x86 设备、Android 7 及以下(装不上);想在手机上编译 LLVM、内核或大型 Rust 项目(TCG 下是小时级,不如在电脑上交叉编译再 scp 过来);把它当生产服务器(后台随时可能被系统杀,存储和网络都有硬限制);需要跑不可信代码(尤其 UML 后端根本不是安全边界)。
和 Termux 是互补,不是替代。 Termux 轻、秒开、适合日常命令行;Podroid 重(300 MB+ 的 APK 加几 GB 磁盘),换来的是完整内核和真容器。两个都装着,按活儿选,是最舒服的用法。
最后三条安全提醒:默认的 root / podroid 一定要改;SSH 和端口转发是暴露在整个局域网的,公共 Wi-Fi 下别开;UML 后端的客户机内核以应用自身权限运行,隔离性远不如 QEMU/AVF。