探索Anbox运行Andorid程序

说明

本文记录的是我折腾数天,在Arch Linux中通过Anbox运行Android程序的经历。这个过程非常曲折,所以我把最终经验总结重新整理成 Anbox运行Andorid程序 ,本文则依然有探索过程排查异常的经验,可以帮助我们了解Android和Anbox运行的原理以及问题排查的思路。

记录比较散乱,也许对你排查会有启发,所以汇总如下。

Anbox

Anbox是开源兼容层,通过LXC容器构建Android运行环境,使得移动应用程序能偶运行在Linux环境。由于使用容器,采用原生Linux内核执行应用程序,所以非常轻量级并且保障了运行速度。

安装Anbox

  • 首先确保系统安装了Linux内核头文件:

    sudo pacman -S linux-headers
    
  • 安装Anbox,如果镜像中不想包含Google apps和houdini,则用 anbox-image 替代 anbox-image-gapps:

    yay -S anbox-git anbox-image-gapps anbox-modules-dkms-git anbox-bridge
    

注解

参考 如何在 Anbox 上安装 Google Play 商店及启用 ARM 支持

默认情况下,Anbox没有Google Play商店或者ARM应用支持。要安装应用,你必须下载每个应用的 APK 并使用 adb 手动安装。此外,默认情况下不能使用 Anbox 安装 ARM 应用或游戏 —— 尝试安装 ARM 应用会显示以下错误:

Failed to install PACKAGE.NAME.apk: Failure [INSTALL_FAILED_NO_MATCHING_ABIS: Failed to extract native libraries, res=-113]

通过 libhoudini 可以支持ARM应用。所以,建议上述安装Anbox时使用 anbox-image-gapps

  • 启动和激活服务:

    sudo systemctl start anbox-container-manager.service
    sudo systemctl enable anbox-container-manager.service
    
  • 如果没有重启主机,则使用以下命令激活DKMS模块:

    sudo modprobe ashmem_linux
    sudo modprobe binder_linux
    

此时检查设备:

ls -1 /dev/{ashmem,binder}

应该看到如下:

/dev/ashmem
/dev/binder

使用Anbox准备

  • 在执行 anbox 之前,需要先执行 anbox-bridge 来激活网络,此时使用 brctl show 可以看到:

    bridge name  bridge id               STP enabled     interfaces
    anbox0               8000.1edb3f6031c8       no
    
  • 安装 adb 工具(用于给虚拟机内部安装应用程序):

    pacman -S android-tools
    

编译报错处理

  • 编译报错:

    /home/huatai/.cache/yay/anbox-git/src/anbox/src/anbox/logger.cpp:20: error: "BOOST_LOG_DYN_LINK" redefined [-Werror]
       20 | #define BOOST_LOG_DYN_LINK
    
这个报错解决方法见 https://bbs.archlinux.org/viewtopic.php?id=249747 ,其中有关patch的方法参考 Arch Linux AUR 中补丁方法。

运行报错处理

  • 先在菜单点击 anbox-bridge 启动网桥
  • 然后在菜单启动 anbox

但是我遇到界面停留在 starting... ,并且执行 adb devices 显示并没有启动模拟设备。

使用命令行调试:

export ANBOX_LOG_LEVEL=debug
anbox session-manager --gles-driver=translator

显示:

[ 2019-10-08 14:25:21] [Renderer.cpp:168@initialize] Using a surfaceless EGL context
[ 2019-10-08 14:25:21] [Renderer.cpp:251@initialize] Successfully initialized EGL
[ 2019-10-08 14:25:21] [service.cpp:41@Service] Successfully acquired DBus service name
[ 2019-10-08 14:25:21] [client.cpp:49@start] Failed to start container: Failed to start container: Failed to start container
[ 2019-10-08 14:25:21] [session_manager.cpp:148@operator()] Lost connection to container manager, terminating.
[ 2019-10-08 14:25:21] [daemon.cpp:61@Run] Container is not running
Stack trace (most recent call last) in thread 9740:

anbox的container日志位于 /var/lib/anbox/logs/container.log 可以看到以下报错:

...
lxc 20191009031012.389 TRACE    cgfsng - cgroups/cgfsng.c:cg_hybrid_init:2562 - No controllers are enabled for delegation in the unified hierarchy
lxc 20191009031012.415 TRACE    cgfsng - cgroups/cgfsng.c:cg_hybrid_init:2589 - Writable cgroup hierarchies:
...
lxc 20191009031012.458 TRACE    cgroup - cgroups/cgroup.c:cgroup_init:61 - Initialized cgroup driver cgfsng
lxc 20191009031012.459 TRACE    cgroup - cgroups/cgroup.c:cgroup_init:66 - Running with hybrid cgroup layout
lxc 20191009031012.459 TRACE    start - start.c:lxc_init:923 - Initialized cgroup driver
lxc 20191009031012.460 TRACE    start - start.c:lxc_init:930 - Initialized LSM
lxc 20191009031012.461 INFO     start - start.c:lxc_init:932 - Container "default" is initialized
lxc 20191009031012.516 DEBUG    cgfsng - cgroups/cgfsng.c:cg_legacy_filter_and_set_cpus:499 - No isolated or offline cpus present in cpuset
lxc 20191009031012.568 INFO     cgfsng - cgroups/cgfsng.c:cgfsng_monitor_create:1405 - The monitor process uses "lxc.monitor/default" as cgroup
lxc 20191009031012.587 ERROR    cgfsng - cgroups/cgfsng.c:__do_cgroup_enter:1500 - No space left on device - Failed to enter cgroup "/sys/fs/cgroup/cpuset//lxc.monitor/default/cgroup.procs"
lxc 20191009031012.589 ERROR    start - start.c:__lxc_start:2009 - Failed to enter monitor cgroup
lxc 20191009031012.596 TRACE    start - start.c:lxc_serve_state_socket_pair:543 - Sent container state "STOPPING" to 13
lxc 20191009031012.598 TRACE    start - start.c:lxc_serve_state_clients:474 - Set container state to STOPPING
lxc 20191009031012.600 TRACE    start - start.c:lxc_serve_state_clients:477 - No state clients registered
lxc 20191009031012.600 DEBUG    lxccontainer - lxccontainer.c:wait_on_daemonized_start:861 - First child 1862 exited
lxc 20191009031012.604 ERROR    lxccontainer - lxccontainer.c:wait_on_daemonized_start:872 - Received container state "STOPPING" instead of "RUNNING"
lxc 20191009031012.694 DEBUG    cgfsng - cgroups/cgfsng.c:cg_legacy_filter_and_set_cpus:499 - No isolated or offline cpus present in cpuset
lxc 20191009031012.706 WARN     cgfsng - cgroups/cgfsng.c:cgfsng_monitor_destroy:1180 - No space left on device - Failed to move monitor 1863 to "/sys/fs/cgroup/cpuset//lxc.pivot/cgroup.procs"
lxc 20191009031012.868 TRACE    start - start.c:lxc_fini:1043 - Closed command socket
lxc 20191009031012.873 TRACE    start - start.c:lxc_fini:1054 - Set container state to "STOPPED"
lxc 20191009031012.567 TRACE    commands - commands.c:lxc_cmd:302 - Connection refused - Command "get_state" failed to connect command socket

检查启动以后建立的 /sys/fs/cgroup/cpuset/lxc.monitor/ 和子目录 lxc.monitor 的所有proc文件内容都是空的,例如 /sys/fs/cgroup/cpuset/lxc.monitor/cpuset.cpus/sys/fs/cgroup/cpuset/lxc.monitor/default/cpuset.cpus ,发现目录下所有的设置值都是空的。参考 Cgroup - no space left on device ,实际上这些proc文件需要有初始值,否则就会出现 no space left on device

为何创建的cgroup配置没有默认继承上一级cgroup配置?

参考 cgfsng - cgroups/cgfsng.c:__do_cgroup_enter:1500 - No space left on device - Failed to enter cgroup “/sys/fs/cgroup/cpuset//lxc.monitor/test/cgroup.procs” #6257 ,child cgroup继承parent的开关参数在 /sys/fs/cgroup/cpuset/cgroup.clone_children ,检查arch linux的默认配置,这个参数值是 0 ,也就是没有继承:

$ cat /sys/fs/cgroup/cpuset/cgroup.clone_children
0

解决方法是在启动anbox之前,先执行:

echo 1 | sudo tee /sys/fs/cgroup/cpuset/cgroup.clone_children

这样所有创建的cgroup子项默认继承上一级配置,就不会出现空值,也就不会出现 No space left on device ,再检查 /var/lib/anbox/logs/container.log 就可以看到日志不再出现ERROR。

启动anbox之后,在控制台使用 adb devices 检查可以看到系统运行了一个模拟器:

List of devices attached
emulator-5558        device

但是现在的anbox也只是显示 starting... 然后退出。不过,此时可以看到,原先始终没有输出的 /var/lib/anbox/logs/console.log 现在有大量内容输出,可以看到报错信息了:

10-09 04:03:03.996     9     9 W         : debuggerd: resuming target 8694
10-09 04:03:04.325    26    26 I lowmemorykiller: ActivityManager disconnected
10-09 04:03:04.325    26    26 I lowmemorykiller: Closing Activity Manager data connection
10-09 04:03:04.337    34    34 E         : eof
10-09 04:03:04.338    34    34 E         : failed to read size
10-09 04:03:04.338    34    34 I         : closing connection
10-09 04:03:04.338    27    27 I ServiceManager: service 'batterystats' died
10-09 04:03:04.338    27    27 I ServiceManager: service 'appops' died
...

看来是分配内存过小了。

注解

参考 Android和Linux关系 :

低内存管理(Low Memory Killer) -

Android中低内存管理和Linux标准OOM(Out of Memory)相比,机制更加灵活,可以根据需要杀死进程类释放需要的内存。Low Memory Killer代码非常简单,里面关键函数lowmem_shrinker(),作为一个模块初始化调用register_shrinke注册一个low_shrinker(),会被vm在内存紧张时候调用。lowmem_shrinker完成具体操作,简单寻找一个最合适进程杀死,从而释放它的占用内存。drivers/staging/android/lowmemorykiller.c

由于后台不断重启模拟器android系统,所以console.log会不断输出日志。所以采用如下命令停止:

systemctl --user stop anbox-session-manager.service
systemctl stop anbox-container-manager.service

此时 adb devices 显示模拟器停止了。

anbox splash screen disappears #814 提示修改 /usr/lib/systemd/user/anbox-session-manager.service

ExecStart=/usr/bin/anbox session-manager --gles-driver=host

注解

参考 Anbox Does Not Running On Arch Linux #171 这里 --gles-driver 是允许你修改Anbox使用从物理主机获取libGL.so或者libGLES.so。如果是 --gles-driver=translator 就选择 libGL.so ,这样Anbox就会使用自己的 GL-to-GLES 转换来提供必要的GLES功能给 Android。

另外,这个 Anbox Does Not Running On Arch Linux #171 也提供一条线索 How to install on archlinux or manjaro pls help me?? #305 就是我这里采用的方法。同样也存在应用程序不刷新窗口问题(只有resize才刷新),在 app does not refresh/update #437 讨论过这个问题,是由于系统采用了较新的mesa库导致的,降级mesa库实在太麻烦了。所以还是推荐采用snap来安装Anbox

启动方式:

sudo systemctl start systemd-resolved.service
sudo systemctl start systemd-networkd.service
sudo systemctl start anbox-container-manager.service

systemctl --user start anbox-session-manager.service

最后再启动Anbox应用。果然,这个方法是正确的,现在可以完整的Android模拟器了:

../_images/anbox.png

不过,无法接受鼠标操作 - 实际我发现是Anbox的应用程序不会刷新图形,只有窗口缩放时候才刷新一次。因为我发现Clock一直不更新时间显示,还以为程序是死掉的,实际不是,缩放一下窗口就看到时间是正确的,只是不会刷新窗口内容。这可能就是无法响应鼠标的原因。

另外,程序启动后,后台console.log日志显示无法解析主机名:

Unable to resolve host "android.googleapis.com": No address associated with hostname

不过,通过 ip addr 可以看到物理主机的虚拟网卡IP地址是 192.168.250.1 ,尝试 ping -b 192.168.250.255 然后检查 arp -a 可以看到这个 anbox0 网络中有另外一个地址 192.168.250.2 ,是Anbox虚拟机的IP地址。 - 请参考 Anbox Network Configuration

关闭窗口,尝试命令行运行:

ANBOX_LOG_LEVEL=debug
anbox launch --package=org.anbox.appmgr --component=org.anbox.appmgr.AppViewActivity

完整设置

为了能够今后自动就绪环境,设置系统服务自动启动:

sudo systemctl enable systemd-resolved.service
sudo systemctl enable systemd-networkd.service
sudo systemctl enable anbox-container-manager.service

准备一个个人用户脚本 anbox-setup.sh

echo 1 | sudo tee /sys/fs/cgroup/cpuset/cgroup.clone_children
anbox-bridge
systemctl --user start anbox-session-manager.service

然后就可以运行:

anbox launch --package=org.anbox.appmgr --component=org.anbox.appmgr.AppViewActivity

或者从菜单选择运行anbox

改为snap来安装Anbox

如上所述,在Arch中确实很难解决运行Anbox问题,所以回退到采用snap来保障运行环境。

  • 停止服务:

    systemctl --user stop anbox-session-manager.service
    sudo systemctl stop anbox-container-manager.service
    
  • 卸载安装包,不过保留了anbox-dkms-git

    yay -Rns anbox-git anbox-image-gapps anbox-bridge
    
  • 安装snapd:

    yay -S snapd
    

注解

snapd 安装了一个 /etc/profile.d/snapd.sh 来输出snapd包和桌面的安装路径。需要重启一次系统来使之生效。

注解

从2.36开始, snapd 需要激活激活 AppArmr 来支持Arch Linux。如果没有激活AppArmor,则所有snaps都运行在 devel 模式,意味着它们运行在相同的不受限制访问系统,类似Arch Linux仓库安装的应用。

要使用AppArmor:

systemctl enable --now apparmor.service
systemctl enable --now snapd.apparmor.service
  • 激活snapd:

    sudo systemctl enable --now snapd.socket
    

为了激活经典snap,执行以下命令创建链接:

sudo ln -s /var/lib/snapd/snap /snap
  • 测试

先安装一个简单的 hello-world snap:

sudo snap install hello-world

这里报错:

error: too early for operation, device not yet seeded or device model not acknowledged

需要等一会等环境就绪再重新执行

需要将 /var/lib/snapd/snap/bin 添加到PATH环境(或者如前所述,先重启一次系统)

然后测试:

hello-world
  • (可选)成功以后,通过snap安装snap-store应用商店:

    sudo snap install snap-store
    

注解

通过snap-store可以安装很多重量级软件,具有独立的容器运行环境,不影响系统。

  • 命令行安装anbox(没有位于正式的snap-store中):

    snap install --devmode --beta anbox
    

注意, --devmode 安装的snap不会自动更新,需要使用胰腺癌命令更新:

snap refresh --beta --devmode anbox

如果beta通道的anbox还不能正常工作,则可以尝试edge通道

卸载方法如下:

snap remove anbox

使用snap案例

  • 查询Ubuntu Store:

    snap find <searchterm>
    
  • 安装snap:

    sudo snap install <snapname>
    

安装将下载snap到 /var/lib/snapd/snaps 并挂载成 /var/lib/snapd/snap/snapname 来使之对系统可用。并且将创建每个snap的挂载点,并将它们加入到 /etc/systemd/system/multi-user.target.wants/ 软链接,以便系统嗯启动时素有snap可用。

  • 检查已经安装的snap:

    snap list
    

可以看到:

Name         Version    Rev   Tracking  Publisher   Notes
anbox        4-e1ecd04  158   beta      morphis     devmode
core         16-2.41    7713  stable    canonical✓  core
hello-world  6.4        29    stable    canonical✓  -
  • 更新snap:

    snap refresh
    
  • 检查最新的刷新时间:

    snap refresh --time
    
  • 设置刷新时间,例如每天2次:

    snap set core refresh.timer=0:00-24:00/2
    
  • 删除snap:

    snap remove snapname
    

net::ERR_NAME_NOT_RESOLVED

使用snap来运行anbox,确实非常容易解决之前直接部署在arch linux主机模式无法刷新和相应键盘鼠标的问题。但是,使用webview浏览器就发现,实际上地址解析存在问题:

../_images/anbox_dns_error.png

net::ERR_NAME_NOT_RESOLVED 报错表明Android系统无法解析DNS。

如何在 Anbox 上安装 Google Play 商店及启用 ARM 支持 提到了 anbox需要主机安装DNSmasq ,应该就是提供IP地址分配,以及分配DNS解析。

  • 安装dnsmasq:

    sudo pacman -S dnsmasq
    
  • 修改 /etc/dnsmasq.conf

    interface=anbox0
    bind-interfaces
    dhcp-range=192.168.250.50,192.168.250.150,255.255.255.0,12h
    

注解

dnsmasq 默认就是设置default gw指向自身,以及dns也指向自身

不过,我还是没有解决这个问题。并且,我发现 anbox 项目实际上已经包含了 Internet not working in Anbox on Ubuntu 18.04 介绍的 anbox-bridge.sh 脚本。

  • 重启Anbox:

    sudo systemctl restart snap.anbox.container-manager.service
    
  • 然后重启anbox网络:

    sudo /snap/anbox/current/bin/anbox-bridge.sh restart
    

但是我发现问题并没有解决。

进一步排查

  • 执行 adb shell 命令可以登陆到Android虚拟机内部

ip addr 可以看到Android虚拟机的IP地址是 192.168.250.2 ,对应的网关应该是host主机上的 192.168.250.1 ,但是,在虚拟机内部检查路由:

netstat -rn

显示:

Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
192.168.250.0   0.0.0.0         255.255.255.0   U         0 0          0 eth0

这表明虚拟机没有默认路由指向外网。

参考 anbox Networker Configuration 结合 ` DNS Problem - I do have connection on anbox but it is not resolving names #954 <https://github.com/anbox/anbox/issues/954>`_

# 以下两个默认正确,不需要重复设置
# snap set anbox bridge.address=192.168.250.1
# snap set anbox container.network.address=192.168.250.2

# 以下设置网络默认路由
snap set anbox container.network.gateway=192.168.250.1

# 检查物理主机当前使用的DNS
nmcli dev show | grep DNS

#根据上述输出的DNS记录
#设置anbox的DNS
snap set anbox container.network.dns=192.168.1.1

# 然后重启,注意不能使用systemctl重启 snap.anbox.container-manager.service (会清空配制),而是
sudo killall -9 anbox

# 此时检查服务会看到这些配制都在服务参数中:
ps aux | grep anbox
# 显示:
# /snap/anbox/158/usr/bin/anbox container-manager --data-path=/var/snap/anbox/common/ --android-image=/snap/anbox/158/android.img --daemon --use-rootfs-overlay --container-network-gateway=192.168.250.1 --container-network-dns-servers=192.168.1.1

# 现在可以启动虚拟机
snap run anbox.appmgr

不过,奇怪的是, adb shell 进入anbox虚拟机,依然没有看到正确的配制路由和DNS配制。

参考 ` The anbox container can not connect to the network #443 <https://github.com/anbox/anbox/issues/443>`_ 使用以下命令可以手工解决网络连接问题:

adb shell
su
ip route add default dev eth0 via 192.168.250.1
ip rule add pref 32766 table main
ip rule add pref 32767 table local

然后再 ping 外部IP地址,就会发现虚拟机已经网络就绪了。

注解

实践发现,实际上 anbox Networker Configuration 是有效的,只不过我发现还是需要重启一次物理主机,重启以后anbox服务启动就具备了配制参数,并且新启动的anbox虚拟机的网络就是按照传递配制设置的IP路由。

不过,遇到的DNS解析还是有问题,看起来不是配制问题,有可能是物理主机转发IP masquerade问题。

但此时使用浏览器 WebView 访问网站报错依旧,此时问题原因和DNS解析相关。DNS解析取决于 resolv.conf ,参考 How to set DNS Server on Android Phone ,可以看到Android的DNS配制位于 /system/etc/resolv.conf (系统的 /etc 是软链接到 /system/etc ,不过这个虚拟机中没有配制。

注意, / 文件系统挂载是只读的:

Filesystem            Size  Used Avail Use% Mounted on
overlay                47G   28G   16G  64% /

参考 Root permission for apps #118 的思路,所有的overlay可以从 anbox 进程的配制参数 --data-path=/var/snap/anbox/common/ ,即 overlay 是位于 rootfs-overlay/ 子目录。对应anbox虚拟机内部的 /system/etc 目录就是host物理主机的 /var/snap/anbox/common/rootfs-overlay/system/etc ,只需要将 resolv.conf 配制在这个目录下,然后重启anbox就可以具有配制。

注解

详细磁盘文件说明见 Android rootfs overlay

Android的DNS解析似乎不是使用 /etc/resolv.conf

比较奇怪,在虚拟机内部可以ping外网,但是DNS解析和直接http的IP地址访问显示不能访问目标地址。在WebView中显示报错:

net:ERR_ADDRESS_UNREACHABLE

检查Host物理机上 iptables -t nat -L 可以看到anbox-bridge已经添加了一个NAT:

MASQUERADE  all  --  192.168.250.0/24    !192.168.250.0/24     /* managed by anbox-bridge */

The anbox container can not connect to the network 有一个调试的建议

Internet Connection Sharing on CentOS 7 提示是由于网段原因导致防火墙阻碍。我想起来anbox是在设置过firewalld之后构建的,可能处于错误的zone被默认屏蔽了。

  • 检查zone:

    firewall-cmd --get-active-zones
    

显示输出:

libvirt
  interfaces: virbr0
public
  interfaces: br0 wlp3s0

并没有包含anbox0接口。

  • 将接口 anbox0 加入到内部zone:

    firewall-cmd --zone=internal --change-interface=anbox0
    

然后检查:

firewall-cmd --get-active-zones

输出显示:

internal
  interfaces: anbox0
libvirt
  interfaces: virbr0
public
  interfaces: br0 wlp3s0

此时anbox的浏览器还不能访问外部。

  • 检查public接口,就会发现没有启用masquerade - masquerade: no ,(奇怪,为何libvirt网络能访问外部)

    firewall-cmd --zone=public --list-all
    

显示输出:

public (active)
  target: default
  icmp-block-inversion: no
  interfaces: br0 wlp3s0
  sources:
  services: dhcpv6-client rdp ssh
  ports: 24800/tcp
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
  • 在public区域添加masquerade:

    firewall-cmd --zone=public --add-masquerade
    

That’s It…!!! We are done…!!!

现在可以访问外网了。

  • 将配制保存成永久:

    firewall-cmd --permanent --zone=internal --change-interface=anbox0
    firewall-cmd --permanent --zone=public --add-masquerade
    

安装Google Play

  • 安装Google Play:

    wget https://raw.githubusercontent.com/geeks-r-us/anbox-playstore-installer/master/install-playstore.sh
    chmod +x install-playstore.sh
    sudo ./install-playstore.sh
    

这样就可以直接访问Google Play安装应用程序,就和Android手机没有什么差异了。

安装应用程序

最好是从Google Play应用商店安装,不过有很多国内应用提供apk包,可以通过adb命令安装到虚拟机中:

adb install app.apk

安装完成后,在 Anbox Application Manager可以看到安装的应用程序。

微信

微信虽然能够安装,但是无法启动。参考 Although I installed wechat,but it can’t run #976 ,有建议改成 Anbox Software Rendering 来解决这个问题:

snap set anbox software-rendering.enable=true
snap restart anbox.container-manager

上述操作会停止所有运行的Anbox进程,必须要重启anbox。

如果要关闭software rendering,则:

snap set anbox software-rendering.enable=false
snap restart anbox.container-manager

注解

修改成software rendering没有解决微信运行问题。但是我发现snap的edge通道的anbox版本可以运行钉钉,比之前beta版本通道要好很多。所以目前还是推荐使用edge通道的软件包。

微信Linux版本

微信有一个Linux版本,基于Qt开发,可以方便在linux下实现基本的沟通。通过snap可以安装:

yay -S snapd snapd-xdg-open

然后安装:

snap install --devmode --channel edge wechart

运行:

wechat.electronic-wechat

注解

微信Linux版本支持托盘,就像原生程序一般,非常流畅。基本上和微信Mac版本一样简洁。

参考 How to install electronic-wechat on Arch Linux

参考