RK3588 上 Docker 跑 Android 容器:AIC 方案实战与避坑指南
1. 为什么要在 RK3588 上折腾 Android 容器1.1 从一块板子到一套容器化 Android 环境手里有一块 RK3588 的板子8 核 CPU4 个 A76 大核加 4 个 A55 小核带 6TOPS 算力的 NPUMali-G610 的 GPU接口丰富到用不完。很多人拿到这块板子第一反应是刷个 Ubuntu 跑跑 AI 推理或者刷个 Android 当电视盒子用。但真正把这块板子用出价值的玩法是在上面跑 Docker然后在 Docker 里跑一个 Android 容器。这里说的 Android 容器指的是 AIC全称 Android In Container。它跟传统的 Android 模拟器完全不是一回事。模拟器是在宿主机上模拟一套 ARM 指令集或者用翻译层跑 Android性能损耗大GPU 加速也麻烦。AIC 的思路是Android 和宿主机 Ubuntu 共享同一个 Linux 内核通过容器技术做隔离Android 系统直接跑在真实的 ARM 硬件上GPU、VPU、NPU 这些硬件加速单元都能直接调用。我实测下来RK3588 上跑 AICAndroid 容器的启动时间能控制在十几秒以内4K 视频硬解流畅GPU 渲染的帧率跟原生 Android 系统差距很小。这个方案特别适合几类人一是做 Android 应用开发和测试的需要一台随时能重置、能快照的 Android 设备二是做云手机、云游戏方案的需要在一台物理机上跑多个 Android 实例三是做嵌入式产品原型的想在同一块板子上同时跑 Linux 服务和 Android 应用。1.2 AIC 方案的核心优势与适用边界AIC 最大的优势是资源利用率。传统方案要么刷 Android 要么刷 Ubuntu二选一。AIC 让你两个都要。宿主机 Ubuntu 跑你的 Docker 服务、跑你的 AI 推理、跑你的 Web 后端Android 容器跑你的 App、跑你的 UI 交互。两者通过共享目录和网络互通数据交换很方便。但 AIC 也不是万能的。它对内核版本有要求需要内核支持特定的 namespace 和 cgroup 特性还需要 Android 侧的 Binder、Ashmem、Logger 等驱动支持。RK3588 的官方 BSP 内核一般都已经包含了这些补丁但如果你用的是自己编译的纯净内核可能需要额外打补丁。另外 AIC 对 GPU 驱动的一致性要求比较高宿主机和容器里的 Mali 驱动版本必须匹配否则会出现渲染异常或者直接崩溃。还有一个容易被忽略的点AIC 容器里的 Android 系统镜像需要专门制作不是随便拿一个 Android 的 system.img 就能用。它需要把 Android 的根文件系统、vendor 分区、odm 分区都打包进容器镜像里同时还要处理好 init 进程的启动逻辑让 Android 的 init 能在容器环境里正常跑起来。2. 环境准备与依赖梳理2.1 硬件与基础系统确认先确认你手里的 RK3588 板子状态。我用的是一块 8GB 内存的 RK3588 开发板存储是 64GB 的 eMMC 加一张 128GB 的 TF 卡。建议至少 8GB 内存因为 Android 容器本身就要吃掉 2GB 左右再加上宿主机 Ubuntu 的开销4GB 内存会非常紧张。系统方面我刷的是 Rockchip 官方提供的 Ubuntu 22.04 镜像内核版本是 5.10。这个版本的内核已经包含了 AIC 需要的所有驱动和配置。如果你刷的是 Ubuntu 20.04 或者更早的版本建议先升级因为老版本内核对 cgroup v2 的支持不完整Docker 跑起来会有各种奇怪的问题。检查内核配置是否满足要求可以逐条确认这几个关键项# 检查 cgroup 版本 stat -fc %T /sys/fs/cgroup/ # 输出应该是 cgroup2fs # 检查 namespace 支持 ls /proc/self/ns/ # 应该能看到 cgroup, ipc, mnt, net, pid, user, uts 等 # 检查 Binder 驱动 ls /dev/binder* # 应该能看到 /dev/binder, /dev/hwbinder, /dev/vndbinder # 检查 Ashmem ls /dev/ashmem如果 Binder 或者 Ashmem 设备节点不存在说明内核没有编译对应的驱动需要重新配置内核。RK3588 的官方内核配置里CONFIG_ANDROID_BINDER_IPC、CONFIG_ANDROID_BINDERFS、CONFIG_ASHMEM这几个选项默认是打开的但如果你自己裁剪过内核就要特别留意。2.2 Docker 安装与关键配置调整RK3588 是 ARM64 架构Docker 的安装跟 x86 平台略有不同。Ubuntu 22.04 的官方源里已经有 docker.io 包但我建议用 Docker 官方的安装脚本版本更新对 ARM64 的支持也更好。# 卸载可能存在的旧版本 sudo apt remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # 添加 Docker 官方 GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加软件源 echo deb [archarm64 signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装 Docker sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后有几个配置必须改。第一个是 cgroup 驱动。Docker 默认可能用 cgroupfs但 systemd 系统建议用 systemd 驱动否则会出现资源限制不生效的问题。编辑/etc/docker/daemon.json{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2, iptables: true, ip-forward: true }第二个是内核参数。Android 容器需要比较大的共享内存和文件描述符限制在/etc/sysctl.conf里加上fs.inotify.max_user_watches524288 fs.inotify.max_user_instances512 kernel.shmmax268435456 kernel.shmall65536改完执行sudo sysctl -p生效然后重启 Docker 服务。注意RK3588 上如果之前装过 docker.io 的包卸载后要手动删除/var/lib/docker目录否则残留的镜像层会导致新装的 Docker 启动失败。2.3 内核模块与设备节点准备AIC 容器启动时Android 的 init 进程会去访问一堆设备节点。这些节点在宿主机上必须存在并且容器要有权限访问。除了前面提到的 Binder 和 Ashmem还需要确认这几个# 检查 GPU 设备 ls /dev/mali* # 应该能看到 /dev/mali0 # 检查 VPU 设备 ls /dev/mpp_service /dev/rga # 应该能看到这两个 # 检查显示设备 ls /dev/dri/ # 应该能看到 card0, renderD128 等如果这些设备节点不存在说明对应的内核驱动没有加载。RK3588 的 GPU 驱动是 Mali 的 kbase 驱动VPU 驱动是 Rockchip 的 mpp 驱动这些在官方内核里都是默认编译的。如果缺失检查/boot/extlinux/extlinux.conf里的设备树是否正确加载。还有一个关键点是devtmpfs的挂载。容器里需要看到宿主机的设备节点所以 Docker 启动时要挂载/dev。这个在后面的启动参数里会详细说。3. 制作 Android 容器镜像3.1 获取 Android 根文件系统AIC 的 Android 镜像不是从 Google 官网下载的通用镜像而是需要针对 RK3588 硬件专门制作的。最省事的办法是用 Rockchip 官方 SDK 编译出来的 Android 镜像从中提取出根文件系统。如果你手里已经有 RK3588 的 Android 固件比如 update.img可以用工具解包。Rockchip 提供了afptool和imgRePacker工具在 Linux 下解包# 解包 update.img afptool -unpack update.img output_dir/ # 进入 output_dir会看到 boot.img, system.img, vendor.img, odm.img 等 # 用 simg2img 把 sparse 格式转成 raw 格式 simg2img system.img system_raw.img simg2img vendor.img vendor_raw.img simg2img odm.img odm_raw.img # 挂载提取文件 mkdir -p /mnt/system /mnt/vendor /mnt/odm sudo mount -o loop system_raw.img /mnt/system sudo mount -o loop vendor_raw.img /mnt/vendor sudo mount -o loop odm_raw.img /mnt/odm然后把这三个分区的文件合并到一个目录里作为 Android 容器的根文件系统。合并的时候要注意文件覆盖顺序先放 system再放 vendor最后放 odm因为后面的会覆盖前面的同名文件。mkdir -p android_rootfs sudo cp -a /mnt/system/* android_rootfs/ sudo cp -a /mnt/vendor/* android_rootfs/ sudo cp -a /mnt/odm/* android_rootfs/合并完成后还需要做一些调整。Android 的 init 进程默认会去挂载/system、/vendor等分区但在容器里这些已经是根文件系统的一部分了不需要再挂载。所以要修改android_rootfs/init.rc或者android_rootfs/system/etc/init/hw/init.rc把相关的 mount 操作注释掉。3.2 编写 Dockerfile 构建镜像有了根文件系统接下来写 Dockerfile。我用的基础镜像是arm64v8/ubuntu:22.04因为 Android 容器里也需要一些基础的 Linux 工具。FROM arm64v8/ubuntu:22.04 # 安装基础依赖 RUN apt update apt install -y \ libc6 \ libstdc6 \ libz1 \ zlib1g \ liblog1 \ libcutils1 \ rm -rf /var/lib/apt/lists/* # 复制 Android 根文件系统 COPY android_rootfs/ / # 设置环境变量 ENV ANDROID_ROOT/system ENV ANDROID_DATA/data ENV ANDROID_RUNTIME_ROOT/apex/com.android.runtime ENV ANDROID_TZDATA_ROOT/apex/com.android.tzdata # 创建必要的目录 RUN mkdir -p /data /dev/socket /dev/binderfs /mnt/vendor /mnt/odm # 设置 init 为入口 ENTRYPOINT [/system/bin/init]构建镜像的时候RK3588 上直接 build 会比较慢因为 Android 根文件系统有好几个 GB。建议在 x86 的 PC 上用 buildx 做交叉构建或者用 qemu 模拟。不过最直接的办法还是把根文件系统拷贝到 RK3588 上本地构建。# 在 RK3588 上构建 docker build -t aic-android:latest .构建过程中如果遇到COPY失败检查一下android_rootfs目录的权限Docker 构建时是以 root 身份运行的但有些文件可能权限不对。可以用sudo chown -R root:root android_rootfs统一权限。3.3 镜像瘦身与启动脚本优化Android 根文件系统里有很多在容器环境里用不到的东西比如 recovery 相关的文件、OTA 升级包、测试工具等。把这些删掉能显著减小镜像体积。我一般会删掉这些目录# 在 android_rootfs 里执行 rm -rf system/recovery rm -rf system/ota rm -rf system/app/Testing* rm -rf system/bin/*test* rm -rf vendor/firmware/*.bin # 如果固件已经在宿主机加载了另外Android 的 init 进程在容器里启动时需要一些额外的参数。我写了一个启动脚本start_aic.sh放在镜像里#!/bin/bash # start_aic.sh # 挂载必要的文件系统 mount -t tmpfs tmpfs /dev mount -t tmpfs tmpfs /tmp mount -t proc proc /proc mount -t sysfs sysfs /sys # 创建 Binder 设备节点 mkdir -p /dev/binderfs mount -t binder binder /dev/binderfs ln -s /dev/binderfs/binder /dev/binder ln -s /dev/binderfs/hwbinder /dev/hwbinder ln -s /dev/binderfs/vndbinder /dev/vndbinder # 创建 Ashmem mkdir -p /dev/ashmem mount -t tmpfs tmpfs /dev/ashmem # 启动 Android init exec /system/bin/init这个脚本在容器启动时执行把容器内的文件系统环境准备好然后再拉起 Android 的 init。4. 启动容器与硬件直通配置4.1 Docker run 参数详解启动 AIC 容器不是一条简单的docker run就能搞定的需要把宿主机的设备节点、GPU、VPU 都直通给容器。我用的启动命令是这样的docker run -itd \ --name aic_android \ --privileged \ --network host \ --ipc host \ --pid host \ -v /dev:/dev \ -v /dev/dri:/dev/dri \ -v /dev/mali0:/dev/mali0 \ -v /dev/mpp_service:/dev/mpp_service \ -v /dev/rga:/dev/rga \ -v /data/aic_data:/data \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -e DISPLAY$DISPLAY \ aic-android:latest逐条解释这些参数的必要性--privileged是必须的因为 Android init 需要挂载文件系统、创建设备节点、修改网络配置这些操作在非特权容器里做不了。虽然从安全角度来说不太理想但在开发板上跑安全边界不是首要考虑。--network host让容器直接用宿主机的网络栈。Android 的网络管理服务需要直接操作网络接口用 bridge 模式会有各种问题。而且 host 模式下Android 容器里的 App 可以直接访问宿主机的服务调试方便。--ipc host和--pid host是为了让 Android 的 Binder IPC 和进程管理能正常工作。Android 的 servicemanager 需要看到宿主机的进程命名空间否则 Binder 通信会失败。-v /dev:/dev把宿主机的设备节点全部映射进容器。这是最省事的做法但要注意容器里的 Android 可能会去操作一些不该操作的设备。如果追求精细控制可以只映射需要的设备节点。-v /data/aic_data:/data把 Android 的 data 分区映射到宿主机的目录这样容器重启后数据不会丢。这个目录要提前创建好并且给足够的权限。4.2 GPU 与 VPU 直通的关键细节GPU 直通是 AIC 方案里最容易出问题的地方。RK3588 的 Mali-G610 GPU 在宿主机上由 kbase 驱动管理容器里的 Android 要直接用这个 GPU需要满足几个条件。第一容器里的 Mali 用户态驱动版本必须和内核态驱动匹配。宿主机 Ubuntu 里的 Mali 驱动版本可以用cat /sys/class/misc/mali0/device/gpuinfo查看。Android 侧的 Mali 驱动在vendor/lib64/egl/下面版本号要一致。如果不一致GPU 渲染会直接失败logcat 里会看到Failed to initialize EGL之类的错误。第二容器里需要正确的libmali.so符号链接。Android 的 gralloc 和 hwcomposer 会去加载libmali.so这个库在vendor/lib64/下面。如果链接不对GPU 初始化会失败。第三/dev/mali0的权限要正确。在宿主机上执行ls -l /dev/mali0确保容器里的进程有读写权限。如果权限不对可以在启动脚本里加chmod 666 /dev/mali0。VPU 直通相对简单一些。RK3588 的 VPU 通过/dev/mpp_service和/dev/rga暴露给用户态。Android 侧的libmpp和librga会去操作这两个设备。只要设备节点映射进去了一般就能用。测试 VPU 是否工作可以在 Android 容器里跑# 在 Android 容器里执行 /system/bin/media_codec_test --codecvideo/avc --file/data/test.mp4如果硬解成功说明 VPU 直通没问题。4.3 显示输出与输入设备处理Android 容器要显示画面需要把宿主机的显示设备映射进去。RK3588 的显示子系统通过/dev/dri/card0和/dev/dri/renderD128暴露。容器里的 SurfaceFlinger 会去操作这些设备。但这里有个问题宿主机 Ubuntu 的显示服务可能已经占用了/dev/dri/card0。如果宿主机跑的是带桌面的 UbuntuXorg 或者 Wayland 会占用显示设备Android 容器再想用就会冲突。解决办法有两个一是宿主机跑无桌面的 Ubuntu Server显示设备完全交给 Android 容器二是宿主机用 Xorg 的 dummy 驱动把物理显示设备让出来。我一般用第一种方案宿主机不装桌面环境需要图形界面的时候通过 Android 容器里的 VNC 或者 scrcpy 来访问。这样最干净没有资源竞争。输入设备方面触摸屏和遥控器的设备节点在/dev/input/下面。Android 的 EventHub 会去读取这些节点。把/dev/input映射进容器后Android 就能识别触摸和按键事件。如果触摸方向不对需要调整vendor/etc/input.idc配置文件里的touch.orientation参数。5. 常见问题排查与实战避坑5.1 容器启动失败类问题问题一Binder 设备节点创建失败现象是容器启动后logcat 里不断报binder: cannot open /dev/binder。原因是容器里的/dev是 tmpfs启动脚本里虽然创建了 binderfs 挂载点但挂载顺序不对。解决办法确保在挂载 binderfs 之前/dev已经挂载为 tmpfs。启动脚本里的顺序不能乱先mount -t tmpfs tmpfs /dev再mkdir -p /dev/binderfs再mount -t binder binder /dev/binderfs。如果顺序反了binderfs 会被 tmpfs 覆盖掉。问题二Android init 启动后卡在开机动画这个问题的原因比较多最常见的是 SELinux 权限问题。Android 的 init 在容器里跑的时候SELinux 的策略可能不匹配。可以在内核启动参数里加androidboot.selinuxpermissive先关掉 SELinux enforcing 模式等系统起来后再慢慢调策略。另一个原因是/data目录的权限不对。Android 的 init 会去检查/data的属主和权限如果不是system:system且权限为0771init 会拒绝启动。在启动脚本里加上chown system:system /data chmod 0771 /data问题三GPU 初始化失败导致 SurfaceFlinger 崩溃logcat 里看到Failed to initialize EGL或者gralloc: failed to open /dev/mali0。先检查/dev/mali0是否映射进了容器再检查 Mali 驱动版本是否匹配。如果版本不匹配需要从宿主机的/usr/lib/aarch64-linux-gnu/libmali.so拷贝到容器的vendor/lib64/下面替换掉 Android 自带的版本。5.2 性能调优与资源限制AIC 容器跑起来之后性能调优主要从三个方面入手CPU 调度、内存管理、IO 优先级。CPU 方面RK3588 的 8 个核心可以分配给容器不同的核心。如果宿主机上还有其他任务可以用--cpuset-cpus参数把 Android 容器绑定到特定的核心上。比如把大核留给 Android小核留给宿主机--cpuset-cpus4-7 # 只用 A76 大核内存方面Android 容器默认可以用完宿主机的所有内存。如果宿主机还要跑其他服务需要限制容器的内存使用--memory4g --memory-swap4gIO 方面Android 的 data 分区如果放在 eMMC 上IO 性能会比较好。如果放在 TF 卡上建议加--device-read-bps和--device-write-bps限制避免 Android 的 IO 把 TF 卡跑满导致宿主机卡顿。5.3 网络与 ADB 连接问题Android 容器起来之后怎么连 ADB 是个高频问题。因为容器用的是 host 网络模式Android 的 adbd 会监听宿主机的 5555 端口。在 PC 上执行adb connect RK3588_IP:5555如果连不上先在 RK3588 上检查 adbd 是否在跑# 在 Android 容器里执行 ps -A | grep adbd如果没有 adbd 进程说明 Android 的 adbd 服务没有启动。可以在 Android 容器里手动启动setprop service.adb.tcp.port 5555 stop adbd start adbd还有一个常见问题是 PC 上的 adb 版本太老跟 Android 容器里的 adbd 版本不匹配。建议用最新版的 platform-tools或者直接用 RK3588 宿主机上的 adb 连接本地容器# 在 RK3588 宿主机上执行 adb connect 127.0.0.1:55555.4 常见问题速查表问题现象可能原因排查命令解决方法容器启动后立即退出init 找不到或权限不对docker logs aic_android检查 ENTRYPOINT 路径和文件权限Binder 通信失败binderfs 未正确挂载ls /dev/binder*调整启动脚本挂载顺序GPU 渲染黑屏Mali 驱动版本不匹配dmesggrep mali视频硬解失败VPU 设备未映射ls /dev/mpp_service添加-v /dev/mpp_service参数ADB 连不上adbd 未启动或端口占用netstat -tlnpgrep 5555触摸方向不对input.idc 配置错误getevent -l修改 touch.orientation 参数系统卡顿内存不足或 CPU 争抢docker stats限制容器资源或调整 cpuset6. 实际使用中的经验与扩展玩法6.1 多实例部署与资源隔离一块 RK3588 板子跑一个 Android 容器有点浪费8 核 CPU 加 8GB 内存跑两到三个 Android 容器完全没问题。多实例部署的关键是资源隔离。每个容器分配不同的 CPU 核心和内存配额# 容器 1 docker run -itd --name aic_1 --cpuset-cpus4-5 --memory2g ... # 容器 2 docker run -itd --name aic_2 --cpuset-cpus6-7 --memory2g ...但 GPU 是共享的多个 Android 容器同时用 GPU 的时候Mali 驱动会做时间片调度。如果某个容器跑重度 GPU 任务其他容器的渲染会变慢。这个目前没有很好的隔离方案只能靠业务层面做错峰。网络方面多实例建议用不同的端口。Android 的 adbd 默认监听 5555第二个实例改成 5556以此类推。在容器的启动脚本里加setprop service.adb.tcp.port 5556。6.2 与宿主机 Ubuntu 的数据互通AIC 方案的一个好处是 Android 容器和宿主机 Ubuntu 可以方便地共享数据。我一般会挂载两个共享目录-v /home/ubuntu/share:/mnt/share -v /data/aic_data:/data/mnt/share是宿主机和 Android 都能读写的公共目录用来传文件、放测试数据。/data是 Android 的 data 分区宿主机也能访问方便备份和调试。如果要在 Android 容器里访问宿主机的服务直接用127.0.0.1就行因为用的是 host 网络模式。比如宿主机上跑了一个 HTTP 服务在 8080 端口Android 容器里的浏览器直接访问http://127.0.0.1:8080就能打开。反过来宿主机要访问 Android 容器里的服务也是127.0.0.1。比如 Android 里跑了一个 RTSP 服务在 8554 端口宿主机上用 ffmpeg 拉流ffmpeg -i rtsp://127.0.0.1:8554/test -c copy output.mp46.3 结合 NPU 做 AI 推理的扩展思路RK3588 的 NPU 是这块板子的核心价值之一。AIC 方案里NPU 可以通过 RKNN 运行时在 Android 容器里直接调用。Rockchip 提供了 Android 版的 RKNN SDK把librknnrt.so放到vendor/lib64/下面Android 应用就能用 NPU 做推理。我试过在 Android 容器里跑 YOLOv8 的目标检测用 NPU 加速1080p 的视频流能做到 30fps 以上。具体做法是在宿主机上用 RKNN Toolkit 把 YOLOv8 的 ONNX 模型转成 RKNN 格式然后把模型文件和推理代码放到 Android 容器的/data目录里Android 应用通过 JNI 调用 RKNN 的 C 接口。这个玩法的想象空间很大。比如你可以做一个智能摄像头方案宿主机 Ubuntu 负责拉 RTSP 流、做视频解码Android 容器负责 UI 显示和 AI 推理两边通过共享内存交换数据。RK3588 的 VPU 解码、GPU 渲染、NPU 推理可以同时跑各司其职。6.4 系统备份与快速恢复开发板上折腾系统最怕的就是把系统搞崩了要重新刷机。AIC 方案的一个好处是 Android 容器可以随时备份和恢复。备份 Android 容器的 data 分区# 在宿主机上执行 docker stop aic_android tar -czf aic_data_backup.tar.gz -C /data/aic_data . docker start aic_android恢复的时候反过来操作就行。如果整个容器都搞坏了直接删掉重建docker rm -f aic_android docker run -itd --name aic_android ... aic-android:latest因为 Android 的系统文件都在镜像里data 分区是挂载出来的所以重建容器不会丢失数据。这个特性在做 Android 应用开发的时候特别有用测试搞崩了直接重置几秒钟就能恢复到一个干净的 Android 环境。宿主机 Ubuntu 本身的备份我建议用 RK3588 的 maskrom 模式加瑞芯微的开发工具做全盘镜像备份。虽然麻烦一点但系统彻底挂了的时候能救命。平时的话把/etc、/home、/data这几个目录定期打包备份就够用了。6.5 几个容易踩的坑第一个坑是内核版本。RK3588 的官方 Ubuntu 镜像有好几个版本内核从 5.10 到 6.1 都有。AIC 方案在 5.10 内核上最稳定6.1 内核虽然也能跑但 Binder 驱动的行为有变化需要额外打补丁。如果你刷的是 6.1 内核的镜像遇到 Binder 相关的问题先考虑换回 5.10。第二个坑是 Docker 的存储驱动。RK3588 的 eMMC 性能一般overlay2 驱动在小文件读写多的时候会比较慢。如果发现 Android 容器启动特别慢可以试试把 Docker 的>