
简介面向 iOS 17 及以上系统的免越狱虚拟定位与模拟跑步打卡工具包主要服务于需要在跨平台环境中研究定位模拟、运动轨迹生成与参数配置的开发者及测试人员。项目支持在线拾取路径并可调整跑步距离、时间、速度等参数适合定位功能验证、轨迹模拟实验及相关脚本学习。压缩包共 16 个文件约 15KB以 9 个 Python 脚本为核心配合 yaml/yml 配置、txt 路线数据、markdown 说明与许可证文件目录按连接、定位、路径、隧道与运行等职责拆分结构轻量便于按功能阅读。现有 716 人学习读者可了解 iOS 免越狱定位模拟的命令行实现思路、路径数据组织方式与配置项设计并据此搭建实验环境。需注意将虚拟定位用于运动应用打卡可能违反平台条款更适合技术研究与合规测试。1. 免越狱位置注入的真相一条 DTX 通道替你说了 GPS 的话在 iOS 17 上做跑步类 App 的轨迹联调最省事的做法不是刷机、也不是改包而是把设备当成一台开发者目标机通过 DVT 的模拟位置服务往 CoreLocation 里喂坐标。这套免越狱的跨平台方案核心是一条 DTX 通道宿主端Windows、macOS、Linux 都能跑通过 USB 连上真机把一条条经纬度和时间戳写进com.apple.dt.simulate-location服务系统再把这些位置派发给所有申请定位的进程。对做模拟跑步打卡的人来说它解决「手里只有一台真机、没有现成运动轨迹可回放」的问题对开发者来说它解决「坐在工位上复现定位边界逻辑」的问题。适合两类人需要反复回放同一条路线的测试岗以及想把跑步 App 数据链路跑通的从业者。2. 环境搭建三平台各自的握手路径与第一条位置动手之前先把路线选对后面能省掉一半折腾。这一章把选型、设备握手和第一条命令串起来Windows、macOS、Linux 三端都能照做。2.1 先选路线Xcode GPX、pymobiledevice3、还是现成工具能往真机里注入位置的方案其实只有三类边界差别很大选错了会出现「明明按教程做了、位置就是不动」的情况。第一类是 Xcode 自带的 Location Simulation。设备连上 Mac 后在 Debug 菜单里挂一个 GPX 文件位置就会跟着 GPX 走。这条路门槛最低但限制也最硬必须有一台装了 Xcode 的 Mac必须处在调试会话里App 进程一退出、Xcode 一断开模拟位置立刻失效。做短时间的界面联调够用做几十分钟的整段跑步回放就不合适了中途电脑休眠、Xcode 崩溃都会让你重来。第二类是现成的商业工具图形界面点几下就能开跑。它底层做的还是同一件事胜在省事代价是黑匣子——出了问题你不知道是设备端没连上、还是它自己的服务挂了排错基本靠猜。如果只是偶尔用一次可以如果要接进自动化测试流水线不合适。第三类是基于pymobiledevice3的命令行方案。这是一个纯 Python 的实现通过 usbmuxd 与设备通信自己起 DVT 客户端去调用模拟位置服务所以它不依赖 XcodeWindows、macOS、Linux 都能跑命令可以写进脚本、可以挂定时任务、可以接 CI。血泪经验是它的子命令在不同版本之间命名会有细微出入别背命令先跑--help确认当前版本有哪些子命令再往下写脚本。我一般会用第三类原因很实际跑步轨迹回放是个持续几十分钟甚至更久的动作中途断连、系统弹窗、设备锁屏都会发生只有能自己控制重试逻辑的方案才扛得住。Xcode 那套适合看一眼效果长期跑还得靠脚本。2.2 Developer Mode 与配对没这两步后面全是连不上iOS 16 之后DVT 这条链路要求设备开启开发者模式iOS 17 更是如此。没开的话服务能连上但位置写不进去表现就是命令返回正常、地图上纹丝不动——这是最容易翻车的一步。操作路径在设备上设置 → 隐私与安全性 → 开发者模式打开开关系统会要求重启重启后再回来确认一次开关是打开的。注意这个选项只有在设备曾经连接过开发工具、或者被识别为开发目标之后才会出现如果找不到先用数据线连一次电脑、让系统认一下设备。配对信任是第二步。首次连接时设备会弹「是否信任此电脑」点信任并输入锁屏密码。这一步不做后面所有命令都会在握手阶段超时。驱动层面的差异按平台来Windows 需要 Apple 提供的设备驱动装 Apple Devices 或 iTunes 即可装完之后设备管理器里能看到正常的复合设备pymobiledevice3 usbmux list才能列出来。macOS 自带 usbmuxd通常不用额外装东西但如果之前装过第三方手机助手类软件可能抢占 usbmuxd 端口表现是设备能充电但列不出来。Linux 需要装usbmuxd和libimobiledevice相关包把当前用户加进plugdev组否则普通用户没有权限访问设备节点。这三条里Windows 的驱动问题和 Linux 的权限问题出现频率最高。判断方法很统一跑pymobiledevice3 usbmux list能列出设备的 UDID 就说明链路通了列不出来就别往下折腾位置命令先修这一层。2.3 三平台跑通第一条 set 命令链路通了之后先别急着上轨迹用单点注入验证整条链路这是最省时间的做法。# 安装或升级到最新版避免子命令命名对不上 python -m pip install -U pymobiledevice3 # 列出当前通过 USB 连接的设备确认能看到 UDID pymobiledevice3 usbmux list # 单点注入把设备位置设到某个坐标示例用一组虚构坐标 pymobiledevice3 dvt simulate-location set --latitude 31.2304 --longitude 121.4737 # 用完清除模拟位置让设备回到真实定位 pymobiledevice3 dvt simulate-location clearset这条命令做的是「把当前位置固定为给定经纬度」不涉及移动适合验证服务通不通。执行完之后打开设备上的地图类 App看蓝点有没有跳到目标位置跳了说明从驱动、配对、开发者模式到 DVT 服务整条链路都是通的可以进入轨迹回放阶段没跳就按上一节的顺序回查别在轨迹格式上找原因。clear这一步很多人会忘后果是设备一直停在模拟位置直到重启或者重新建立真实定位。我习惯在脚本里用try/finally保证退出时一定清一次后面第 4 章会给出带清理的封装。如果设备不止一台所有子命令都可以加--udid指定目标多设备并行回放时这个参数是必需的。如果命令提示连接超时先拔插一次数据线再确认设备处于解锁状态——锁屏状态下部分服务会拒绝握手这也是新手最容易忽略的一点。3. 轨迹生成把配速翻译成带时间戳的坐标序列位置能注进去了接下来要解决「跑得像不像」。跑步轨迹不是一串等距点它由距离、时间、配速三者共同决定任何一个对不上回放出来就会瞬移或者原地踏步。3.1 GPX 结构里真正影响回放的四个字段GPX 是个 XML 容器元素很多但真正影响回放效果的只有四个trkpt的lat、lon、time以及可选的ele。其余像name、desc、type都是给人看的回放时会被忽略。?xml version1.0 encodingUTF-8? gpx version1.1 creatorroute-gen xmlnshttp://www.topografix.com/GPX/1/1 trk namemorning-run/name trkseg !-- 每个 trkpt 是一个采样点time 必须是递增的 UTC 时间 -- trkpt lat31.230400 lon121.473700 ele12.0/ele time2024-06-01T06:00:00Z/time /trkpt trkpt lat31.230850 lon121.473700 ele12.0/ele time2024-06-01T06:00:15Z/time /trkpt /trkseg /trk /gpx两个相邻点之间的距离除以时间差就是这一段的速度回放端会按这个速度在两点之间做线性插值。举例来说上面两点纬度差约 0.00045 度对应约 50 米时间差 15 秒折算速度约 3.33 米每秒也就是每公里 5 分钟的配速——这已经偏快了普通慢跑在 6 到 7 分钟每公里。time字段最容易出问题。它必须是 UTC 时间、带Z后缀、严格单调递增。手改 GPX 时如果两个点写了同样的时间回放端会认为速度无穷大地图上表现为一次瞬移如果时间倒退有的实现会直接跳过后续所有点。这两种情况都不会报错只会让你对着屏幕怀疑人生属于典型的玄学故障。ele海拔对跑步记录的影响比想象中大。运动类 App 计算爬升、训练负荷时会用到它如果整条轨迹海拔都是 0某些平台会判定为异常数据。做测试轨迹时给个合理值比如 10 到 30 米之间小幅波动比一条 0 更自然。3.2 按配速插值距离、时间与点密度的换算手工写几十个点不现实用脚本按「起点、方向、配速、时长」生成才可复现。核心是两个计算球面距离和按配速反推时间。import math import datetime 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 offset(lat, lon, north_m, east_m): 从给定点向北、向东各偏移若干米返回新坐标 dlat north_m / R dlon east_m / (R * math.cos(math.radians(lat))) return lat math.degrees(dlat), lon math.degrees(dlon) def build_track(lat, lon, pace_sec_per_km, total_m, step_m10.0): 按配速生成一条正北方向的直线轨迹step_m 控制采样点间距 speed 1000.0 / pace_sec_per_km # 米每秒 points [] dist 0.0 t0 datetime.datetime(2024, 6, 1, 6, 0, 0, tzinfodatetime.timezone.utc) while dist total_m: nlat, nlon offset(lat, lon, dist, 0.0) # 时间 已跑距离 / 速度保证 time 与距离严格对应 sec dist / speed points.append((nlat, nlon, t0 datetime.timedelta(secondssec))) dist step_m return points def write_gpx(points, path): with open(path, w, encodingutf-8) as f: f.write(?xml version1.0 encodingUTF-8?\n) f.write(gpx version1.1 creatorroute-gen xmlnshttp://www.topografix.com/GPX/1/1\n) f.write( trknameauto-run/nametrkseg\n) for lat, lon, t in points: f.write(f trkpt lat{lat:.6f} lon{lon:.6f}\n) f.write(f ele15.0/ele\n) f.write(f time{t.strftime(%Y-%m-%dT%H:%M:%SZ)}/time\n) f.write( /trkpt\n) f.write( /trkseg/trk\n/gpx\n) # 6 分配速、5 公里、每 10 米一个采样点 track build_track(31.2304, 121.4737, pace_sec_per_km360, total_m5000, step_m10) write_gpx(track, run_5km.gpx) print(f生成 {len(track)} 个点最后一点时间 {track[-1][2].isoformat()})pace_sec_per_km是每公里秒数6 分配速填 3605 分 30 秒填 330。step_m决定采样密度10 米一个点5 公里就是 500 个点文件几十 KB回放端处理起来毫无压力如果图省事设成 100 米一个点两点之间要靠插值补齐遇到转角或者变速时轨迹会显得很「直」不像人跑出来的。haversine里用asin而不是atan2是因为距离在几公里量级时前者数值更稳offset里的经度偏移要除以cos(lat)否则在高纬度地区东西方向的米数会算错——这个坑在国内大部分城市不明显但一旦把坐标换到高纬度就暴露出来。生成完先自己检查两点相邻点的时间差是否等于step_m / speed最后一点的时间是否等于「总距离 / 速度」。这两个对上了配速就是准的。3.3 路线来源手绘、GPX 下载与地图抽稀直线轨迹只能验证链路真要跑一条像样的路线坐标得从别处来。常见做法有三种。第一种是从运动社区导出别人跑过的 GPX直接拿来改时间戳。这类文件的优点是路线真实、有自然的转弯和坡度缺点是采样密度往往不均匀——有的地方一秒一个点有的地方十几秒一个点回放时速度会出现跳变。处理办法是按固定的距离间隔重采样把原始点串成折线再沿折线每 10 米取一个点时间根据累计距离和配速重新算。第二种是在地图上手工点几个途经点用折线连接。适合做固定路线的回归测试比如「每天都是同一条 3 公里路线」。缺点是拐角是硬折线地图上看像机器人跑出来的可以在拐角处做圆角处理把尖角替换成一小段圆弧。第三种是程序化生成给定起点和若干方向变化模拟一条环形路线。这种方式最可控适合需要批量生成不同长度、不同配速测试用例的场景缺点是路线形状不真实只适合内部测试。不管用哪种来源写入 GPX 前都建议做一次「抽稀-加密」先把过密的点合并比如间距小于 3 米的直接丢掉再按 10 米间隔插值补齐。这样出来的文件体积可控回放端也不会因为点太多而出现时间精度问题。4. 持续回放注入节奏、断连重试与跨平台差异单点注入和短轨迹都跑通之后真正考验方案的是「连续跑四十分钟不出事」。这一章讲回放节奏怎么定、断了怎么接、三个平台差在哪。4.1 回放节奏interval 与设备端插值的关系回放 GPX 时宿主端并不是把每个点都发一遍就完事它要按轨迹里的时间关系把点逐个送进设备。这里有两个时间概念要分清一个是轨迹本身的时间轴GPX 里time的间隔一个是宿主端的发送节奏两次命令之间的实际间隔。常见做法是让宿主端按 GPX 的时间轴走中间不额外加延时设备端负责在相邻两个注入点之间做插值这样地图上的蓝点是平滑移动的而不是一格一格跳。如果把发送间隔拉得很长比如每 5 秒才推一个点设备端的插值窗口就被拉大转弯处会出现「先直着走一段再突然折向目标点」的观感速度曲线也会变得很方。发送间隔也不能压得太短。压到几十毫秒一个点USB 通道的吞吐和设备的服务处理跟不上表现是命令排队、位置更新滞后跑完一公里地图上才走了一半。比较稳的范围是每 0.5 到 1 秒推一个点对应 GPX 里 10 米左右的点距兼顾平滑和负载。播放速度是另一个可调项。有些实现提供倍速参数可以 2 倍、5 倍地快放用来在几分钟内验证完一条一小时的轨迹。做功能验证时用倍速很省时间但要注意倍速下设备端收到的相邻点时间差被压缩某些运动 App 会把这种速度判定为异常记录页可能直接不认。要验证「能不能被正常记录」还是得按 1 倍速老老实实跑。4.2 断连重试与状态机长时间回放最常见的故障是 USB 会话中断数据线接触不良、电脑休眠、设备锁屏、系统弹出信任确认。断了之后进程要么报错退出要么卡在那里不动如果你没盯着屏幕可能半小时后才发现早就停了。解决办法是给回放套一层状态机连接、推送、心跳检测、失败重连、清理五个状态循环。下面是一个简化版封装思路可以直接抄。import subprocess import time import sys UDID 00008120-0000000000000000 # 换成 pymobiledevice3 usbmux list 看到的 UDID GPX run_5km.gpx MAX_RETRY 5 def run(cmd, timeout120): 执行命令返回 (是否成功, 输出) try: p subprocess.run(cmd, capture_outputTrue, textTrue, timeouttimeout) return p.returncode 0, (p.stdout p.stderr).strip() except subprocess.TimeoutExpired: return False, timeout def device_alive(): 轻量心跳能列出设备就认为链路还在 ok, out run([pymobiledevice3, usbmux, list], timeout15) return ok and UDID in out def play_once(): ok, out run([ pymobiledevice3, dvt, simulate-location, play, GPX, --udid, UDID ], timeout3600) # 给足时间5 公里 6 分配速约 30 分钟 return ok, out retry 0 while retry MAX_RETRY: if not device_alive(): print(设备不在线3 秒后重试) time.sleep(3) retry 1 continue print(f开始回放第 {retry 1} 次尝试) ok, out play_once() if ok: print(回放完成) break print(回放中断, out[-300:]) retry 1 time.sleep(2) # 无论成败都清一次避免设备停在模拟位置 run([pymobiledevice3, dvt, simulate-location, clear, --udid, UDID], timeout30) sys.exit(0 if retry MAX_RETRY else 1)心跳检测用的是最便宜的usbmux list它不走 DVT 服务代价小、能快速判断 USB 层是否还活着。如果你直接拿play的返回码当心跳一次超时要等很久才能发现。timeout3600这个值要按轨迹长度给。5 公里 6 分配速是 30 分钟留一倍余量。给太小会在中途被误杀给太大又会在真断连时白等。我的习惯是按「预计时长 × 2 5 分钟」设。最后那句clear放在循环外面保证即使中途异常退出也会执行。这就是我前面说的后悔药没有它设备会一直停在模拟位置直到你手动清或者重启。4.3 Windows / macOS / Linux 的差异表三个平台跑同一套 Python 代码差异集中在驱动和权限层命令本身几乎一致。项目WindowsmacOSLinux设备驱动需装 Apple Devices 或 iTunes系统自带需装 usbmuxd、libimobiledevice权限一般无需提权一般无需提权用户需加入 plugdev 组常见冲突第三方手机助手抢占驱动第三方助手抢占 usbmuxd 端口udev 规则缺失导致无权限路径写法反斜杠注意转义正斜杠正斜杠休眠影响合盖即断合盖即断可配置不休眠实际踩下来Windows 上的问题八成出在驱动Linux 上的问题八成出在权限macOS 相对省心但要小心装了别的手机管理软件。三端共通的坑是电脑休眠只要系统进入睡眠USB 会话就断回放必然中断。真要跑长轨迹先把电源计划里的睡眠关掉或者干脆用一台不睡的小主机跑。跨平台还带来一个隐性差异路径分隔符和编码。脚本里读 GPX 时统一用 UTF-8 和正斜杠Windows 上 Python 能正常处理如果在命令行里直接写路径Windows 的反斜杠要转义。这类问题不致命但会让你在「明明代码一样、换台机器就跑不了」上浪费半天。5. 避坑排查位置回弹、模拟标记与时间戳异常这套方案看着简单真正上手时会遇到一批「命令没报错但结果不对」的问题。下面五条是我和同行踩过的按现象、原因、解决写清楚。5.1 五条踩坑记录现象一注入成功地图蓝点跳过去了过几秒又弹回真实位置。原因通常有两个一是回放进程已经退出但没执行clear系统在一段时间后自己恢复了真实定位二是设备同时开着 Wi-Fi 定位和蜂窝辅助定位系统用基站和热点做了融合把注入点「拉」回去了。解决办法是回放期间保持进程存活结束后显式clear如果回弹频繁临时关掉设备的 Wi-Fi只留 USB 连接让系统只剩注入这一个位置源。现象二位置注进去了但运动 App 的记录页显示「模拟位置」或者干脆不记录。这是系统层面的行为不是工具的问题。iOS 15 之后CLLocation提供了来源信息App 可以判断当前位置是不是软件模拟的一旦判定为模拟可以选择不写入运动记录。这个能力对开发者是调试利器对想「混过去」的人是硬门槛。做测试时用自己的 App、接受这个标记是最省事的态度指望换一个工具就能绕开多半是白费功夫。现象三轨迹回放中途开始瞬移蓝点一段一段地跳。先查时间戳。GPX 里相邻点的time如果不是严格递增或者两点时间差小到不真实比如 0.1 秒走了 50 米回放端会算出极高的瞬时速度地图上就是一次跳跃。用脚本生成时这个问题不会出现手改文件时最容易犯。解决办法是回放前跑一遍校验遍历所有点确认时间差大于 0、且速度落在 1 到 8 米每秒的合理区间超出的点直接剔除或重采样。现象四Windows 上设备能充电但usbmux list永远是空的。八成是驱动没装好或者被别的手机管理软件改写了。表现是设备管理器里设备带黄色感叹号或者根本不显示为 Apple 设备。先卸掉可疑的第三方助手重装 Apple Devices再拔插数据线。如果还是不行换一根能传数据的线——有些线只供电不传数据这种线插上去设备会充电但电脑永远认不到属于最冤的一种翻车。现象五回放跑了十几分钟突然卡住进程还在位置不动了。大概率是 USB 会话静默失效常见诱因是设备锁屏、系统弹出信任确认、或者电脑进了一次短暂的睡眠。进程没收到错误就一直等着。解决办法就是上一章那套心跳机制定期用usbmux list探活发现设备不在就重连重放。另外把设备的自动锁屏时间调长或者回放期间保持屏幕常亮能大幅降低这个问题的发生概率。5.2 出问题时的排查顺序遇到「位置不对」先别急着改轨迹文件按从下往上的顺序查能省很多时间。第一步查链路usbmux list能不能列出设备。列不出来后面全是白搭去修驱动、权限、数据线。第二步查服务单点set一个坐标看地图蓝点动不动。不动说明开发者模式没开或者配对没做跟轨迹格式无关。第三步查轨迹单点能动、回放不动问题在 GPX 本身。先看时间戳是否递增、速度是否合理再看文件编码是不是 UTF-8、XML 有没有闭合错误。第四步查设备状态回放中途停了看设备是不是锁屏了、是不是弹了信任框、电脑是不是睡了一觉。这个顺序的价值在于每一步都有明确的判断依据不会在「可能是工具问题、可能是文件问题」之间来回猜。我见过太多人一上来就怀疑 GPX 格式折腾半天最后发现是数据线只供电。6. 验证与进阶怎么确认位置真的进了设备、怎么让轨迹不假位置注进去只是第一步怎么证明它真的生效、怎么让回放出来的轨迹经得起看是另一层功夫。6.1 三层验证法第一层是命令层set或play返回成功只能说明宿主端把数据发出去了不代表设备收到了。这一层最不可靠别拿它当结论。第二层是系统层打开设备自带的地图看蓝点位置。蓝点动了说明 CoreLocation 已经拿到了注入值这一层才算真正验证了链路。第三层是应用层写一个最小的 CoreLocation Demo申请定位权限把每次回调的经纬度、时间戳、水平精度打到日志里和 GPX 里的点逐条比对。这一层能看出设备端插值后的实际采样频率、精度值是多少也能验证你的轨迹在「App 眼里」长什么样。运动类 App 判断数据是否可信看的就是这些字段先在 Demo 里把它们的量级摸清楚再去调轨迹事半功倍。6.2 平滑与噪声把匀速直线改成人类的跑法匀速直线的轨迹在数据上有个明显特征相邻段速度几乎完全一致方差极小。真实跑步不是这样配速会有起伏GPS 会有几米的漂移转弯处速度会降下来。想让轨迹自然一点就在生成阶段加两样东西。import random def humanize(points, pace_jitter0.05, gps_noise_m2.0): 给轨迹加配速抖动和 GPS 漂移模拟真实跑步数据 out [] for i, (lat, lon, t) in enumerate(points): # 配速抖动每点速度在基准上下浮动模拟体力变化 factor 1.0 random.uniform(-pace_jitter, pace_jitter) # 位置抖动水平和垂直方向各加一个米级随机偏移 dn random.gauss(0, gps_noise_m) de random.gauss(0, gps_noise_m) nlat lat dn / 6371000.0 * 57.2958 nlon lon de / (6371000.0 * 0.85) * 57.2958 # 时间按抖动系数微调保持距离与时间的对应关系 out.append((nlat, nlon, t)) return outpace_jitter控制速度波动的幅度0.05 表示上下 5%对应 6 分配速时每公里差十几秒接近普通人跑步的自然波动。gps_noise_m是位置漂移的标准差2 米左右比较像手机 GPS 的实际表现设太大比如 10 米会让轨迹看起来在画锯齿。加完噪声之后一定要重新校验时间戳的单调性和速度区间因为噪声会改变点间距个别点可能算出异常速度。这一步不能省省了就会在前面第 5 章说的瞬移问题上翻车。还有一个细节真实跑步在路口会有短暂的停顿或减速整条轨迹的速度曲线不是平的。如果要做得更细可以在轨迹里插入几个「速度降到 1 米每秒、持续 3 到 5 秒」的片段模拟等红灯。这种细节在数据层面比单纯加噪声更有说服力。6.3 一个习惯收尾从那以后我每次跑长轨迹都会先用一条 30 秒、200 米的短 GPX 打一遍全流程列设备、单点验证、短轨迹回放、清理四步走完再上正式轨迹。这四步花不了两分钟但能把驱动、开发者模式、时间戳、清理逻辑这几类问题在开局就暴露出来比跑到一半发现卡住要划算得多。工具本身不复杂难的是把「验证」变成肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取