苹果手机群控系统必不可少的几个源代码分享!

0 阅读1分钟

做苹果手机群控这块快三年了,从最早拿真机一台台手动点,到后来用脚本批量跑,踩过的坑能写半本书,很多新手一上来就问"有没有现成的群控源码",其实核心模块就那几个,搞懂了自己拼一套出来不难。

今天把项目里最常用的几段核心代码拆出来分享,都是Python写的,基于tidevice和Facebook的WebDriverAgent,直接能跑。

一、设备自动发现与批量连接模块

群控第一步是识别线上有哪些设备,Windows上靠iTunes的驱动,macOS和Linux直接用libimobiledevice,下面这段代码启动一个监听服务,自动扫描USB和局域网内的iOS设备,上线就自动注册到设备池:

# device_pool.py
import threading
import time
from tidevice import Usbmux, Device
from tidevice._usbmux import MuxType

class DevicePool:
    def __init__(self):
        self.devices = {}
        self.lock = threading.Lock()
        self.running = False

    def _on_device_added(self, udid):
        with self.lock:
            if udid not in self.devices:
                try:
                dev = Device(udid)
                info = dev.disk_image_image_control()
                self.devices[udid] = {
                    "udid": udid,
                    "name": dev.name,
                    "version": dev.version,
                    "status": "online",
                    "last_heartbeat": time.time()
                }
                print(f"[上线] {dev.name} ({udid[:8]}...) iOS {dev.version}")
                except Exception as e:
                    print(f"[连接失败] {udid}: {e}")

    def _on_device_removed(self, udid):
        with self.lock:
            if udid in self.devices:
                name = self.devices[udid]["name"]
                del self.devices[udid]
                print(f"[下线] {name}")

    def start_watch(self):
        self.running = True
        mux = Usbmux()
        for udid in mux.device_iter():
            self._on_device_added(udid)
        # 局域网WiFi设备额外扫描
        self._scan_network_devices()
        threading.Thread(target=self._heartbeat_check, daemon=True).start()

    def _scan_network_devices(self):
        import subprocess
        result = subprocess.run(
            ["idevice_id", "-n"], capture_output=True, text=True
        )
        for udid in result.stdout.strip().split("\n"):
            if udid:
                self._on_device_added(udid)

    def _heartbeat_check(self):
        while self.running:
            time.sleep(10)
            with self.lock:
                for udid in list(self.devices.keys()):
                    try:
                        d = Device(udid)
                        _ = d.name
                        self.devices[udid]["last_heartbeat"] = time.time()
                    except:
                        self._on_device_removed(udid)

    def get_online_devices(self):
        with self.lock:
            return list(self.devices.values())

if __name__ == "__main__":
    pool = DevicePool()
    pool.start_watch()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pool.running = False

这段是整个系统的地基,设备池维护一个全局字典,每台手机上线自动注册、掉线自动剔除,后面所有操作都从这个池子里取设备,实际项目里我会把它挂在Redis里,这样多台控制主机也能共享设备列表。

二、批量屏幕操作与手势模拟

设备连上之后,核心需求就是批量点击、滑动、输入,WDA(WebDriverAgent)是苹果官方允许的自动化框架,群控基本都靠它,下面这段封装了常用手势,支持单设备执行,也支持多设备广播:

# device_controller.py
import time
import threading
from tidevice import Device
from wda import Client as WDAClient

class DeviceController:
    def __init__(self, udid):
        self.udid = udid
        self.device = Device(udid)
        self.wda = None

    def launch_wda(self, wda_bundle="com.facebook.WebDriverAgentRunner.xctrunner"):
        """启动WDA服务,首次需要手机上信任证书"""
        self.device.app_install(wda_bundle)  # 已装过则跳过
        self.device.app_launch(wda_bundle, wait=True)
        # 等待WDA端口就绪
        for _ in range(30):
            try:
                self.wda = WDAClient("http://localhost:8100")
                self.wda.healthcheck()
                break
            except:
                time.sleep(1)
        if not self.wda:
            raise RuntimeError("WDA启动超时")

    def tap(self, x, y):
        """屏幕点击,坐标基于逻辑分辨率"""
        self.wda.tap(x, y)

    def swipe(self, x1, y1, x2, y2, duration=0.3):
        self.wda.swipe(x1, y1, x2, y2, duration=duration)

    def input_text(self, text):
        self.wda.keyboard.frame()
        self.wda.send_keys(text)

    def launch_app(self, bundle_id):
        self.wda.app_activate(bundle_id)

    def screenshot(self, save_path=None):
        png = self.wda.screenshot()
        if save_path:
            with open(save_path, "wb") as f:
                f.write(png)
        return png

class BroadcastController:
    """多设备广播控制器,把一个动作同步发给所有在线设备"""
    def __init__(self, device_controllers):
        self.controllers = device_controllers

    def _run_on_all(self, method_name, *args, **kwargs):
        threads = []
        results = [None] * len(self.controllers)
        def worker(idx, ctrl):
            try:
                method = getattr(ctrl, method_name)
                results[idx] = method(*args, **kwargs)
            except Exception as e:
                results[idx] = e
        for i, ctrl in enumerate(self.controllers):
            t = threading.Thread(target=worker, args=(i, ctrl))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()
        return results

    def broadcast_tap(self, x, y):
        return self._run_on_all("tap", x, y)

    def broadcast_swipe_up(self):
        return self._run_on_all("swipe", 200, 600, 200, 200, 0.3)

    def broadcast_input(self, text):
        return self._run_on_all("input_text", text)

手势广播这里用了多线程并发,每台手机一个线程,否则串行执行一百台手机光点一下就得等半天,实际生产环境里我还加了失败重试和延迟随机化,不然所有手机同一毫秒点同一个位置,平台风控一眼就看出来了。

三、应用批量安装与启动控制

做群控经常需要批量装App、批量启动某个应用做任务,下面这段处理ipa包的批量安装,以及按机型分组执行不同任务:

# app_manager.py
import os
import threading
from tidevice import Device

class AppManager:
    def __init__(self, device_pool):
        self.pool = device_pool

    def install_ipa_on_all(self, ipa_path):
        """把指定ipa批量装到所有在线设备"""
        if not os.path.exists(ipa_path):
            raise FileNotFoundError(ipa_path)
        devices = self.pool.get_online_devices()
        results = {}
        def install_one(udid, name):
            try:
                d = Device(udid)
                d.app_install(ipa_path)
                results[udid] = (name, True, "安装成功")
            except Exception as e:
                results[udid] = (name, False, str(e))
        threads = []
        for dev in devices:
            t = threading.Thread(
                target=install_one, args=(dev["udid"], dev["name"])
            )
            threads.append(t)
            t.start()
        for t in threads:
            t.join()
        # 输出汇总
        success = sum(1 for v in results.values() if v[1])
        print(f"批量安装完成:{success}/{len(devices)} 成功")
        return results

    def launch_app_grouped(self, bundle_map):
        """按分组启动不同App,bundle_map: {udid: bundle_id}"""
        threads = []
        def launch(udid, bundle_id):
            try:
                d = Device(udid)
                d.app_launch(bundle_id)
            except Exception as e:
                print(f"{udid} 启动 {bundle_id} 失败: {e}")
        for udid, bid in bundle_map.items():
            t = threading.Thread(target=launch, args=(udid, bid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def get_app_list(self, udid):
        """获取某台设备已安装的用户App列表"""
        d = Device(udid)
        apps = d.installation.iter_user_apps()
        return [
            {"bundle_id": a["BundleIdentifier"],
             "name": a.get("CFBundleDisplayName", ""),
             "version": a.get("CFBundleShortVersionString", "")}
            for a in apps
        ]

四、自动化任务调度引擎

前面三个模块是工具,真正让群控"跑起来"的是任务调度,下面这个简化版任务引擎支持给不同设备分配不同脚本,带随机延迟和失败上报:

# task_engine.py
import time
import random
import threading
import traceback
from datetime import datetime

class TaskEngine:
    def __init__(self, controllers, max_workers=20):
        self.controllers = controllers  # {udid: DeviceController}
        self.max_workers = max_workers
        self.running_tasks = {}

    def run_task_on_device(self, udid, task_script, jitter=2.0):
        """在单台设备上执行任务脚本,task_script是一个接收controller的函数"""
        ctrl = self.controllers.get(udid)
        if not ctrl:
            return {"udid": udid, "status": "offline"}
        try:
            # 随机延迟,避免所有设备同时动作
            time.sleep(random.uniform(0.1, jitter))
            task_script(ctrl)
            return {"udid": udid, "status": "success",
                    "time": datetime.now().isoformat()}
        except Exception as e:
            return {"udid": udid, "status": "failed",
                    "error": str(e),
                    "trace": traceback.format_exc()}

    def broadcast_task(self, task_script, device_udids=None):
        """把同一个任务广播到所有或指定设备"""
        udids = device_udids or list(self.controllers.keys())
        results = []
        threads = []
        def worker(udid):
            res = self.run_task_on_device(udid, task_script)
            results.append(res)
        for udid in udids:
            t = threading.Thread(target=worker, args=(udid,))
            threads.append(t)
            t.start()
            # 控制并发数
            if len(threads) >= self.max_workers:
                for t in threads:
                    t.join()
                threads.clear()
        for t in threads:
            t.join()
        ok = sum(1 for r in results if r["status"] == "success")
        print(f"任务完成:{ok}/{len(results)} 成功")
        return results

# 示例:在所有设备上打开抖音并滑动三次
def douyin_watch_task(ctrl):
    ctrl.launch_app("com.ss.iphone.ugc.Aweme")
    time.sleep(random.uniform(5, 8))
    for i in range(3):
        ctrl.swipe(200, 600, 200, 200, duration=random.uniform(0.3, 0.6))
        time.sleep(random.uniform(8, 15))

if __name__ == "__main__":
    # engine = TaskEngine(controllers_dict)
    # engine.broadcast_task(douyin_watch_task)
    pass

五、写在最后

这四段代码拼起来已经是一个可用的苹果群控雏形了:设备发现→手势控制→应用管理→任务调度,实际商用系统还需要加Web管理后台、WebSocket实时投屏、风控伪装(改IMEI/序列号/环境参数)、以及任务结果回传这些模块,但核心逻辑就是上面这些。

几个容易踩的坑提前说:第一,WDA的证书7天过期,越狱设备可以用企业证书,非越狱设备得每周重新签名;第二,USB一拖多的时候带宽是瓶颈,超过20台建议分HUB,每个HUB独立供电;第三,坐标适配要按机型分辨率单独算,别所有手机用同一套坐标。

源码我已经尽量精简了,每段都能独立跑通,有需要扩展的地方自己加就行。