☰
RK3588 上跑 Android in Container:Docker 与 Android 共存的完整配置指南
2026/9/28 17:43:03 网站建设 项目流程

RK3588 这颗芯片在边缘计算和嵌入式 AI 圈子里热度一直没降过,八核 A76/A55 加 6TOPS NPU 的配置,跑推理、做视觉网关、当轻量服务器都够用。但很多人拿到板子之后会卡在同一个地方:Android 应用生态和 Linux 容器化部署这两件事,能不能在同一块板子上同时要?传统做法是刷 Android 就丢了 Docker,刷 Ubuntu 就丢了 Android App 的兼容性。Android in Container(AIC)这套方案就是冲着这个矛盾来的——在 Linux 宿主机上用容器的方式跑一个完整的 Android 系统,Docker 管生命周期,Android 跑应用,两边互不干扰。

我前后在 RK3588 上折腾 AIC 大概花了三周时间,中间踩的坑比预想的多得多:内核 config 缺项导致 binder 起不来、GPU 设备节点权限不对导致 SurfaceFlinger 反复重启、Docker 的 cgroup 版本和 Android 容器要求对不上、ADB 连不上容器里的 Android 等等。这篇就把整个配置流程从头到尾捋一遍,包括每一步为什么这么做、参数怎么算、出问题怎么排查。适合手里有 RK3588 板子、想同时用 Docker 和 Android 的开发者,也适合单纯想搞清楚容器化 Android 原理的读者。

1. 先搞清楚 AIC 到底在做什么

1.1 容器化 Android 和模拟器的本质区别

很多人第一反应是"这不就是个模拟器吗",其实完全不是一回事。QEMU 那类模拟器是在宿主机上模拟一套完整的 ARM 指令集,Android 系统跑在虚拟 CPU 上,性能损耗大,GPU 加速也经常出问题。AIC 的思路完全不同:Android 系统和宿主机共享同一个 Linux 内核,通过 namespace 做进程隔离,通过 cgroup 做资源限制,Android 的各个系统服务(SurfaceFlinger、AudioFlinger、zygote 等)就是宿主机上的一批普通进程,只不过被关在容器里。

这个区别带来的直接好处是性能。因为不需要指令翻译,Android 里的应用跑的是原生 ARM 指令,CPU 性能基本没有额外损耗。GPU 方面,容器里的 Android 直接访问宿主机的 /dev/mali0 设备节点,用同一套 Mali 驱动做硬件加速,渲染性能接近原生。NPU 也是同理,RKNN 的运行时库在宿主机上装好,容器里挂载进去就能用。

代价是隔离性没有虚拟机那么彻底。容器里的 Android 和宿主机共享内核,如果 Android 侧触发了内核 panic,宿主机也会挂。所以生产环境里一般会给 Android 容器配上比较严格的 cgroup 限制,防止它把宿主机资源吃光。

1.2 RK3588 上跑 AIC 的硬件前提

不是所有 RK3588 板子都能顺利跑 AIC,有几个硬件层面的前提需要先确认。首先是内存,Android 系统本身加上容器运行时,空载就要吃掉 1.5GB 到 2GB,如果你还要在 Android 里跑应用,建议板子至少 8GB 内存,4GB 的版本会非常紧张。其次是存储,Android 系统镜像解压后大概 3GB 到 4GB,加上 Docker 镜像层和数据,eMMC 建议 32GB 起步,用 TF 卡的话一定要选 A2 级别的高速卡,否则 Android 启动会慢到怀疑人生。

GPU 方面,RK3588 的 Mali-G610 在主线 Mesa 和 Rockchip 的闭源驱动下表现差异很大。AIC 方案通常要求用 Rockchip 提供的 libmali 闭源驱动,因为 Android 的 gralloc 和 hwcomposer 依赖特定的 ioctl 接口,主线驱动不一定支持完整。这一点在选内核和设备树的时候就要注意,后面内核配置章节会详细说。

还有一个容易被忽略的点是散热。RK3588 满负荷跑的时候功耗不低,AIC 场景下宿主机 Linux 和容器里的 Android 同时在跑,CPU 和 GPU 负载都不小。我用的板子一开始没加散热片,跑压力测试十分钟就开始降频,Android 界面明显卡顿。后来加了个小风扇,温度压在 60 度以下,就稳定多了。

1.3 整体架构分层

把整个系统拆开看,从上到下大概是这么几层。最上面是 Android 应用层,就是你在容器里装的 APK。往下是 Android Framework 层,包括 system_server、各种系统服务。再往下是 Android Native 层,SurfaceFlinger、AudioFlinger、binder 驱动接口这些。这一层往下就开始和宿主机交界了。

交界的地方有几个关键通道。binder 是通过 /dev/binder 和 /dev/binderfs 设备节点,Android 容器里的进程通过这个和容器内的其他 Android 进程通信。显示是通过 /dev/dri/card0 和 /dev/mali0,SurfaceFlinger 把渲染结果写到 DRM 设备,由宿主机的显示驱动输出到屏幕。音频是通过 ALSA 或者 PulseAudio 的 socket。网络方面,Android 容器一般用独立的 network namespace,通过 veth pair 和宿主机桥接,或者直接用 host 网络模式。

最底下是 Linux 宿主机,跑着 Docker daemon、containerd、runc 这些容器运行时。宿主机的内核需要开启一系列 Android 相关的 config,这是整个方案能不能跑起来的关键,下一章详细讲。

2. 内核与 BSP 的前置配置

2.1 必须打开的 Android 相关内核选项

AIC 能不能跑起来,八成的问题都出在内核 config 上。Android 系统依赖一批特定的内核特性,如果宿主机内核没开,容器里的 Android 启动到一半就会卡死或者反复重启。下面这几个是必须确认打开的:

CONFIG_ANDROID=y CONFIG_ANDROID_BINDER_IPC=y CONFIG_ANDROID_BINDERFS=y CONFIG_ANDROID_BINDER_DEVICES="binder,hwbinder,vndbinder" CONFIG_ASHMEM=y CONFIG_ION=y CONFIG_ION_SYSTEM_HEAP=y CONFIG_ION_CMA_HEAP=y CONFIG_SYNC_FILE=y CONFIG_SW_SYNC=y CONFIG_DMABUF_HEAPS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_SCHED=y CONFIG_CGROUPS=y CONFIG_NAMESPACES=y CONFIG_NET_NS=y CONFIG_PID_NS=y CONFIG_IPC_NS=y CONFIG_UTS_NS=y CONFIG_USER_NS=y

这里面最容易漏的是CONFIG_ANDROID_BINDERFS。老版本的 Android 用的是 /dev/binder 这种固定设备节点,新版本(Android 10 以后)改成了 binderfs,需要在挂载的时候动态创建 binder 设备。如果内核只开了CONFIG_ANDROID_BINDER_IPC没开CONFIG_ANDROID_BINDERFS,容器里的 init 进程会报 "Failed to open /dev/binder" 然后直接退出。

CONFIG_ASHMEM和CONFIG_ION是内存共享相关的。Android 的图形系统大量使用共享内存做 buffer 传递,SurfaceFlinger 和应用的渲染进程之间就是通过 ashmem 和 ion 共享图形 buffer 的。这两个没开的话,Android 界面要么黑屏要么花屏。

cgroup 和 namespace 相关的选项是 Docker 本身需要的,但 Android 容器对 cgroup 的要求比普通容器更细。特别是CONFIG_CGROUP_DEVICE,Android 容器需要精确控制容器内能访问哪些设备节点,这个选项不开的话设备权限没法限制。

2.2 设备树里要预留的节点

内核 config 是一方面,设备树里也要做相应配置。RK3588 的 BSP 里,显示相关的节点需要确认几个东西。首先是 mali 节点,要确保 GPU 的电源域和时钟配置正确,否则容器里的 Android 访问 /dev/mali0 的时候会报权限或者超时错误。

然后是 ion 或者 dmabuf heap 的预留内存。Android 的图形系统需要一块连续的物理内存做图形 buffer,通常是在设备树里通过 reserved-memory 预留。RK3588 的 BSP 一般默认预留了 256MB 左右给显示和摄像头,如果要在 Android 里跑比较重的图形应用,建议把这个值调大到 512MB。

reserved-memory { ion_cma: ion-cma { compatible = "shared-dma-pool"; reusable; reg = <0x0 0x10000000 0x0 0x20000000>; }; };

上面这段是预留 512MB 给 ion CMA 的示例,起始地址 0x10000000,大小 0x20000000。具体地址要根据你板子的内存布局调整,不能和内核、其他 reserved-memory 区域重叠。改完设备树之后要重新编译 dtb 并更新到板子上。

2.3 编译和烧录的实操细节

内核编译这块,RK3588 的 SDK 一般提供了 build.sh 脚本,但直接跑脚本容易出问题。我的做法是先手动配置一遍,确认 config 对了再走脚本。具体步骤是先make ARCH=arm64 rockchip_defconfig,然后make ARCH=arm64 menuconfig进去逐项检查上面列的那些选项,确认无误后保存成自己的 defconfig,再编译。

编译命令大概是:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip_defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) dtbs

烧录的时候要注意,如果板子用的是 AB 分区,内核和 dtb 要烧到正确的分区。RK3588 的 AB 分区方案里,boot 分区是 A/B 两份轮换的,用 rkdeveloptool 或者 upgrade_tool 烧录的时候要确认当前活动分区是哪个。我一开始没注意这个,烧到了非活动分区,重启之后发现内核根本没更新,排查了半天才发现是分区搞错了。

提示:烧录前先用cat /proc/cmdline看一下当前启动参数,确认 root 分区和 Android 容器要用的分区不冲突。如果 Android 容器要单独占一个分区,建议在分区表里单独划一块,不要和宿主机 rootfs 混在一起。

3. Docker 运行时的准备与调优

3.1 在 RK3588 上装 Docker 的正确姿势

RK3588 跑的是 arm64 架构,装 Docker 不能用 x86 的那套 apt 源。Ubuntu 22.04 或者 24.04 上,直接用官方源就行:

sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

装完之后先别急着跑容器,有几个配置要改。首先是 cgroup 版本,Android 容器对 cgroup v2 的支持不如 v1 成熟,建议在宿主机上强制用 cgroup v1。修改/etc/default/grub里的GRUB_CMDLINE_LINUX,加上systemd.unified_cgroup_hierarchy=0,然后sudo update-grub重启。如果是用 U-Boot 直接启动的,就在 bootargs 里加这个参数。

其次是 Docker daemon 的配置。默认的 storage driver 在 RK3588 上可能是 overlay2,这个没问题,但要注意如果根文件系统是 ext4 且内核支持 overlayfs 的话性能最好。修改/etc/docker/daemon.json:

{ "storage-driver": "overlay2", "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "iptables": false }

iptables设成 false 是因为 Android 容器一般用 host 网络模式,不需要 Docker 自己做 NAT。关掉可以避免和宿主机上其他网络配置冲突。

3.2 cgroup 和设备权限的坑

Android 容器启动的时候,需要访问宿主机上的一批设备节点:/dev/mali0、/dev/dri/card0、/dev/dri/renderD128、/dev/binderfs 等等。Docker 默认是不允许容器访问这些设备的,需要在启动参数里显式指定--device。

但这里有个坑:binderfs 是一个动态挂载的文件系统,不是固定的设备节点。你不能简单地--device /dev/binderfs,因为容器启动的时候这个挂载点可能还不存在。正确的做法是在宿主机上先挂载好 binderfs,然后把整个目录挂载进容器:

sudo mkdir -p /dev/binderfs sudo mount -t binder binder /dev/binderfs

然后在 Docker run 的时候用-v /dev/binderfs:/dev/binderfs挂进去。容器里的 Android init 会在 /dev/binderfs 下面创建 binder、hwbinder、vndbinder 这几个设备节点。

设备权限方面,mali 和 dri 设备在宿主机上的 group 通常是 video 或者 render,容器里的 Android 进程需要对应的权限。最简单的做法是在 Docker run 的时候加--privileged,但这样隔离性就没了。更精细的做法是--device加上--group-add:

docker run --device /dev/mali0 --device /dev/dri/card0 --device /dev/dri/renderD128 \ --group-add video --group-add render \ ...

我实测下来,mali 设备如果权限不对,SurfaceFlinger 会一直报 "Failed to open /dev/mali0: Permission denied",然后 Android 界面就起不来,只能看到黑屏。这个错误在 logcat 里不一定明显,要去看 dmesg 才能发现。

3.3 资源限制怎么设才合理

Android 容器吃资源比较凶,如果不做限制,它可能把宿主机的内存和 CPU 都占满。Docker 提供了--memory、--cpus、--memory-swap这些参数来做限制。我的经验是,8GB 内存的板子,给 Android 容器分 4GB 比较合适,留 4GB 给宿主机和其他服务。

docker run --memory=4g --memory-swap=4g --cpus=4 ...

--memory-swap设成和--memory一样,意思是禁用 swap。Android 容器用 swap 的话性能会很差,因为 Android 的内存管理本身就很激进,再叠加 swap 会导致频繁的换入换出,界面卡顿明显。

CPU 方面,RK3588 是 4 个 A76 大核加 4 个 A55 小核。如果给 Android 容器分配 4 个 CPU,默认是调度到所有核心上。但 Android 的 UI 渲染对延迟敏感,建议用--cpuset-cpus把 Android 容器绑定到大核上:

docker run --cpuset-cpus="4-7" ...

RK3588 的 CPU 编号一般是 0-3 是 A55,4-7 是 A76。把 Android 绑到大核上,UI 流畅度会好很多。当然这样宿主机上其他任务就只能用小核了,要根据实际负载权衡。

4. Android 容器镜像的构建与启动

4.1 镜像从哪来:自己构建还是用现成的

AIC 的 Android 镜像一般有两种来源。一种是社区维护的预构建镜像,直接 docker pull 下来就能用,省事但版本可能比较旧,而且不一定适配 RK3588 的硬件。另一种是自己从 Android 源码构建,工作量大但可控性强,能针对 RK3588 做优化。

我建议第一次上手先用预构建镜像跑通流程,确认宿主机环境没问题,再考虑自己构建。预构建镜像一般托管在某个 registry 上,pull 下来之后用docker images看一下大小,正常的 Android 容器镜像大概 2GB 到 4GB。

自己构建的话,流程大概是:先按正常方式编译 RK3588 的 Android SDK,得到 system.img、vendor.img 这些。然后用一个工具把这些镜像转换成容器 rootfs,通常是解压成目录结构,再写一个 Dockerfile 把必要的文件 COPY 进去。这个过程比较繁琐,而且 Android 的 SELinux 策略在容器里需要调整,否则很多服务起不来。

4.2 启动命令的完整拆解

下面是一条完整的 Android 容器启动命令,我把它拆开逐段解释:

docker run -d --name android-aic \ --privileged \ --memory=4g --memory-swap=4g \ --cpuset-cpus="4-7" \ --network=host \ -v /dev/binderfs:/dev/binderfs \ -v /dev/dri:/dev/dri \ -v /dev/mali0:/dev/mali0 \ -v /data/android:/data \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -e ANDROID_ROOT=/system \ -e ANDROID_DATA=/data \ android-aic:latest

--privileged这里我用了,因为 Android 容器需要访问的设备节点比较多,一个个加--device太麻烦。如果对安全性要求高,可以改成精细化的--device列表,但调试阶段用 privileged 省事。

--network=host让 Android 容器直接用宿主机的网络栈。这样 ADB 连接、网络访问都简单很多。如果要用独立网络,需要额外配置 veth 和 NAT,复杂度高不少。

-v /dev/binderfs:/dev/binderfs是前面说的 binderfs 挂载。-v /dev/dri和-v /dev/mali0是图形设备。-v /data/android:/data是把宿主机的目录挂载成 Android 的 /data 分区,这样 Android 里装的应用和数据能持久化,容器重启不丢。

-e ANDROID_ROOT和-e ANDROID_DATA是告诉 Android 的 init 进程系统分区和数据分区在哪。这两个环境变量如果不对,Android 启动会找不到系统文件。

4.3 启动后怎么确认 Android 真的起来了

容器起来之后,docker ps看到状态是 Up 不代表 Android 就正常了。要确认 Android 系统真的跑起来了,有几个检查点。

先看容器日志:

docker logs -f android-aic

正常的启动日志会看到 init 进程的各个阶段,从 "init: starting service" 到 zygote 启动,再到 system_server 起来。如果卡在某个服务反复重启,日志里会有明显的 "Service ... crashed" 或者 "restarting" 字样。

然后进容器里看进程:

docker exec -it android-aic sh ps -ef | grep -E "zygote|system_server|surfaceflinger"

zygote、system_server、surfaceflinger 这三个进程都在,说明 Android 的核心服务起来了。如果只有 init 没有 zygote,多半是 binder 或者 ashmem 的问题。如果 zygote 在但 system_server 反复重启,一般是 SELinux 或者权限问题。

最后是 ADB 连接。Android 容器里的 adbd 默认监听 5555 端口,因为用了 host 网络,直接在宿主机上adb connect localhost:5555就能连上。连上之后adb shell进去,能执行命令就说明 Android 系统基本正常了。

注意:如果 adb connect 报 "connection refused",先确认容器里的 adbd 进程在不在。有些镜像默认不开 adbd,需要手动setprop service.adb.tcp.port 5555然后重启 adbd。

5. 踩坑实录:那些让我熬夜的报错

5.1 binder 设备节点创建失败

这是我最开始遇到的一个坑,现象是容器启动后 Android init 报 "Failed to open /dev/binder: No such file or directory",然后直接退出。排查过程是这样的:先确认宿主机上 binderfs 挂载了没有,mount | grep binder看到有挂载。然后进容器里ls /dev/binderfs,发现目录是空的。

问题出在挂载顺序上。binderfs 挂载之后,binder 设备节点不是自动创建的,需要 Android 的 init 进程或者手动创建。但容器里的 init 在启动早期就要打开 /dev/binder,这时候节点还没创建,就报错了。

解决办法是在宿主机上挂载 binderfs 之后,手动创建好设备节点:

sudo mount -t binder binder /dev/binderfs sudo touch /dev/binderfs/binder /dev/binderfs/hwbinder /dev/binderfs/vndbinder

或者更规范的做法是用 binderfs 的 mount 选项指定要创建的设备:

sudo mount -t binder -o devices=binder,hwbinder,vndbinder binder /dev/binderfs

这样挂载的时候内核会自动创建这三个设备节点。这个细节在官方文档里不太显眼,但实际部署的时候很关键。

5.2 SurfaceFlinger 反复重启的排查链路

SurfaceFlinger 起不来是另一个高频问题,现象是 Android 界面黑屏,logcat 里 SurfaceFlinger 反复崩溃重启。排查这个问题的链路比较长,我按顺序列一下。

第一步看 SurfaceFlinger 的崩溃日志,docker exec进去之后logcat -s SurfaceFlinger。如果看到 "Failed to open /dev/mali0",就是 GPU 设备权限问题。如果看到 "Failed to initialize EGL",可能是 libmali 的版本和内核驱动不匹配。

第二步确认 GPU 驱动。宿主机上cat /sys/class/misc/mali0/device/gpuinfo看 GPU 信息,然后确认容器里挂载的 libmali 库版本。RK3588 的 libmali 有 gbm、x11、wayland 几个版本,Android 容器一般用 gbm 版本。版本不对的话 EGL 初始化会失败。

第三步看 DRM 设备。ls -l /dev/dri/确认 card0 和 renderD128 都在,权限是 video 组可读写。容器里如果这两个设备权限不对,SurfaceFlinger 也没法工作。

第四步看 ion 或者 dmabuf。ls /dev/dma_heap/看有没有 system、cma 这些 heap。Android 的图形 buffer 分配依赖这些。如果 dma_heap 目录是空的,说明内核的 DMABUF_HEAPS 没配置好,要回去检查内核 config。

我遇到的一次是 dma_heap 目录存在但容器里访问不了,原因是容器启动的时候 /dev/dma_heap 没有挂载进去。加上-v /dev/dma_heap:/dev/dma_heap就好了。

5.3 ADB 连接不上的几种情况

ADB 连不上 Android 容器,原因可能有好几种。最常见的是 adbd 没启动,进容器ps -ef | grep adbd确认一下。如果没启动,可能是 Android 的 init.rc 里 adbd 服务被禁用了,需要改 init.rc 或者用 setprop 手动开。

第二种是端口冲突。宿主机上如果已经有 adb server 在跑,占用了 5037 端口,容器里的 adbd 可能起不来。解决办法是先在宿主机上adb kill-server,再启动容器。

第三种是网络模式问题。如果容器不是 host 网络,adbd 监听的端口没有映射出来,宿主机就连不上。用docker port android-aic看一下端口映射,或者干脆改成 host 网络。

第四种比较隐蔽,是 adb 的认证问题。Android 容器里的 adbd 可能要求 RSA 认证,宿主机的 adb key 没被授权。这种情况adb devices会显示 "unauthorized",需要在 Android 界面上点确认,或者把宿主机的 adbkey.pub 推到容器的 /data/misc/adb/adb_keys。

5.4 内存不足导致的 OOM

Android 容器跑一段时间之后突然挂掉,docker logs看到 "Killed" 或者 dmesg 里有 OOM killer 的记录,这就是内存不够了。Android 系统本身加上应用,内存占用会逐渐增长,如果--memory设得太小,就会被 OOM killer 干掉。

我的经验是,Android 容器的内存限制要留足余量。8GB 板子给 4GB 是底线,如果要在 Android 里跑比较重的应用,建议给到 5GB 甚至 6GB。另外可以在容器里限制 Android 的后台进程数量,通过settings put global background_process_limit 2之类的命令减少后台应用,降低内存压力。

还有一个技巧是关掉 Android 的 zram。Android 默认会开 zram 做内存压缩,但在容器里 zram 和宿主机的内存管理会打架,反而增加开销。可以在容器的 init.rc 里禁用 zram 相关的服务。

6. 性能调优与日常维护

6.1 让 Android 界面更流畅的几个调整

AIC 跑起来之后,默认的界面流畅度可能不太理想,尤其是滑动和动画。有几个调整能明显改善。

首先是 CPU 调度。前面说了把 Android 容器绑到大核上,这是最直接有效的。另外可以在宿主机上把 CPU governor 设成 performance,避免调频带来的延迟:

echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

其次是 GPU 频率。RK3588 的 Mali-G610 默认频率可能不是最高,可以通过 sysfs 调整:

cat /sys/class/devfreq/fb000000.gpu/available_frequencies echo 1000000000 | sudo tee /sys/class/devfreq/fb000000.gpu/userspace/set_freq

把 GPU 锁在最高频,渲染性能会好一些,但功耗和发热也会上去,要配合散热。

第三是关闭 Android 的动画。如果只是跑应用不需要动画效果,可以在开发者选项里把窗口动画、过渡动画、动画程序时长都设成 0.5x 或者关闭,能省不少 GPU 资源。

6.2 容器和宿主机的资源隔离实践

生产环境里,Android 容器和宿主机上的其他服务要能和平共处。除了前面说的 memory 和 cpuset 限制,还有几个地方要注意。

IO 方面,Android 容器读写 /data 分区比较频繁,如果和宿主机的 IO 抢资源,会影响两边。可以用--device-read-bps和--device-write-bps限制容器的磁盘带宽:

docker run --device-read-bps /dev/mmcblk0:50mb --device-write-bps /dev/mmcblk0:50mb ...

网络方面,如果 Android 容器用 host 网络,它和宿主机共享网络栈,Android 里的应用发起的连接会占用宿主机的端口。如果 Android 里跑了服务端应用,要注意端口冲突。更隔离的做法是用 bridge 网络加端口映射,但配置复杂一些。

还有一点是 PID namespace。Android 容器里会创建大量进程,如果和宿主机共享 PID namespace,宿主机的进程数会很快涨上去。建议给 Android 容器独立的 PID namespace,Docker 默认就是独立的,不用额外配置,但要注意--pid=host这种参数不要加。

6.3 升级和备份的注意事项

Android 容器用久了,系统更新或者应用升级是免不了的。升级的时候有几个坑。

如果是替换整个容器镜像,要注意 /data 分区的兼容性。Android 的系统版本升级后,/data 里的数据格式可能不兼容,直接挂载旧的 /data 可能导致新系统起不来。稳妥的做法是升级前备份 /data,升级后如果起不来就清空 /data 重新初始化。

备份的话,宿主机的 /data/android 目录直接打包就行,但要注意 Android 里有些文件是运行时生成的,备份的时候最好先把容器停掉,保证文件系统一致性:

docker stop android-aic sudo tar czf android-data-backup.tar.gz -C /data android docker start android-aic

容器镜像本身可以用docker save导出成 tar 文件,方便迁移到其他板子:

docker save android-aic:latest -o android-aic-image.tar

迁移到新板子的时候,除了镜像,还要确认新板子的内核 config 和设备节点和原来一致,否则可能跑不起来。

6.4 长期运行的稳定性观察

AIC 方案我连续跑过两周多,中间遇到过几次问题,记录一下供参考。

一次是运行到第五天的时候,Android 容器里的 SurfaceFlinger 突然挂了,重启容器后恢复。查日志发现是 GPU 驱动的一个偶发错误,dmesg 里有 "mali: OOM" 的记录。后来把 ion 预留内存从 256MB 加到 512MB,就没再出现过。

还有一次是宿主机的 Docker daemon 自己挂了,导致容器全停。查下来是 Docker 的日志文件把磁盘写满了,因为 Android 容器的日志量比较大。后来在 daemon.json 里加了日志轮转配置,限制单个日志文件 50MB,最多保留 3 个,问题解决。

另外建议给 Android 容器配一个 healthcheck,定期检查 system_server 是否还在。如果挂了自动重启容器:

docker run --health-cmd="pgrep system_server || exit 1" --health-interval=30s --health-retries=3 ...

这样即使 Android 内部出问题,也能自动恢复,不用人工干预。

7. 几个实际应用场景的配置差异

7.1 跑视觉推理应用时的额外配置

如果要在 Android 容器里跑视觉推理,比如用 RKNN 跑 YOLO 之类的模型,需要额外挂载 NPU 设备。RK3588 的 NPU 设备节点是 /dev/rknpu,容器启动的时候要加进去:

-v /dev/rknpu:/dev/rknpu

同时要把 RKNN 的运行时库挂载或者打包进镜像。RKNN 的库在宿主机上是 /usr/lib/librknnrt.so,容器里需要同样的路径下有这个库。可以在 Dockerfile 里 COPY 进去,或者运行时用 -v 挂载。

摄像头方面,如果 Android 应用要用摄像头,需要把 V4L2 设备节点挂进去。RK3588 的 MIPI CSI 摄像头一般是 /dev/video0 到 /dev/videoX,USB 摄像头也是类似的节点。挂载的时候要注意权限,video 组可读写。

NPU 推理的性能方面,RK3588 的 6TOPS 算力在容器里跑和宿主机上跑基本没差别,因为都是直接访问硬件。我实测 YOLOv5s 在容器里跑,单帧推理时间大概 30ms 左右,和宿主机上一致。

7.2 当轻量服务器用时的网络配置

有些场景下会把 Android 容器当轻量服务器用,比如跑一些 Android 端的服务,通过 ADB 或者网络接口对外提供能力。这种场景下网络配置要仔细。

如果用 host 网络,Android 容器里的服务监听端口直接暴露在宿主机上,外部能访问。但要注意 Android 的防火墙(iptables)可能会拦截,需要在 Android 里配置放行规则。

如果用 bridge 网络,需要做端口映射:

docker run -p 8080:8080 -p 5555:5555 ...

这样宿主机的 8080 映射到容器的 8080,5555 映射到 adbd。但 Android 容器里的网络配置(比如 DNS)可能需要手动设置,因为 bridge 网络的 DNS 和宿主机不一样。

还有一点是 Android 的网络权限管理。Android 10 以后对后台应用的网络访问限制比较严,如果跑的是后台服务,可能需要在 Android 里配置网络权限白名单,否则服务发不出网络请求。

7.3 多容器并行的资源分配

如果一块 RK3588 上要跑多个 Android 容器,资源分配就更关键了。8GB 内存的板子跑两个 Android 容器基本就到极限了,每个容器分 3GB 左右,宿主机留 2GB。CPU 方面,两个容器各绑两个大核,剩下的大核给宿主机。

存储方面,多个容器的 /data 要分开挂载,不能共用,否则 Android 的数据会互相覆盖。每个容器用独立的目录:

-v /data/android1:/data -v /data/android2:/data

GPU 方面,多个容器共享同一个 /dev/mali0,Mali 驱动本身支持多进程访问,但高负载下可能会有竞争。如果两个容器都在跑图形密集型应用,帧率可能会下降。这种场景下建议错开使用,或者给每个容器限制 GPU 使用。

实际部署中,一块 RK3588 跑两个 Android 容器的情况不多,因为资源确实紧张。更常见的是一块板子跑一个 Android 容器加几个轻量的 Linux 容器,这样资源分配更合理。

8. 写在最后的几点个人体会

AIC 这套方案在 RK3588 上跑通之后,确实解决了不少实际问题。我现在的用法是宿主机 Ubuntu 跑 Docker 和各种服务,Android 容器里跑一些只有 Android 版本的应用,两边通过 ADB 和共享目录交换数据,用起来挺顺手的。

但要说几个真实的感受。第一是这套方案的复杂度不低,内核 config、设备树、Docker 配置、Android 镜像,每一环都有坑,没有一定 Linux 和 Android 基础的话,排查问题会比较痛苦。第二是性能虽然有优势,但和原生 Android 比还是有差距,特别是图形性能,容器里的 Android 跑大型游戏基本不现实,跑普通应用没问题。第三是稳定性需要时间验证,我跑了两周多,出过几次小问题,虽然都能恢复,但生产环境用的话建议加监控和自动恢复机制。

如果你只是想在一台设备上同时用 Linux 和 Android,除了 AIC 还有别的方案,比如双系统切换、或者用 Waydroid 这类更轻量的方案。AIC 的优势在于 Docker 的管理能力和资源隔离,如果你本来就在用 Docker 管理服务,那 AIC 的集成度是最好的。如果只是偶尔用一下 Android 应用,Waydroid 可能更省事。

最后分享一个调试小技巧:Android 容器出问题的时候,docker logs只能看到 init 的输出,Android 系统内部的日志要用adb logcat或者进容器看 /data/logs。我习惯在容器启动参数里加一个-v /var/log/android:/data/logs,把 Android 的日志持久化到宿主机,出问题的时候直接看文件,比在容器里翻方便。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询