
简介面向 iOS 17 及以上系统、希望在不越狱设备上研究虚拟定位与模拟跑步打卡的开发者这份跨平台命令行工具给出了一套可参考的实现思路。它围绕在线拾取路径、跑步参数设置与设备连接等环节组织代码将定位、路由、隧道、运行控制等职责拆分到不同模块适合具备一定 Python 基础、想了解 iOS 虚拟定位工具结构与跨平台 CLI 设计的读者研读。压缩包共 16 个文件以 9 个 Python 脚本为核心辅以 yaml/yml 配置、txt 路径数据、依赖清单、README 说明与许可证等整体仅约 15KB轻量且目录划分清晰。目前已有 716 人学习。读者可从中获取在线路径配置、参数调节、模块解耦与依赖组织等方面的参考并借说明文档理解项目边界同时需注意此类技术可能违反运动应用条款宜用于学习与合规研究不应替代真实锻炼。1. iOS 17 之后还能免越狱改定位吗开发者模式下的位置模拟到底怎么跑做跑步类 App 的人大概都遇到过一个尴尬轨迹纠偏、配速曲线、分段计时这几块逻辑要反复验证真去操场跑十公里显然不现实。于是 iOS 17 之后的免越狱虚拟定位方案就成了刚需——不越狱、不改系统、不装描述文件靠苹果自己留给开发者的调试通道把坐标塞进locationd再用 GPX 回放出模拟跑步打卡的整条轨迹。麻烦在于 iOS 17 把这条通道从老的 usbmux 明文服务换成了 RemoteXPC 隧道很多团队手里那套基于 libimobiledevice 的idevicesetlocation直接哑火这才是真正卡人的地方。这套跨平台方案解决的正是这个断层Windows、macOS、Linux 三端都能拉起隧道单点定位、GPX 轨迹回放、清除复位全覆盖设备重启即恢复真实定位。适合做定位功能自测的开发、需要复现特定路线的测试以及要给业务方演示轨迹效果的工程师。有一点得先摆明白它只作用于 GPS 层加速度计、陀螺仪、步频这些传感器数据它一概不碰需要真实运动数据的正式场景请守住平台规则别拿它当长期方案。2. 打通隧道三端环境搭建与 DVT 服务的握手过程2.1 为什么必须走 DVT而不是 WDA 或描述文件先把这个选择的理由说清楚不然后面报错会一头雾水。iOS 上改定位的路径其实有三条装带定位模拟功能的描述文件需要签名、有有效期、重启失效、用 WebDriverAgent 这类自动化框架加壳依赖签名的 runneriOS 17 上签名成本很高、以及苹果给 Xcode 用的 DVTDeveloper Tools服务里的 LocationSimulation 接口。第三条是官方调试链路不涉及任何 App 层注入设备端只认开发者调试会话所以既不用越狱也不会在设备上留下常驻组件。DVT 的问题在于它的传输层。iOS 16 及以前DVT 服务挂在 usbmux 的明文端口上用idevicesetlocation一条命令就能搞定。iOS 17 开始苹果把开发者服务迁移到 RemoteXPC需要先建立一条基于虚拟网卡的隧道拿到 RSDRemote Service Discovery地址和端口之后所有 DVT 调用都要带上这对地址。这就是为什么你照着两年前的教程敲命令会卡在Could not connect to device或者一直转圈。提示隧道是每台设备、每次重连都会变的RSD 端口不是固定值别把它写死在脚本里。2.2 三端安装与配对流程核心工具是pymobiledevice3一个纯 Python 实现的设备通信库把 usbmux、lockdown、DVT 这些底层协议都封好了也是目前少数跟进了 RemoteXPC 的开源实现。安装它之前先确认 Python 版本在 3.8 以上我一般用 3.113.12 在某些依赖上还会碰到编译问题。# 建议放在独立虚拟环境里避免和系统 Python 的依赖打架 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install --upgrade pip pip install pymobiledevice3[usbmuxd]装完之后三端各有一段前置工作这部分最容易翻车平台前置依赖常见缺失表现WindowsApple 移动设备支持服务一般随 iTunes 或 Apple Devices 装上、wintun.dll放到 pymobiledevice3 可执行目录隧道启动报WinTun not foundmacOS系统自带 usbmuxd无需额外安装隧道需 sudo普通权限下 RSD 端口拿不到Linuxusbmuxd包、libimobiledevice工具链、当前用户加入plugdev组设备列表空lsusb能看到但工具识别不到配对这一步在 iOS 16 之后多了一个动作设备要在「设置 → 隐私与安全性 → 开发者模式」里打开开关然后重启一次。很多人以为这个开关只对 Xcode 有用实际上它决定了调试类服务能不能被拉起不开的话后面隧道能建、DVT 调用会被直接拒绝。设备连上后先验证识别# 列出当前所有已连接设备拿到 UDID 和连接类型 pymobiledevice3 usbmux list输出里会给出Identifier也就是 UDID、DeviceName和ConnectionTypeUSB 或 Network。如果这里是空的先别往下走问题一定在驱动或配对信任不在隧道。2.3 建立 RSD 隧道并验证握手iOS 17 必须单独起一个隧道进程它会创建一张虚拟网卡并返回 RSD 地址端口。这个进程要占住终端所以常规做法是后台运行并把日志重定向到文件。# 需要管理员/root 权限因为它要创建虚拟网卡 sudo pymobiledevice3 remote tunneld --daemon --log-level INFO # 另开一个终端查看隧道是否建起来 pymobiledevice3 remote tunneld --status拿到 RSD 之后用一条最轻的命令验证开发者服务是否活着# 用 RSD 地址和端口访问设备确认 DVT 服务可枚举 pymobiledevice3 developer dvt simulate-location clear --rsd 127.0.0.1 51234clear是最安全的试探命令它只是把定位恢复到系统默认。如果这条命令返回成功说明整条链路全通了后面设坐标、放轨迹都只是换参数的事。反过来如果这里报Service not found八成是开发者模式没开报连接超时是 RSD 端口取错了或者隧道进程已经退出。3. 坐标注入的三种粒度单点、GPX 与程序化插值3.1 单点设置最快的连通性验证单点定位适合两类场景一是确认链路是否真的生效二是某些业务只需要一个固定位置比如验证「附近门店」的排序逻辑。命令极其简单经纬度按 WGS-84 传小数点后至少六位少一位误差就是百米级别。# 参数顺序是 纬度 经度和国内地图常用的「经度在前」相反这里栽过很多人 pymobiledevice3 developer dvt simulate-location set --rsd 127.0.0.1 51234 -- 31.230416 121.473701执行完不要只盯着命令行看返回码去设备上打开系统地图蓝点会平滑滑到目标位置带一个脉冲圈。设备端不会弹任何提示也不会留下开关这也是它比描述文件方案干净的地方。想复位直接再执行一次clear子命令或者拔掉数据线重启定位服务。参数上有一个细节值得单独说--rsd后面的地址端口组合必须和tunneld输出的完全一致包括 IPv6 地址里的方括号。有些版本的输出是fdXX:...:1这种脚本里做字符串拼接时容易把方括号吃掉导致连接直接失败。我一般让脚本从--status的 JSON 输出里解析而不是人工抄。3.2 GPX 回放把配速写进轨迹文件GPX 是 GPS 交换的标准格式苹果的模拟器从 Xcode 时代就支持它DVT 接口自然也认。它的价值在于「时间维度」每个trkpt可以带一个time时间戳播放时设备端会按时间戳推进而不是瞬间跳过去。这就是模拟跑步打卡和单纯改坐标的本质差别。?xml version1.0 encodingUTF-8? gpx version1.1 creatorroute-builder xmlnshttp://www.topografix.com/GPX/1/1 trk namemorning-run/name trkseg !-- 每个点带时间戳设备端按时间差推进配速由相邻点距离/时间差决定 -- trkpt lat31.230416 lon121.473701ele8.0/eletime2024-05-01T07:00:00Z/time/trkpt trkpt lat31.230916 lon121.473701ele8.0/eletime2024-05-01T07:00:18Z/time/trkpt trkpt lat31.231416 lon121.473701ele8.2/eletime2024-05-01T07:00:36Z/time/trkpt trkpt lat31.231916 lon121.473701ele8.4/eletime2024-05-01T07:00:54Z/time/trkpt /trkseg /trk /gpx播放命令# play 会异步推进命令本身很快返回轨迹在设备端按时间轴跑完 pymobiledevice3 developer dvt simulate-location play --rsd 127.0.0.1 51234 ./morning-run.gpx这段 XML 里有几个点必须对上。time用的是 UTC末尾那个Z不能省写成带时区偏移的08:00在部分版本上解析会失败。上面四个点纬度每步 0.0005 度约 55.6 米间隔 18 秒换算下来配速大概是 5 分 24 秒每公里属于正常慢跑区间业务侧的配速校验不会报警。还有一个容易忽略的地方GPX 点太密或者太稀疏都会出问题。一秒一个点轨迹平滑但文件巨大三十秒一个点业务侧算出来的瞬时速度会跳变某些带异常检测的 App 会判为「非人类移动」。我的经验值是 3 到 5 秒一个点跑十公里也就两三千个点文件几百 KB。3.3 程序化插值用 Python 生成任意路线手工编 GPX 不现实实际项目里都是拿起点终点或者一条折线按目标配速反推时间戳。这里给出一个能直接用的生成器核心是 Haversine 距离加上分段等距插值。import math from datetime import datetime, timedelta, timezone R 6371000.0 # 地球平均半径米 def haversine(lat1, lon1, lat2, lon2): 两点球面距离返回米。业务上足够不需要上椭球模型。 p1, p2 math.radians(lat1), math.radians(lat2) dp math.radians(lat2 - lat1) dl math.radians(lon2 - lon1) a math.sin(dp / 2) ** 2 math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * R * math.asin(math.sqrt(a)) def interpolate(points, speed_mps, start_time, interval_s4): points: [(lat, lon, ele), ...] 折线控制点 speed_mps: 目标速度米/秒。360 秒/公里 对应 2.778 interval_s: 采样间隔秒。3~5 之间比较自然 step speed_mps * interval_s # 每个采样点的推进距离 out, t [], start_time for i in range(len(points) - 1): la1, lo1, e1 points[i] la2, lo2, e2 points[i 1] seg haversine(la1, lo1, la2, lo2) n max(1, int(seg // step)) # 这一段要切几刀 for k in range(n): r k / n out.append((la1 (la2 - la1) * r, lo1 (lo2 - lo1) * r, e1 (e2 - e1) * r, t)) t timedelta(secondsinterval_s) out.append((*points[-1], t)) return out def write_gpx(pts, path): head (?xml version1.0 encodingUTF-8?\n gpx version1.1 creatorroute-builder xmlnshttp://www.topografix.com/GPX/1/1trktrkseg\n) with open(path, w, encodingutf-8) as f: f.write(head) for lat, lon, ele, t in pts: # 统一转 UTC 并以 Z 结尾避免时区解析差异 ts t.astimezone(timezone.utc).strftime(%Y-%m-%dT%H:%M:%SZ) f.write(ftrkpt lat{lat:.6f} lon{lon:.6f} fele{ele:.1f}/eletime{ts}/time/trkpt\n) f.write(/trkseg/trk/gpx\n) if __name__ __main__: route [(31.230416, 121.473701, 8.0), (31.235416, 121.473701, 9.5)] generated interpolate(route, speed_mps2.778, start_timedatetime.now(timezone.utc)) write_gpx(generated, run_1km.gpx) print(fpoints{len(generated)})逻辑上有三点值得说明。haversine用球面近似在城市尺度上误差在千分之几对配速计算完全够用上 Vincenty 属于过度设计。interpolate里n seg // step是向下取整意味着每段末尾会有一个略小于step的余量累积长了会让配速略微偏快想更精确可以改成按总长统一分配。时间戳用datetime.now(timezone.utc)起算而不是硬编码日期是因为某些业务会校验轨迹时间和当前时间的时间差写死一个去年的日期会直接触发「历史数据」判定。参数上speed_mps是最关键的一个。跑步配速换算5 分 00 秒每公里是 3.3336 分 00 秒是 2.7787 分 30 秒是 2.222。走路大概 1.2 到 1.4骑车 5 以上。填错这一项业务侧的配速曲线会立刻暴露。4. 封装成工具把命令行包成可复用的跨平台脚本4.1 为什么用 subprocess 包 CLI 而不是直接调 Python APIpymobiledevice3有 Python API但版本间改动比较大LocationSimulation的构造签名和 DVT provider 的获取方式在 3.x 到 4.x 之间都调整过锁版本又会让新设备支持滞后。所以我一般用subprocess调它的命令行——CLI 的参数是相对稳定的契约出问题也容易在终端里复现。参数设计上一个够用的封装只需要这几项参数含义默认值说明--rsd-host/--rsd-port隧道地址端口自动探测手工指定用于排障--udid目标设备首台多设备时必须指定--modeset/play/clearplay单点、回放、复位--gpx轨迹文件路径无play模式必填--timeout回放超时秒数轨迹时长 60防止子进程挂死4.2 一个能跑的实现import argparse import json import subprocess import sys import time def get_rsd(): 从 tunneld --status 的 JSON 输出里解析 RSD 地址端口避免手工抄错。 raw subprocess.run([pymobiledevice3, remote, tunneld, --status], capture_outputTrue, textTrue, timeout15).stdout try: info json.loads(raw) except json.JSONDecodeError: sys.exit(tunneld 未运行或输出格式异常先执行 sudo pymobiledevice3 remote tunneld --daemon) if not info: sys.exit(隧道列表为空设备可能已断开) first info[0] if isinstance(info, list) else next(iter(info.values())) # 不同版本字段名可能是 rsd_address/address做个兼容 host first.get(rsd_address) or first.get(address) port str(first.get(rsd_port) or first.get(port)) return host, port def run(cmd, timeout): 统一入口打印完整命令便于复现非零退出码直接抛出。 print([cmd], .join(cmd)) p subprocess.run(cmd, capture_outputTrue, textTrue, timeouttimeout) if p.returncode ! 0: print(p.stdout) print(p.stderr, filesys.stderr) raise RuntimeError(fexit{p.returncode}) return p.stdout def main(): ap argparse.ArgumentParser() ap.add_argument(--mode, choices[set, play, clear], defaultplay) ap.add_argument(--gpx) ap.add_argument(--lat, typefloat) ap.add_argument(--lon, typefloat) ap.add_argument(--timeout, typeint, default3600) args ap.parse_args() host, port get_rsd() base [pymobiledevice3, developer, dvt, simulate-location] if args.mode clear: run(base [clear, --rsd, host, port], 30) elif args.mode set: if args.lat is None or args.lon is None: sys.exit(set 模式需要 --lat 与 --lon) run(base [set, --rsd, host, port, --, f{args.lat:.6f}, f{args.lon:.6f}], 30) else: if not args.gpx: sys.exit(play 模式需要 --gpx) # 回放是异步的这里给足超时结束后主动 clear 归位 run(base [play, --rsd, host, port, args.gpx], args.timeout) time.sleep(1) run(base [clear, --rsd, host, port], 30) if __name__ __main__: main()这段代码的关键设计在于两处。get_rsd()从状态接口解析而不是让用户手填直接消掉了「端口抄错」这一大类问题末尾的自动clear是血泪教训换来的——回放结束后如果不清除设备会一直停在轨迹终点下次连上做别的测试时位置是错的排查半天才发现是上一次没复位。timeout参数别设太小。一段 30 分钟的轨迹回放命令本身是异步返回的但如果底层实现里带阻塞式等待超时设成 60 秒就会在半路被杀掉设备停在轨迹中间。稳妥做法是按轨迹实际时长算留 60 秒余量。4.3 定时与断线恢复长时间回放最容易碰上的是进程被系统挂起后隧道失效。常见做法是加一个守护循环每 30 秒调一次tunneld --status发现设备消失就退出并提示重连而不是傻等。Windows 上还要注意笔记本的 USB 选择性挂起息屏后 usbmux 连接会断把电源计划里「USB 选择性暂停」关掉能省掉很多莫名其妙的掉线。5. 避坑与排查五类实际翻车记录5.1 连接与隧道类问题现象一usbmux list一直是空的。原因是驱动层没就位。Windows 上多半是只装了 iTunes 的旧版驱动或者 Apple 移动设备服务被优化软件禁用了Linux 上是当前用户不在plugdev组或者usbmuxd服务没起。解决顺序是Windows 装 Apple Devices 应用微软商店版本即可后重插数据线设备上点「信任」Linux 执行sudo systemctl restart usbmuxd并把用户加组后重新登录。现象二隧道起来了但 DVT 调用报Service not found。这是开发者模式没开。iOS 16 之后这个开关藏在「隐私与安全性」里打开后必须重启设备才生效。另一种可能是设备锁屏了——锁屏状态下调试服务会被挂起解锁再试。现象三昨天好好的今天 RSD 端口连不上。隧道是每次连接重新分配的端口全变。要么重启tunneld重新取要么用前面脚本里的自动解析。把端口硬编码进配置文件的第二天必然翻车。5.2 轨迹与业务校验类问题现象四轨迹回放完了App 里显示的位置还在原地。先分清是谁的问题。打开系统地图看蓝点有没有动动了说明定位注入成功问题在 App 侧——很多跑步类 App 用的是自己的定位 SDK做了坐标系转换WGS-84 转 GCJ-02 之类还有些做了缓存和去抖位置更新有几十秒延迟。没动则是注入没生效回到现象二排查。现象五轨迹跑通了但被判定为异常。根因几乎都在物理合理性上。第一是速度相邻点算出来的瞬时速度如果超过 10 米每秒任何带风控的业务都会标记第二是海拔平地跑步ele却在一分钟内上下几十米同样可疑第三是传感器数据对不上——这是这套方案的天花板虚拟定位只改 GPS加速度计读到的还是静止状态带运动传感器融合校验的业务一定识别得出来。第三点没法靠调参绕过去只能接受。注意上述第五条的边界是硬约束不是配置问题。把方案用在需要真实运动数据的正式场景里风险和责任都在使用者这边。6. 让轨迹更像真的插值平滑与端到端验证方法调通之后真正决定成败的是「像不像」。我一般做三件事来验证。第一件是速度曲线自检把生成的 GPX 读回来逐点算相邻点的距离除以时间差输出一条速度序列看有没有尖刺。人跑步的速度是连续变化的起跑慢、中途稳、最后冲刺一条完全水平的直线反而假。可以在插值时给speed_mps加一个正负 5% 的缓变扰动用正弦函数按时间加权就行一两行代码的事。第二件是时间戳对齐。生成的轨迹时间基准如果是datetime.now()跑到一半就变成「未来时间」了某些业务会校验轨迹点时间不得晚于当前时间。做法是让起始时间往前推正好让最后一个点落在当前时刻附近。这一点我在一个模拟项目上栽过排查了两小时才发现是时钟偏移。第三件是端到端对照。开两个终端一个跑回放另一个用pymobiledevice3 syslog live抓设备日志过滤locationd关键字能看到定位更新的真实频率。日志里的时间间隔和你 GPX 里设定的采样间隔对不上说明设备端做了节流这时候把采样间隔调大反而更接近真实。验证项方法合格标准速度连续性逐点算瞬时速度无超过 8 m/s 的尖刺时间合理性检查末点时间戳不晚于当前 UTC 时间采样频率syslog 过滤 locationd与设定间隔偏差小于 50%坐标精度系统地图目视蓝点落在目标路段上最后说个习惯。从那以后我每次跑轨迹回放都强制先走一遍clear → set 单点 → 目视确认 → play → clear这条五步流程不管多急都不跳步。单点那一步看似多余但它能在十秒内告诉你链路到底通不通省掉后面「轨迹跑了但没效果」时的两小时瞎猜。工具是死的流程是拿来兜底的。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。