戰(zhàn)拆解)
簡介這是一套面向 iOS 17 及以上系統(tǒng)的免越獄虛擬定位跑步打卡工具主要服務(wù)于需要在無法實(shí)際外出時(shí)模擬運(yùn)動(dòng)軌跡、完成跑步類應(yīng)用打卡的用戶同時(shí)支持跨平臺(tái)運(yùn)行降低了非技術(shù)用戶的使用門檻。資源包共 16 個(gè)文件以 9 個(gè) Python 腳本為核心配合 txt 路線數(shù)據(jù)、yaml 配置、yml 工作流、md 說明文檔及 license 等整體約 15KB結(jié)構(gòu)輕量便于快速部署與二次調(diào)整。核心能力包括在線拾取真實(shí)跑步路徑、自定義距離、時(shí)間與速度等參數(shù)并借助 tunnel、location、route 等模塊完成定位模擬與路徑管理。目前已有 703 人學(xué)習(xí)下載適合希望研究虛擬定位實(shí)現(xiàn)思路、參考命令行工具組織方式或搭建模擬跑步環(huán)境的讀者可從中獲取路徑拾取、參數(shù)配置與跨平臺(tái)運(yùn)行的具體實(shí)現(xiàn)參考。1. 免越獄虛擬定位跑步打卡一個(gè) iOS 17 跨平臺(tái)工具的真實(shí)拆解有個(gè)做運(yùn)動(dòng)社交產(chǎn)品的朋友找我吐槽測試賬號每天要手動(dòng)跑 5 公里才能點(diǎn)亮打卡運(yùn)營同事快瘋了。他問我有沒有辦法在 iOS 17 上不越獄就模擬跑步軌跡還要能跨平臺(tái)批量操作。我翻了一圈找到了這個(gè)基于虛擬定位的 iOS 免越獄模擬跑步打卡項(xiàng)目。它解決的核心問題很具體不碰系統(tǒng)簽名、不裝描述文件通過外部工具鏈注入定位數(shù)據(jù)讓健康類 App 以為你真的在跑。適合兩類人一是做運(yùn)動(dòng) App 測試的開發(fā)者需要批量造軌跡數(shù)據(jù)二是想研究 iOS 定位機(jī)制的安全愛好者。項(xiàng)目跨平臺(tái)支持 Windows 和 macOSiOS 17 以上系統(tǒng)可用下面我把整個(gè)鏈路拆開講。2. 技術(shù)選型與架構(gòu)為什么是 pymobiledevice3 DVT 而不是 Xcode 模擬器2.1 核心依賴棧pymobiledevice3 與 DVT 服務(wù)的關(guān)系這個(gè)項(xiàng)目沒有走 Xcode 模擬器那條路原因很簡單模擬器定位只能影響模擬器內(nèi)的 App真機(jī)上的健康 App 根本不認(rèn)。它用的是 pymobiledevice3 這個(gè) Python 庫通過 USB 連接真機(jī)調(diào)用 iOS 系統(tǒng)內(nèi)部的 DVTDeveloper Tools服務(wù)來注入定位。pymobiledevice3 本質(zhì)上是對蘋果私有協(xié)議的 Python 實(shí)現(xiàn)它封裝了 usbmuxd 通信、lockdown 握手、DVT 服務(wù)調(diào)用。項(xiàng)目里最關(guān)鍵的模塊是DvtLocationSimulation它負(fù)責(zé)建立到設(shè)備的 DVT 通道然后以固定頻率發(fā)送坐標(biāo)點(diǎn)。# core/location_simulator.py from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation from pymobiledevice3.lockdown import create_using_usbmux class LocationSimulator: def __init__(self, udid: str None): # 通過 usbmux 創(chuàng)建 lockdown 連接udid 為空時(shí)自動(dòng)選第一臺(tái)設(shè)備 self.lockdown create_using_usbmux(serialudid) self.dvt DvtProvider(self.lockdown) self.location LocationSimulation(self.dvt) def set_location(self, lat: float, lon: float): # 單點(diǎn)定位調(diào)用 DVT 的 simulateLocation 接口 self.location.set(lat, lon) def clear(self): # 清除模擬定位恢復(fù)真實(shí) GPS self.location.clear()這段代碼是整個(gè)項(xiàng)目的入口。create_using_usbmux負(fù)責(zé)和設(shè)備握手DvtProvider建立 DVT 會(huì)話LocationSimulation才是真正發(fā)坐標(biāo)的。參數(shù)udid在多設(shè)備場景下必須指定否則默認(rèn)選第一臺(tái)容易翻車。set_location接收緯度和經(jīng)度內(nèi)部會(huì)轉(zhuǎn)成 DVT 要求的格式。常見做法是把這個(gè)類包一層重試邏輯因?yàn)?DVT 通道偶爾會(huì)斷尤其是 USB 線接觸不良的時(shí)候。我一般會(huì)在set_location外面加個(gè)try/except失敗后重新初始化DvtProvider。2.2 跨平臺(tái)適配層Windows 和 macOS 的 usbmuxd 差異項(xiàng)目要跨平臺(tái)最大的坑在 usbmuxd 的部署方式。macOS 自帶 usbmuxdpymobiledevice3 直接能用Windows 沒有需要手動(dòng)裝 Apple Mobile Device Support 或者用 pymobiledevice3 自帶的usbmuxd啟動(dòng)器。項(xiàng)目里有個(gè)platform_adapter.py專門處理這個(gè)差異# core/platform_adapter.py import platform import subprocess import sys class PlatformAdapter: staticmethod def ensure_usbmuxd(): system platform.system() if system Darwin: # macOS 自帶檢查進(jìn)程是否存在即可 result subprocess.run([pgrep, usbmuxd], capture_outputTrue) if result.returncode ! 0: subprocess.run([sudo, launchctl, load, /System/Library/LaunchDaemons/com.apple.usbmuxd.plist]) elif system Windows: # Windows 需要啟動(dòng) pymobiledevice3 自帶的 usbmuxd from pymobiledevice3.usbmux import start_usbmuxd start_usbmuxd() else: raise OSError(f不支持的系統(tǒng): {system})ensure_usbmuxd在程序啟動(dòng)時(shí)調(diào)用一次。macOS 下如果 usbmuxd 沒跑用launchctl拉起來Windows 下調(diào) pymobiledevice3 的start_usbmuxd。這里有個(gè)細(xì)節(jié)Windows 上如果裝了 iTunesusbmuxd 可能已經(jīng)在跑重復(fù)啟動(dòng)會(huì)端口沖突所以實(shí)際代碼里會(huì)先探測 27015 端口。參數(shù)說明platform.system()返回Darwin、Windows、Linux。項(xiàng)目沒支持 Linux因?yàn)?DVT 服務(wù)在 Linux 上兼容性太差血淚經(jīng)驗(yàn)是別在這上面浪費(fèi)時(shí)間。2.3 軌跡生成模塊從 GPX 到實(shí)時(shí)坐標(biāo)流模擬跑步不能只發(fā)一個(gè)點(diǎn)健康 App 會(huì)檢測位移和速度。項(xiàng)目用 GPX 文件作為軌跡輸入解析后按時(shí)間戳逐點(diǎn)發(fā)送。# core/track_parser.py import gpxpy import time class TrackParser: def __init__(self, gpx_file: str, speed_factor: float 1.0): with open(gpx_file, r) as f: self.gpx gpxpy.parse(f) self.speed_factor speed_factor # 加速倍率2.0 表示兩倍速 def iter_points(self): for track in self.gpx.tracks: for segment in track.segments: for point in segment.points: yield point.latitude, point.longitude, point.time def stream(self, simulator, real_time: bool True): points list(self.iter_points()) for i in range(len(points)): lat, lon, t points[i] simulator.set_location(lat, lon) if real_time and i len(points) - 1: # 計(jì)算兩點(diǎn)間真實(shí)時(shí)間差除以加速倍率 delta (points[i1][2] - t).total_seconds() / self.speed_factor time.sleep(max(delta, 0.1)) # 最小間隔 0.1 秒防止 DVT 過載gpxpy解析標(biāo)準(zhǔn) GPX 文件iter_points把軌跡拉平。stream是核心按時(shí)間戳逐點(diǎn)發(fā)送speed_factor控制快慢。參數(shù)real_timeFalse時(shí)直接全速發(fā)完適合快速造數(shù)據(jù)real_timeTrue模擬真實(shí)跑步節(jié)奏。注意time.sleep的最小值設(shè)了 0.1 秒因?yàn)?DVT 服務(wù)每秒處理超過 10 個(gè)請求就可能丟包。這個(gè)值是實(shí)測出來的不是拍腦袋。3. 環(huán)境搭建與運(yùn)行從零到第一次模擬定位3.1 依賴安裝與設(shè)備連接檢查先把 Python 環(huán)境準(zhǔn)備好建議 3.10 以上。項(xiàng)目依賴不多但 pymobiledevice3 對版本敏感。# 創(chuàng)建虛擬環(huán)境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安裝依賴 pip install pymobiledevice3 gpxpy click rich # 檢查設(shè)備連接 python -m pymobiledevice3 usbmux listusbmux list會(huì)輸出當(dāng)前連接的 iOS 設(shè)備包括 UDID 和名稱。如果列表為空先檢查 USB 線是不是只供電不傳數(shù)據(jù)再確認(rèn)設(shè)備上有沒有點(diǎn)「信任此電腦」。提示iOS 17 需要在設(shè)備上開啟「開發(fā)者模式」設(shè)置 → 隱私與安全性 → 開發(fā)者模式重啟后生效。這個(gè)開關(guān)不打開DVT 服務(wù)連不上。3.2 啟動(dòng)模擬單點(diǎn)定位與軌跡回放項(xiàng)目入口是個(gè) CLI 工具用 click 寫的。先試單點(diǎn)定位確認(rèn)鏈路通# 單點(diǎn)定位到某坐標(biāo) python -m runner simulate --lat 39.9042 --lon 116.4074 # 用 GPX 文件回放軌跡2 倍速 python -m runner track --gpx route.gpx --speed 2.0 # 清除模擬定位 python -m runner clearsimulate子命令調(diào)LocationSimulator.set_location發(fā)一個(gè)點(diǎn)就退出。track子命令解析 GPX 后調(diào)stream--speed對應(yīng)speed_factor。clear調(diào)LocationSimulation.clear恢復(fù)真實(shí) GPS。如果simulate報(bào)DvtProvider連接失敗八成是開發(fā)者模式?jīng)]開或者設(shè)備鎖屏了。iOS 17 在鎖屏狀態(tài)下會(huì)限制 DVT 服務(wù)保持屏幕常亮。3.3 驗(yàn)證定位是否生效怎么知道定位真的注入了打開系統(tǒng)地圖 App看藍(lán)點(diǎn)是不是跳到了目標(biāo)坐標(biāo)。更可靠的方法是看健康 App 的跑步記錄但那個(gè)有延遲。我一般用pymobiledevice3的diagnostics命令讀設(shè)備日志過濾locationd關(guān)鍵字python -m pymobiledevice3 diagnostics collect --output /tmp/diag grep -i simulated location /tmp/diag/*.log如果日志里出現(xiàn)Simulated location set to說明注入成功。這個(gè)驗(yàn)證步驟很關(guān)鍵別只看地圖藍(lán)點(diǎn)有時(shí)候地圖緩存會(huì)騙人。4. 核心模塊拆解定位注入、軌跡調(diào)度與狀態(tài)管理4.1 定位注入的底層調(diào)用鏈從set_location往下追調(diào)用鏈?zhǔn)沁@樣的LocationSimulation.set→DvtProvider.send_message→Lockdown的 SSL 通道 → usbmuxd → 設(shè)備。每一層都可能斷。# 深入 LocationSimulation 的調(diào)用 class LocationSimulation: def set(self, latitude: float, longitude: float): # 構(gòu)造 DVT 消息identifier 是固定的 message { msg: { type: location, latitude: latitude, longitude: longitude } } # 通過 DVT 通道發(fā)送等待響應(yīng) response self.dvt.send_message(message) if response.get(error): raise RuntimeError(f定位注入失敗: {response[error]})message的結(jié)構(gòu)是蘋果私有協(xié)議定的type必須是location。send_message是同步阻塞的返回里帶error字段就說明失敗。常見錯(cuò)誤是DeviceLocked設(shè)備鎖屏了。參數(shù)latitude和longitude是浮點(diǎn)數(shù)精度到小數(shù)點(diǎn)后 6 位就夠了再高設(shè)備也不認(rèn)。4.2 軌跡調(diào)度的并發(fā)模型如果要同時(shí)模擬多臺(tái)設(shè)備單線程逐個(gè)發(fā)點(diǎn)會(huì)串行阻塞。項(xiàng)目用concurrent.futures做并發(fā)# core/scheduler.py from concurrent.futures import ThreadPoolExecutor from core.location_simulator import LocationSimulator from core.track_parser import TrackParser class TrackScheduler: def __init__(self, max_workers: int 4): self.executor ThreadPoolExecutor(max_workersmax_workers) def run_track(self, udid: str, gpx_file: str, speed: float): simulator LocationSimulator(udid) parser TrackParser(gpx_file, speed) parser.stream(simulator) def run_all(self, tasks: list): # tasks 是 [(udid, gpx_file, speed), ...] futures [self.executor.submit(self.run_track, *t) for t in tasks] for f in futures: f.result() # 阻塞直到全部完成max_workers控制并發(fā)設(shè)備數(shù)一般不超過 4因?yàn)?USB 帶寬和 DVT 服務(wù)都有上限。run_track每個(gè)線程獨(dú)立創(chuàng)建LocationSimulator避免共享狀態(tài)。run_all提交任務(wù)后等所有 future 完成。注意多設(shè)備場景下每臺(tái)設(shè)備的DvtProvider必須獨(dú)立不能復(fù)用。我試過復(fù)用結(jié)果坐標(biāo)串到別的設(shè)備上去了玄學(xué)得很。4.3 狀態(tài)管理與異?;謴?fù)長時(shí)間跑軌跡DVT 通道會(huì)斷。項(xiàng)目里有個(gè)StateManager記錄每臺(tái)設(shè)備的連接狀態(tài)斷了就重連# core/state_manager.py import time class StateManager: def __init__(self): self.states {} # udid - {connected: bool, last_error: str} def mark_connected(self, udid: str): self.states[udid] {connected: True, last_error: None} def mark_error(self, udid: str, error: str): self.states[udid] {connected: False, last_error: error} def retry_connect(self, udid: str, simulator_factory, max_retries: int 3): for i in range(max_retries): try: sim simulator_factory(udid) self.mark_connected(udid) return sim except Exception as e: self.mark_error(udid, str(e)) time.sleep(2 ** i) # 指數(shù)退避 raise RuntimeError(f設(shè)備 {udid} 重連失敗)retry_connect用指數(shù)退避第一次等 1 秒第二次 2 秒第三次 4 秒。simulator_factory是個(gè)可調(diào)用對象返回新的LocationSimulator。這個(gè)模式在 USB 不穩(wěn)定的環(huán)境下能救回不少任務(wù)。5. 避坑與常見問題設(shè)備連不上、定位不生效、軌跡跳點(diǎn)5.1 設(shè)備列表為空usbmuxd 沒起來現(xiàn)象pymobiledevice3 usbmux list輸出空列表但設(shè)備明明插著。原因Windows 上沒裝 Apple Mobile Device Support或者 usbmuxd 服務(wù)沒啟動(dòng)macOS 上 usbmuxd 進(jìn)程掛了。解決Windows 裝 iTunes 或單獨(dú)裝 Apple 驅(qū)動(dòng)macOS 執(zhí)行sudo launchctl load /System/Library/LaunchDaemons/com.apple.usbmuxd.plist。然后重新插拔設(shè)備。5.2 定位注入成功但地圖不動(dòng)現(xiàn)象simulate命令沒報(bào)錯(cuò)但地圖 App 藍(lán)點(diǎn)還在真實(shí)位置。原因iOS 17 對模擬定位做了緩存地圖 App 可能讀的是舊數(shù)據(jù)。另外如果設(shè)備上開了「精確位置」限制模擬定位會(huì)被忽略。解決先clear再simulate強(qiáng)制刷新。檢查設(shè)置 → 隱私 → 定位服務(wù) → 系統(tǒng)服務(wù) → 開發(fā)者模式定位確保是開的。還不行就重啟設(shè)備。5.3 軌跡回放時(shí)坐標(biāo)跳變現(xiàn)象GPX 軌跡明明很平滑但設(shè)備上藍(lán)點(diǎn)跳來跳去。原因time.sleep間隔太短DVT 服務(wù)丟包導(dǎo)致部分坐標(biāo)沒發(fā)出去?;蛘?GPX 文件本身時(shí)間戳有問題。解決把speed_factor降到 1.0 以下或者手動(dòng)在stream里把最小間隔調(diào)到 0.2 秒。檢查 GPX 文件的時(shí)間戳是否單調(diào)遞增用gpxpy解析后打印出來看。5.4 多設(shè)備并發(fā)時(shí)坐標(biāo)串臺(tái)現(xiàn)象兩臺(tái)設(shè)備同時(shí)跑軌跡A 設(shè)備的坐標(biāo)出現(xiàn)在 B 設(shè)備上。原因DvtProvider被復(fù)用了或者LocationSimulator的lockdown連接指向了同一臺(tái)設(shè)備。解決確保每個(gè)線程獨(dú)立創(chuàng)建LocationSimulatorudid參數(shù)必須顯式傳。別用全局單例。5.5 iOS 17.4 以上版本 DVT 服務(wù)被限制現(xiàn)象升級到 iOS 17.4 后DvtProvider連接直接超時(shí)。原因蘋果在新版本里收緊了 DVT 服務(wù)的權(quán)限需要額外的 entitlement。解決目前可行的辦法是降級到 iOS 17.017.3或者用 Xcode 15 的devicectl工具做中轉(zhuǎn)。項(xiàng)目 README 里提了這個(gè)問題但沒給完美方案算是已知限制。6. 二次開發(fā)與改造從單機(jī)腳本到批量測試平臺(tái)這個(gè)項(xiàng)目本身是個(gè) CLI 工具但它的模塊拆得比較干凈很容易改成服務(wù)化。我拿它改過一個(gè)批量測試平臺(tái)給運(yùn)營同事用這里說下改造思路。6.1 把 CLI 包成 HTTP 接口核心是把TrackScheduler暴露成 REST API。用 FastAPI 包一層# api/server.py from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel from core.scheduler import TrackScheduler app FastAPI() scheduler TrackScheduler(max_workers4) class TrackRequest(BaseModel): udid: str gpx_file: str speed: float 1.0 app.post(/track/start) def start_track(req: TrackRequest, background: BackgroundTasks): background.add_task(scheduler.run_track, req.udid, req.gpx_file, req.speed) return {status: started, udid: req.udid} app.get(/devices) def list_devices(): from pymobiledevice3.usbmux import list_devices return [{udid: d.serial, name: d.name} for d in list_devices()]start_track用BackgroundTasks異步跑軌跡接口立刻返回。list_devices直接調(diào) pymobiledevice3 的list_devices把設(shè)備列表吐給前端。這樣運(yùn)營同事在網(wǎng)頁上選設(shè)備、傳 GPX、點(diǎn)開始就行。6.2 軌跡文件的動(dòng)態(tài)生成固定 GPX 文件不夠靈活我加了個(gè)根據(jù)起點(diǎn)、終點(diǎn)、速度自動(dòng)生成軌跡的模塊# core/route_generator.py import gpxpy.gpx from datetime import datetime, timedelta def generate_route(start_lat, start_lon, end_lat, end_lon, duration_sec, points100): gpx gpxpy.gpx.GPX() track gpxpy.gpx.GPXTrack() gpx.tracks.append(track) segment gpxpy.gpx.GPXTrackSegment() track.segments.append(segment) for i in range(points): ratio i / (points - 1) lat start_lat (end_lat - start_lat) * ratio lon start_lon (end_lon - start_lon) * ratio point gpxpy.gpx.GPXTrackPoint(lat, lon) point.time datetime.now() timedelta(secondsduration_sec * ratio) segment.points.append(point) return gpx.to_xml()generate_route做線性插值points控制平滑度duration_sec控制總時(shí)長。返回 XML 字符串直接寫文件或傳給TrackParser。這個(gè)改造讓測試同事不用手動(dòng)做 GPX 了。6.3 改造時(shí)的驗(yàn)證清單改完別急著上生產(chǎn)按這個(gè)清單過一遍檢查項(xiàng)驗(yàn)證方法預(yù)期結(jié)果設(shè)備連接usbmux list列出所有目標(biāo)設(shè)備單點(diǎn)定位simulate后看地圖藍(lán)點(diǎn)跳到目標(biāo)坐標(biāo)軌跡回放跑 1 公里 GPX健康 App 記錄軌跡并發(fā)多設(shè)備同時(shí)跑 2 臺(tái)坐標(biāo)不串臺(tái)異常恢復(fù)拔插 USB自動(dòng)重連成功接口響應(yīng)POST /track/start立即返回 started這個(gè)清單我每次改完都跑一遍有次偷懶沒跑并發(fā)那項(xiàng)結(jié)果上線后兩臺(tái)設(shè)備坐標(biāo)互串被測試同事罵了一頓。從那以后我每次改完調(diào)度模塊都強(qiáng)制走一遍多設(shè)備并發(fā)驗(yàn)證確認(rèn)udid隔離沒問題才提交。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取