<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>How It Ships · 中文</title><description>记录软件怎么做出来，又怎么发布出去。</description><link>https://blog.masonships.app/zh/</link><language>zh-Hans</language><item><title>我的模拟器哪来的位置？</title><link>https://blog.masonships.app/zh/posts/simulator-app-location/</link><guid isPermaLink="true">https://blog.masonships.app/zh/posts/simulator-app-location/</guid><description>开着的 Simulator app 会给本该没有位置的 UI 测试送来一个位置，simctl location clear 也收不回来。</description><pubDate>Fri, 16 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有些 UI 测试要求模拟器开始时没有位置。这一点，光靠 &lt;code&gt;xcrun simctl location clear&lt;/code&gt; 保证不了：只要 Simulator app 开着，Features ▸ Location 里又选了位置，模拟器开机几秒后就会收到这个位置，而 &lt;code&gt;clear&lt;/code&gt; 清不掉已经收到的位置。所以跑这类测试前，要先退出 Simulator app（或者把 Features ▸ Location 设成 None），再重启模拟器。&lt;/p&gt;
&lt;p&gt;示例 app 和文中所有脚本都放在 GitHub 上：&lt;a href=&quot;https://github.com/masonhaoye/blog-examples/tree/main/simulator-app-location&quot;&gt;blog-examples/simulator-app-location&lt;/a&gt;。测试环境是 macOS 26.6.2、Xcode 26.5，模拟器是 iPhone 16 Pro（iOS 18.5）。&lt;/p&gt;
&lt;aside&gt;&lt;p id=&quot;sidenote-label-1&quot;&gt;名词说明&lt;/p&gt;
&lt;p&gt;文中的“模拟器”指模拟出来的那台 iPhone，“Simulator app”指 Mac 上显示它的那个应用。&lt;code&gt;simctl&lt;/code&gt; 是 Xcode 自带的命令行工具，用 &lt;code&gt;xcrun simctl&lt;/code&gt; 调用。脚本和测试都用它控制模拟器，用不着打开 Simulator app。&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;经过&quot;&gt;经过&lt;/h2&gt;
&lt;p&gt;我在用 coding agent 开发一个 iPhone app，由 Codex 写代码，并在我的 Mac 上用模拟器跑 UI 测试。其中有个测试要检查 app 等待定位时的界面，所以模拟器一开始不能有位置。测试脚本会先重启模拟器，再执行 &lt;code&gt;simctl location clear&lt;/code&gt;。测试确认界面上还没有地图，才用 XCTest 提供的 &lt;a href=&quot;https://developer.apple.com/documentation/xctest/xcuidevice/location&quot;&gt;&lt;code&gt;XCUIDevice.shared.location&lt;/code&gt;&lt;/a&gt; 放入位置。&lt;/p&gt;
&lt;p&gt;有一天，这一步怎么跑都失败：测试还没放入位置，地图就已经出来了。同一份代码早上还能通过，问题显然出在环境上。查下来，Mac 上开着一个 Simulator app，是 Codex 无意中打开的。当时另一个工具读不到界面层级，Codex 想恢复，就调用了 &lt;a href=&quot;https://mobilebuildmcp.com/&quot;&gt;XcodeBuildMCP&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;https://blog.masonships.app/zh/posts/simulator-app-location/#user-content-fn-rename&quot; id=&quot;user-content-fnref-rename&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; 的 &lt;code&gt;open_sim()&lt;/code&gt;，顺手把 Simulator app 打开了。把它退出，测试就又通过了。&lt;/p&gt;
&lt;p&gt;测试是修好了，可我还是想知道：明明重启又清除了，位置是从哪来的？&lt;/p&gt;
&lt;h2 id=&quot;位置是从哪来的&quot;&gt;位置是从哪来的&lt;/h2&gt;
&lt;p&gt;模拟器上的位置，测试自己能控制的有两个来源：脚本里的 &lt;code&gt;simctl location&lt;/code&gt;，和测试代码里的 &lt;code&gt;XCUIDevice.shared.location&lt;/code&gt;。但还有两个来源，测试管不到。&lt;/p&gt;
&lt;p&gt;一个是 Simulator app 的 Features ▸ Location 菜单。这个设置整个 Simulator app 共用一份，存在 Mac 的用户偏好设置里，谁上次选了什么，它就一直是什么。可以用 &lt;code&gt;defaults read com.apple.iphonesimulator LocationMode&lt;/code&gt; 查看：5101 是 None，5102 是 Custom Location。不过这些数值没有公开文档，换个 Xcode 版本可能就不一样了。只要 Simulator app 开着、这里又选了位置，模拟器一开机，过几秒它就开始每秒发一次这个位置。我遇到的就是这样：位置比脚本的 &lt;code&gt;clear&lt;/code&gt; 早到了好几秒。&lt;/p&gt;
&lt;p&gt;另一个是模拟器自己留着的最后一个位置。&lt;code&gt;simctl location clear&lt;/code&gt; 只能让新的位置不再进来，已经收到的那个它收不回，下一个要定位的 app 马上就能拿到。要清掉它只能重启；而重启之后想保持没有位置，Simulator app 还得关着，或者设成 None。&lt;/p&gt;
&lt;p&gt;这样一来，我的清除步骤其实一点用都没有：位置早就到了，清除又清不掉。&lt;/p&gt;
&lt;h2 id=&quot;怎么验证的&quot;&gt;怎么验证的&lt;/h2&gt;
&lt;p&gt;为了排除是我自己 app 的问题，我们写了一个最简单的 app，叫 LocationProbe：只有一个页面，显示收到的坐标，以及打开后第几秒收到。它的核心就是遍历 Core Location 的异步位置流 &lt;a href=&quot;https://developer.apple.com/documentation/corelocation/cllocationupdate/liveupdates(_:)&quot;&gt;&lt;code&gt;CLLocationUpdate.liveUpdates()&lt;/code&gt;&lt;/a&gt;，每收到一个位置记一条日志（已简化）：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; try&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; update &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; CLLocationUpdate.&lt;/span&gt;&lt;span&gt;liveUpdates&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    guard&lt;/span&gt;&lt;span&gt; let&lt;/span&gt;&lt;span&gt; location &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; update.location &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    let&lt;/span&gt;&lt;span&gt; seconds &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;timeIntervalSince&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;launchedAt&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    coordinate &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;format&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;%.6f, %.6f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, location.&lt;/span&gt;&lt;span&gt;coordinate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;latitude&lt;/span&gt;&lt;span&gt;, location.&lt;/span&gt;&lt;span&gt;coordinate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;longitude&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    logger.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Location &lt;/span&gt;&lt;span&gt;\(&lt;/span&gt;&lt;span&gt;coordinate, privacy&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; at &lt;/span&gt;&lt;span&gt;\(&lt;/span&gt;&lt;span&gt;seconds, format&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;span&gt;fixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;precision&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;, privacy&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; s&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;每个实验在 &lt;code&gt;scripts/&lt;/code&gt; 里都有对应的脚本。脚本会先编译、安装 LocationProbe，并授予它定位权限。&lt;/p&gt;
&lt;h3 id=&quot;实验一位置从哪来&quot;&gt;实验一：位置从哪来？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;scripts/observe-location.sh &amp;lt;UDID&amp;gt; &amp;lt;output-folder&amp;gt;&lt;/code&gt; 会按我测试脚本的方式准备模拟器，然后打开 LocationProbe，60 秒内每 5 秒截一张图，最后导出系统日志。&lt;code&gt;&amp;lt;UDID&amp;gt;&lt;/code&gt; 是模拟器的标识，用 &lt;code&gt;xcrun simctl list devices&lt;/code&gt; 能查到。脚本的核心部分如下（已简化）：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; shutdown&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;BOOT_TIME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;+%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; boot&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; bootstatus&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; clear&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; launch&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; com.example.LocationProbe&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; io&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; screenshot&lt;/span&gt;&lt;span&gt; t05.png&lt;/span&gt;&lt;span&gt;    # 之后每 5 秒截一张&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; spawn&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; log&lt;/span&gt;&lt;span&gt; show&lt;/span&gt;&lt;span&gt; --start&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$BOOT_TIME&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; --info&lt;/span&gt;&lt;span&gt; --debug&lt;/span&gt;&lt;span&gt; --style&lt;/span&gt;&lt;span&gt; compact&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    --predicate&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;subsystem == &quot;com.example.LocationProbe&quot; OR process == &quot;locationd&quot;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们跑了三次：Simulator app 关着；开着，Features ▸ Location 设为 None；开着，选了旧金山的一个自定义位置。&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;img alt=&quot;Simulator app 关着&quot; width=&quot;1206&quot; height=&quot;1560&quot; src=&quot;https://blog.masonships.app/_astro/closed-at-5s.Dhi7QNNN_2wzz05.webp&quot; /&gt;&lt;figcaption&gt;Simulator app 关着&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img alt=&quot;Simulator app 开着，选了自定义位置&quot; width=&quot;1206&quot; height=&quot;1560&quot; src=&quot;https://blog.masonships.app/_astro/open-at-5s.B2mt6q0R_1iOlY0.webp&quot; /&gt;&lt;figcaption&gt;Simulator app 开着，选了自定义位置&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;开机后第 5 秒。左边 Simulator app 关着；右边开着并选了自定义位置，屏幕上已经有坐标，状态栏也出现了定位箭头。&lt;/p&gt;





























&lt;div&gt;&lt;div&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Simulator app&lt;/th&gt;&lt;th&gt;Features ▸ Location&lt;/th&gt;&lt;th&gt;模拟器收到位置&lt;/th&gt;&lt;th&gt;LocationProbe 拿到位置&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;关着&lt;/td&gt;&lt;td&gt;任意&lt;/td&gt;&lt;td&gt;没有&lt;/td&gt;&lt;td&gt;没有&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;开着&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;没有&lt;/td&gt;&lt;td&gt;没有&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;开着&lt;/td&gt;&lt;td&gt;旧金山的自定义位置&lt;/td&gt;&lt;td&gt;开机约 5 秒后起每秒一次，直到清除&lt;/td&gt;&lt;td&gt;打开后 0.1–0.2 秒&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;第三次的日志能看出先后顺序。下面是删节过的日志，另外补上了脚本执行的命令：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;14:01:59.119  simctl boot&lt;/span&gt;
&lt;span&gt;14:02:04.148  locationd      Accepting New Location&lt;/span&gt;
&lt;span&gt;14:02:04.182  locationd      Received an initial location (pinned), unregistering&lt;/span&gt;
&lt;span&gt;14:02:05.154  locationd      Accepting New Location&lt;/span&gt;
&lt;em&gt;... (once a second)&lt;/em&gt;
&lt;span&gt;14:02:11.175  locationd      Accepting New Location&lt;/span&gt;
&lt;span&gt;14:02:11.616  simctl location clear&lt;/span&gt;
&lt;span&gt;14:02:12.177  simctl launch&lt;/span&gt;
&lt;span&gt;14:02:13.779  LocationProbe  Location 37.785834, -122.406417 at 0.1 s&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;可以看到，脚本执行清除时，位置早就到了。&lt;/p&gt;
&lt;h3 id=&quot;实验二为什么清除没用&quot;&gt;实验二：为什么清除没用？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;scripts/leftover-location.sh &amp;lt;UDID&amp;gt; &amp;lt;output-folder&amp;gt;&lt;/code&gt; 开始前，Simulator app 是关着的，菜单里选了一个位置。脚本一共六步，每步结束后重新打开 LocationProbe，观察 15 秒。下面的 &lt;code&gt;reboot&lt;/code&gt; 代表关机、开机并等待开机完成：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;reboot&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; clear&lt;/span&gt;&lt;span&gt;                       # 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; set&lt;/span&gt;&lt;span&gt; -37.813600,144.963100&lt;/span&gt;&lt;span&gt;           # 2  墨尔本&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; clear&lt;/span&gt;&lt;span&gt;                               # 3  只清除，不重启&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; set&lt;/span&gt;&lt;span&gt; -37.813600,144.963100&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;reboot&lt;/span&gt;&lt;span&gt;   # 4  只重启，不清除&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;open&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; Simulator&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;reboot&lt;/span&gt;&lt;span&gt;                                         # 5  打开 Simulator app&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;osascript&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;quit app &quot;Simulator&quot;&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;reboot&lt;/span&gt;&lt;span&gt;                       # 6  退出 Simulator app（AppleScript，相当于 ⌘Q）&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;脚本输出：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;Rebooted and cleared:                          no location (0 in 15 s)&lt;/span&gt;
&lt;span&gt;Set Melbourne with simctl:                     Location -37.813600, 144.963100 at 0.1 s (3 in 15 s)&lt;/span&gt;
&lt;span&gt;Cleared, not rebooted:                         Location -37.813600, 144.963100 at 0.1 s (1 in 15 s)&lt;/span&gt;
&lt;span&gt;Set Melbourne again, rebooted, not cleared:    no location (0 in 15 s)&lt;/span&gt;
&lt;span&gt;Simulator app open, rebooted, not cleared:     Location 37.785834, -122.406417 at 0.1 s (3 in 15 s)&lt;/span&gt;
&lt;span&gt;Simulator app quit, rebooted, not cleared:     no location (0 in 15 s)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;清除只能挡住后续的更新（15 秒内从三次变成一次），最后那个位置还在。重启则能把它清掉，不管它是怎么来的。前提是 Simulator app 没开着，否则它又会把位置发过来。&lt;/p&gt;
&lt;h3 id=&quot;实验三真正的-ui-测试里也这样吗&quot;&gt;实验三：真正的 UI 测试里也这样吗？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;scripts/run-probe-test.sh &amp;lt;UDID&amp;gt; &amp;lt;output-folder&amp;gt;&lt;/code&gt; 用同样的方式准备模拟器，然后运行 LocationProbe 自带的 UI 测试。这个测试打开 app，等 10 秒，再把 app 显示的内容打印出来：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;xcodebuild&lt;/span&gt;&lt;span&gt; build-for-testing&lt;/span&gt;&lt;span&gt; -project&lt;/span&gt;&lt;span&gt; LocationProbe.xcodeproj&lt;/span&gt;&lt;span&gt; -scheme&lt;/span&gt;&lt;span&gt; LocationProbe&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -destination&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;id=&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# ...像实验一那样重启、清除，然后：&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcodebuild&lt;/span&gt;&lt;span&gt; test-without-building&lt;/span&gt;&lt;span&gt; -project&lt;/span&gt;&lt;span&gt; LocationProbe.xcodeproj&lt;/span&gt;&lt;span&gt; -scheme&lt;/span&gt;&lt;span&gt; LocationProbe&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -destination&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;id=&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;# Simulator app closed&lt;/span&gt;
&lt;span&gt;PROBE coordinate: No location yet&lt;/span&gt;
&lt;span&gt;PROBE detail: Waiting for a location&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;# Simulator app open, custom location&lt;/span&gt;
&lt;span&gt;PROBE coordinate: 37.785834, -122.406417&lt;/span&gt;
&lt;span&gt;PROBE detail: First location 0.2 s after launch&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;换成 &lt;code&gt;xcodebuild&lt;/code&gt; 跑测试，结果也一样：只要 Simulator app 开着、又选了位置，被测 app 一启动就已经有位置了。&lt;/p&gt;
&lt;h2 id=&quot;我现在的做法&quot;&gt;我现在的做法&lt;/h2&gt;
&lt;p&gt;如果 UI 测试在本机跑，又要求一开始没有位置，我现在会做两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;跑测试时，Simulator app 必须关着。&lt;/strong&gt; 由测试脚本来检查，不指望人或 agent 记得。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这类测试开跑前，先重启模拟器。&lt;/strong&gt; 光清除不够：模拟器已经收到的位置，比如前一个测试设的，清除收不回来。本机为了快，我一直复用同一台模拟器，不会每次都新建或抹掉，前面留下的位置很可能还在。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;放进测试脚本里，就是这几行：&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; pgrep&lt;/span&gt;&lt;span&gt; -x&lt;/span&gt;&lt;span&gt; Simulator&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;Quit the Simulator app first: its Features &amp;gt; Location setting can override simctl location clear.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &amp;gt;&amp;amp;2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; shutdown&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; boot&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; bootstatus&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xcrun&lt;/span&gt;&lt;span&gt; simctl&lt;/span&gt;&lt;span&gt; location&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$UDID&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; clear&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这些我还没在 CI 上验证过。托管 CI 的每个 job 通常都从一台全新的机器开始，也不会打开 Simulator app，第一条应该天然满足。但同一个 job 里的测试共用一台模拟器，前一个测试设的位置可能留给后一个，所以重启这一步最好还是留着。&lt;/p&gt;
&lt;section&gt;&lt;h2 id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-rename&quot;&gt;
&lt;p&gt;XcodeBuildMCP 现已改名 MobileBuildMCP，是一个开源的 MCP 服务，让 agent 可以操作 Xcode 和模拟器。Codex 是通过 OpenAI 的 Build iOS Apps 插件用上它的。 &lt;a href=&quot;https://blog.masonships.app/zh/posts/simulator-app-location/#user-content-fnref-rename&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item></channel></rss>