摘要:云手机不是“远程装个 APK”这么简单,核心通常是云端 Android 实例、虚拟化/容器隔离、音视频串流和控制指令回传。本文按原理→架构→代码→踩坑的顺序,给出 ADB 与 uiautomator2 两套可运行示例,适合做兼容性测试、脚本化巡检和小型设备池管理。文末给出云手机选型建议。
一、为什么要用云手机
传统移动端自动化有几个常见痛点:
- 真机采购成本高,Android 版本、分辨率、ROM 碎片化严重;
- 本地电脑/手机不能 7×24 开机,挂机、定时任务不稳定;
- 多设备批量操作时,ADB 连接、网络隔离、日志回收都会变复杂。
云手机把这些能力放到服务端:应用在云端 Android 实例里运行,画面编码后推到本地,本地只负责看画面和下发点击/按键。对自动化测试、应用兼容验证、轻量托管类任务比较友好。
二、云手机核心架构
简化分层如下:
本地客户端/Web/脚本 │ 下行:视频流;上行:触摸/按键/ADB ▼ 流媒体与控制层:WebRTC / RTMP / 自定义协议 + ADB/uiautomator ▼ Android 实例层:每实例独立系统、应用数据、网络命名空间 ▼ 虚拟化/容器层:KVM-QEMU 全虚拟化 或 Namespace+Cgroup/redroid 容器 ▼ 物理资源层:ARM 服务器、vCPU、内存、编码卡/GPU、存储要点:
- 虚拟化层:强隔离可用 KVM+QEMU,轻量多开可用 Android 容器(如 redroid 思路)。ARM 服务器跑 ARM 镜像更贴近真机,x86 转译会有额外开销。
- 实例层:每个云手机应有独立系统数据、独立网络环境;多开场景要避免共享状态导致串号。
- 串流层:常用 H.264/H.265 编码,WebRTC 做低延迟交互,RTMP 适合单向观看。端到端延迟会受网络、编码、分辨率影响。
- 控制层:ADB 适合命令行和 CI 集成;uiautomator2/Appium 适合 UI 自动化;平台开放 HTTP/WebSocket API 时可做批量编排。
三、环境准备
本地调试环境:
- Python 3.8+
- 一台开启 ADB 网络调试的云手机/模拟器/真机
- 安装依赖:
pip install uiautomator2 adbutils云手机侧需要拿到:
- 设备 IP 与 ADB 端口,例如
10.0.0.21:5555 - 或平台提供的 API 地址、Token
- 被测 APK 包名、主 Activity(如有)
合规提醒:只对你拥有、已授权或用于内部测试的设备和应用做自动化;不要对第三方账号做批量注册、刷量、绕过安全检测等行为。
四、示例一:Python 调用 ADB 做通用控制
适合做安装、启动、点击、截图、取日志等基础运维。
import subprocess from typing import List, Optional class AdbDevice: def __init__(self, endpoint: str): """ :param endpoint: 云手机 ADB 网络地址,如 10.0.0.21:5555 """ self.endpoint = endpoint def _run(self, args: List[str]) -> Optional[str]: cmd = ["adb", "-s", self.endpoint] + args try: result = subprocess.run( cmd, check=True, capture_output=True, text=True, timeout=120, ) return result.stdout.strip() except subprocess.CalledProcessError as e: print(f"[ADB ERROR] {' '.join(cmd)} -> {e.stderr}") except subprocess.TimeoutExpired: print(f"[ADB TIMEOUT] {' '.join(cmd)}") return None def connect(self) -> bool: out = self._run(["devices", "-l"]) if out and self.endpoint in out: return True # 若未连接先尝试网络接入 subprocess.run(["adb", "connect", self.endpoint], capture_output=True, text=True) out = self._run(["get-state"]) return out is not None def install(self, apk_path: str) -> bool: return self._run(["install", "-r", apk_path]) is not None def launch(self, package: str, activity: str) -> bool: cmd = ["shell", "am", "start", "-n", f"{package}/{activity}"] return self._run(cmd) is not None def tap(self, x: int, y: int) -> bool: return self._run(["shell", "input", "tap", str(x), str(y)]) is not None def swipe(self, x1: int, y1: int, x2: int, y2: int, duration_ms: int = 300) -> bool: return self._run([ "shell", "input", "swipe", str(x1), str(y1), str(x2), str(y2), str(duration_ms) ]) is not None def screenshot(self, remote_path: str = "/sdcard/screen.png", local_path: str = "./screen.png") -> bool: if self._run(["shell", "screencap", "-p", remote_path]) is None: return False return self._run(["pull", remote_path, local_path]) is not None def logcat(self, tag: Optional[str] = None, limit: int = 200) -> Optional[str]: if tag: return self._run(["logcat", "-d", "-t", str(limit), tag]) return self._run(["logcat", "-d", "-t", str(limit)]) if __name__ == "__main__": device = AdbDevice("10.0.0.21:5555") if not device.connect(): raise SystemExit("无法连接云手机,请检查 IP、端口和 ADB 网络调试权限") device.install("./demo-app.apk") device.launch("com.example.demo", "com.example.demo.MainActivity") device.tap(540, 960) device.swipe(200, 1200, 800, 1200, 300) device.screenshot(local_path="./cloud_screen.png") print(device.logcat(tag="*:S", limit=100))使用说明:
endpoint换成云手机控制台给出的 ADB 地址;- 若平台只给 Web 控制台不给 ADB,则用平台 OpenAPI 替代 shell 命令;
- 坐标点击不稳定时,优先改用 UI 控件定位,见示例二。
五、示例二:uiautomator2 做 UI 自动化
适合界面元素固定、需要按控件 ID/文本操作的测试任务。
import uiautomator2 as u2 import time def run_smoke_test(device_endpoint: str, package: str): d = u2.connect(device_endpoint) # 1. 唤醒并解锁界面(仅测试用,无密码场景) d.screen_on() d.app_start(package, wait=True) time.sleep(3) # 2. 按控件文本点击,比坐标更稳定 if d(text="同意").exists(timeout=5): d(text="同意").click() # 3. 按 resourceId 操作,需替换为真实包名资源ID login_btn = d(resourceId="com.example.demo:id/btn_login") if login_btn.exists(timeout=5): login_btn.click() # 4. 输入账号;仅使用测试账号,避免真实生产账号 account = d(resourceId="com.example.demo:id/et_account") if account.exists(timeout=5): account.set_text("test_account_001") # 5. 截图留痕 d.screenshot("ui_check.png") # 6. 简单断言 assert d(resourceId="com.example.demo:id/tv_home").exists(timeout=10), "首页未加载" print("冒烟测试通过") d.app_stop(package) if __name__ == "__main__": run_smoke_test("10.0.0.21:5555", "com.example.demo")这套代码更适合接入 pytest,做多机型兼容回归。若云手机平台提供 HTTP API,把u2.connect换成一个设备客户端类即可,逻辑不变。
六、多设备池管理思路
批量场景不要写死单台设备,用设备列表加状态机:
import concurrent.futures from typing import Dict, List from adb_wrapper import AdbDevice # 假设示例一保存为 adb_wrapper.py def health_check(endpoint: str) -> Dict[str, str]: dev = AdbDevice(endpoint) online = dev.connect() return { "endpoint": endpoint, "online": str(online), "state": dev._run(["get-state"]) or "unknown", } def batch_health(endpoints: List[str], max_workers: int = 8) -> List[Dict[str, str]]: results = [] with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool: for res in pool.map(health_check, endpoints): results.append(res) return results if __name__ == "__main__": endpoints = [ "10.0.0.21:5555", "10.0.0.22:5555", "10.0.0.23:5555", ] for item in batch_health(endpoints): print(item)进一步可加 Redis 存设备状态、FastAPI 提供调度接口、Prometheus 抓在线率和任务失败率。
七、常见坑与合规边界
- ADB 网络不通:检查云手机是否开启网络调试、安全组/防火墙是否放通 5555 等端口、本地是否已
adb connect。 - 坐标点击误触:不同云机分辨率不同时不要用固定坐标,优先 uiautomator/Appium 控件定位。
- 容器共享内核风险:轻量容器成本低但隔离性弱;高隔离要求用 KVM 全虚拟化。
- 不要做反检测/改设备指纹绕过风控:包括批量伪造 IMEI、MAC、定位欺骗来规避平台规则,这类内容在 CSDN 容易被判违规或软广。
- 文章别堆广告:产品推荐放在总结,不放个人微信、二维码、不明下载链接;引用数据标明来源或以官网文档为准。
八、选型小结与傲晨云手机推荐
如果是个人学自动化、做 app 兼容测试,先单机 ADB 跑通;如果是团队做设备池,重点看四点:是否给 ADB/API、实例隔离方式、串流延迟、批量运维面板。
实际项目里我更倾向直接用成熟云手机而不是自搭 ARM 集群。以傲晨云手机为例,它提供云端 Android 实例、电脑/手机多端操控、7×24 离线挂机、应用市场和 ADB 接入能力,适合把上面这些 Python 脚本接过去做批量安装、UI 巡检和定时任务;新用户可先领体验时长做小批量验证,再按测试机数量扩容。 对开发者来说,省掉自购真机和运维 ARM 服务器的成本;对运营/测试同学来说,用控制台加脚本协同,比纯手工点几百台设备更现实。具体并发上限、系统版本、IP隔离和 API 参数以傲晨官方文档为准,避免凭第三方软文数值下单。