Android系统编译并打包成容器镜像的原理详解

一、背景分析

之所以能够把Android打包成docker,主要还是由于Android建立在Linux kernel之上。我们知道,容器技术实现的基础是chroot+namespace+cgroups,这些功能都是由内核直接提供的。那么从理论上而言,将Android的init的进程以容器的形式跑在一个独立的名称空间中是完全可行的。

此时Android的最底层的kernel将共享宿主机的kernel:

  • 当宿主机kernel运行在ARM处理器时,容器在CPU及内存层面几乎没有损耗。

  • 当宿主机kernel运行在X86处理器时,需转义成ARM指令集,存在损耗。

Android的kernel与桌面的kernel相比,做了一定的定制和优化,比如:

  • 增加了移动设备所需的特定驱动程序和功能的支持。

  • 针对移动设备特定功能的补丁和改进,以提高功耗效率、性能和与移动硬件的兼容性。

同时对于内核进行了扩展,比如:

  • 增加了Binder,Android系统中的进程间通信(IPC)机制,用于不同应用程序组件之间的通信。它允许应用程序在不同的进程之间传递数据,执行远程调用等。这种机制对于Android的组件化架构和应用间通信非常关键。

  • 增加了Ashmem,Android Shared Memory,用于共享匿名内存的机制。它允许不同的应用程序共享内存,这对于共享数据和内存映射文件对Android系统的性能和资源管理非常重要。

从实际运行的角度而言,我们只需要保证我们的X86 server系统,如Ubuntu的运行中的内核,包含了Android内核所需的特定模块,就能确保Android系统能在主机内核的基础上跑起来。

二、remote-android项目分析

这个项目的文档支撑不是很好,对于Android framework部分几乎没有谈及,只是对于使用部分进行了简单的介绍。好在repo的整体架构比较清晰。

说明部分,含部分操作代码:

Framework部分:

Android patch脚本:

X86 to ARM指令转义:

docker build部分:

三、Android系统源码整合

Android系统源码极其庞大,使用传统的git进行组织及管理已经不现实。因此google开发了repo工具进行代码的辅助管理。

建议直接下载google官方最新的repo工具并放置到PATH中。通过apt安装的版本可能不支持最新的功能。
link:https://storage.googleapis.com/git-repo-downloads/repo

源码的下载,Android的源码大小已经达到了上百G,原始存储在https://android.googlesource.com这个网站,已经被墙。因此想通过VPN直接下载下来不是很现实。同时,我们在进行repo同步前,还有可能对于相关的依赖进行二次的裁剪及增加,如通过VPN控制,会使得整个过程变得比较复杂。

因此建议将最原始的google/LineageOS的源替换成清华源,其余的部分不会非常大,可以继续使用VPN拉取,确保稳定。
cat >> ~/.gitconfig <<EOF

[url "https://mirrors.tuna.tsinghua.edu.cn/git/git-repo"]
	insteadof = https://gerrit.googlesource.com/git-repo
[url "https://mirrors.tuna.tsinghua.edu.cn/git/lineageOS/"]
	insteadof = https://review.lineageos.org/
[url "https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/"]
	insteadof = https://android.googlesource.com/
[url "https://mirrors.tuna.tsinghua.edu.cn/git/lineageOS/LineageOS/"]
	insteadof = https://github.com/LineageOS/
EOF


源码下载完成之后就可以进行正常的framework的开发、优化、配置,最终编译。
这个部分是整个项目最核心的部分,需要Android framework工程师参与,一般的业务开发可能还不行。

redroid的源码准备过程:
# fetch code
#####################
mkdir ~/redroid && cd ~/redroid
# check supported branch in https://github.com/remote-android/redroid-patches.git
repo init -u https://android.googlesource.com/platform/manifest --git-lfs --depth=1 -b android-11.0.0_r48
# add local manifests
git clone https://github.com/remote-android/local_manifests.git ~/redroid/.repo/local_manifests -b 11.0.0
# sync code
repo sync -c
# apply redroid patches
git clone https://github.com/remote-android/redroid-patches.git ~/redroid-patches
~/redroid-patches/apply-patch.sh ~/redroid

四、Android系统的编译过程

redroid基于docker容器进行Anroid系统的打包,打包时会临时启动一个交互式接口的容器。

创建build容器:
git clone <https://github.com/remote-android/redroid-doc>
cd android-builder-docker/
sudo docker build --build-arg userid=$(id -u) --build-arg groupid=$(id -g) --build-arg username=$(id -un) -t redroid-builder .

整个容器的重点创建了与宿主机完全一样的uid/gid的用户,并完成Android系统编译所需依赖包的安装,最终使用与宿主机一致的用户chroot到该容器内,开展进一步的编译。
特别注意,这个容器仅仅用作编译使用,与后期android容器的制作无关。同时,理论上直接使用宿主机编译也是可以的。

raw Dockerfile:

FROM ubuntu:20.04
ARG userid
ARG groupid
ARG username
# COPY apt.conf /etc/apt/apt.conf
# COPY sources.list etc/apt/sources.list
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \\
    && echo "install package for building AOSP" \\
    && apt-get install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \\
        gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev \\
        libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig \\
    && echo "install utils" \\
    && apt-get install -y sudo rsync \\
    && echo "install packages for build mesa3d or meson related" \\
    && apt-get install -y python3-pip pkg-config python3-dev ninja-build \\
    && pip3 install mako meson \\
    && echo "packages for legacy mesa3d (< 22.0.0)" \\
    && apt-get install -y python2 python-mako python-is-python2 python-enum34 gettext
RUN groupadd -g $groupid $username \\
    && useradd -m -u $userid -g $groupid $username \\
    && echo "$username ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \\
    && echo $username >/root/username \\
    && echo "$username:$username" | chpasswd && adduser $username sudo
ENV HOME=/home/$username \\
    USER=$username \\
    PATH=/src/.repo/repo:/src/prebuilts/jdk/jdk8/linux-x86/bin/:$PATH
ENTRYPOINT chroot --userspec=$(cat /root/username):$(cat /root/username) / /bin/bash -i

Android系统编译:
# start builder
#####################
docker run -it --rm --hostname redroid-builder --name redroid-builder -v ~/redroid:/src redroid-builder
#################### 
# build redroid
#####################
cd /src
. build/envsetup.sh
lunch redroid_x86_64-userdebug
# redroid_arm64-userdebug
# redroid_x86_64_only-userdebug (64 bit only, redroid 12+)
# redroid_arm64_only-userdebug (64 bit only, redroid 12+)
# start to build
m

五、容器镜像制作

在讲容器镜像制作之前,简单的对于linux的系统启动流程进行一下回顾,不然其实比较难以理解OS镜像制作的本质是什么。

一个传统的linux操作系统的启动主要由以下部分组成:

POST → (legacy/UEFI) BIOS → 按照顺序找到启动介质 → 找到bootloader (MBR/GPT) → kernel(ramdisk/ramfs) → rootfs(只读) → switchroot(chroot) → init

当我们使用容器的方式启动一个系统时,kernel是已经启动的状态,并且已经加载所需的模块。启动容器时,会调用kernel中namespace相关的系统调用,创建一个隔离的名称空间,挂载文件目录,chroot后init即可。

因此,创建一个OS镜像的本质就是制定一个根文件系统并指定该文件系统的init程序,也就是Android系统的init。

理解之后,就可以进行镜像的制作。

# create redroid image in *HOST*
#####################
cd ~/redroid/out/target/product/redroid_x86_64
sudo mount system.img system -o ro
sudo mount vendor.img vendor -o ro
sudo tar --xattrs -c vendor -C system --exclude="vendor" . |sudo  docker import -c 'ENTRYPOINT ["/init", "androidboot.hardware=redroid"]' - redroid
sudo umount system vendor
# create rootfs only image for develop purpose
tar --xattrs -c -C root . | docker import -c 'ENTRYPOINT ["/init", "androidboot.hardware=redroid"]' - redroid-dev

此处使用tar及docker import指令相结合的方式,将Android系统的文件目录打包成基础镜像,将Androidinit指定为ENTRYPOINT,这样就可以在启动时方便的接收参数。

cd C:\\Users\\Administrator\\Desktop\\scrcpy-win64-v2.0
.\\adb.exe connect  192.168.100.199:5555
.\\scrcpy.exe -s 192.168.100.199:5555

Param

Description

Default

androidboot.redroid_width

display width

720

androidboot.redroid_height

display height

1280

androidboot.redroid_fps

display FPS

30(GPU enabled)15 (GPU not enabled)

androidboot.redroid_dpi

display DPI

320

androidboot.use_memfd

use memfd to replace deprecated ashmemplan to enable by default

false

androidboot.use_redroid_overlayfs

use overlayfs to share data partition/data-base: shared data partition/data-diff: private data

0

androidboot.redroid_net_ndns

number of DNS server, 8.8.8.8 will be used if no DNS server specified

0

androidboot.redroid_net_dns<1..N>

DNS



androidboot.redroid_net_proxy_type

Proxy type; choose from: static, pac, none, unassigned



androidboot.redroid_net_proxy_host





androidboot.redroid_net_proxy_port



3128

androidboot.redroid_net_proxy_exclude_list

comma seperated list



androidboot.redroid_net_proxy_pac





androidboot.redroid_gpu_mode

choose from: auto, host, guest;guest: use software rendering;host: use GPU accelerated rendering;auto: auto detect

auto

androidboot.redroid_gpu_node



auto-detect

ro.xxx

DEBUG purpose, allow override ro.xxx prop; For example, set ro.secure=0, then root adb shell provided by default