Windows 上真机调试 iOS App,断点、变量与崩溃定位
这篇讲在 Windows 上用 VS Code 调试跑在 iPhone 真机上的 App,从连上手机到定位一次崩溃。适用于 Swift、Objective-C、C / C++ 写的原生代码,Flutter、React Native、Unity 等框架工程的原生部分也是同样的操作。你需要一台 iPhone 和数据线,工程已经能用 KXBuild 编译,KXBuild 的安装见安装页。
连接并信任设备
用数据线把 iPhone 接到电脑上,打开 VS Code 的 KXBuild 视图,「设备」节点会显示手机名、连接方式和系统版本。设备节点显示「点击配置」说明电脑上还没有 Apple 设备驱动,点一下按提示安装,过程中要以管理员身份确认一次。命令行执行 kxdevice doctor 效果相同。
第一次连接要在手机上完成两步设置。
- 信任此电脑。解锁手机,在弹窗中点「信任」。用的是充电线或者手机锁屏时弹窗不会出现。
- 打开开发者模式。在「设置 → 隐私与安全性 → 开发者模式」中打开,手机会重启一次,重启后按提示确认。这个开关在手机连接过电脑之后才会出现。
同一台手机可能同时出现 USB 和 Wi-Fi 两行,安装和调试选 USB 更稳定。接了多台手机时点设备节点或状态栏选一台,之后 F5 都装到这一台。命令行用下面的命令查看设备,第一列是 UDID。
1kxdevice device list
列表为空时换一根数据线或换个 USB 口,再解锁手机看有没有弹窗。更多情况见设备准备。
第一次调试某个 iOS 版本时会自动下载对应的符号库,有几个 GB,大约十多分钟,进度显示在通知中。只需下载一次,中断后下次继续。下载完成之前断点可能不停,或者只能看到汇编。
用 Debug 配置构建
调试前确认 KXBuild 视图「构建配置」节点显示的是 Debug。Release 配置开启了优化,断点位置会跳,变量也常常显示不出来。证书目录里要有 Apple Development 证书和包含这部手机 UDID 的 Development 描述文件,见证书与签名。
三种启动方式
按场景选一种。
| 方式 | 入口 | 适合的情况 |
|---|---|---|
| 构建并运行 | F5、KXBuild 视图标题栏 ▶、target 行内按钮 | 日常改代码后调试,会构建、签名、安装、启动并附加调试器 |
| 运行 | 「KXBuild: 运行」、target 行内按钮 | 代码没改,只想重新装一次上一次的构建产物并启动 |
| 附加 | 「KXBuild: 附加到运行中的 App」 | App 已经在手机上运行,问题要操作一段时间才出现 |
构建发生在调试会话开始之前,调试工具栏上的「重启」不会重新编译。改了代码要先停止,再按 F5。
默认按 F5 就能启动,不需要 launch.json。想固定某台设备、某个 target 或构建配置时再写 launch.json,字段说明见运行与调试。

设置断点
在源码行号左侧单击就能加断点,命中后使用 VS Code 标准的调试视图,单步、继续、跳出的快捷键和其他语言一样。下面几种断点都在行号左侧右键添加,或者在已有断点上右键「编辑断点」修改。
条件断点
循环里或者被频繁调用的函数,普通断点会停很多次。右键「添加条件断点」,选「表达式」,填一个用源码语言写的条件,只有条件为真时才停。
1index == 42 && item.name == "test"
同一个菜单里选「命中次数」,填 10 表示第 10 次经过时才停,适合排查第 N 次才出现的问题。
日志断点
右键「添加日志点」,填一段消息,经过这一行时把消息打印到调试控制台,程序不停。花括号里的表达式会被替换成当时的值。
1加载第 {index} 项,count = {items.count}
临时想看某个值又不想改代码、重新编译时用它,比加 print 快。
异常断点
调试时展开「运行和调试」视图底部的「断点」面板,会列出 Swift Throw、Objective-C Throw、C++ Throw 等选项,勾选后在异常抛出的位置停下。
排查 Objective-C 的 NSException 崩溃(例如数组越界、给 nil 发不存在的消息)时勾选 Objective-C Throw,停下来的位置就是抛出异常的那一行,比等崩溃后看报告直接得多。
函数断点
在「断点」面板标题栏点「+」,输入函数名,例如 viewDidLoad 或 -[MyViewController viewDidLoad],不用找到源码就能在函数入口停下。
查看变量和调用栈
断点命中后,左侧几个面板分别回答不同的问题。
| 面板 | 用途 |
|---|---|
| 变量 | 当前函数的局部变量和 self,对象可以逐层展开 |
| 监视 | 添加表达式,每次停下都自动求值,适合一直盯着的值 |
| 调用栈 | 每个线程的调用链,点一帧就切到那一层查看它的变量 |
鼠标悬停在源码里的变量上也能看到当前值。停在没有源码的位置时,可以打开汇编视图查看。
在调试控制台执行 LLDB 命令
调试控制台(Ctrl+Shift+Y)可以直接输入 LLDB 命令,下面是最常用的几条。
| 命令 | 作用 |
|---|---|
po self |
打印对象的描述,等同于调用 description |
p count * 2 |
求表达式的值并打印类型 |
v |
打印当前帧的所有局部变量,不执行代码,速度快 |
bt |
打印当前线程的调用栈 |
bt all |
打印所有线程的调用栈,排查卡死、死锁时用 |
thread list |
列出所有线程和各自停在哪里 |
expr self.view.backgroundColor = .red |
在运行中修改状态,继续执行后能看到效果 |
输入的内容被当成表达式而不是命令时,在前面加一个反引号,例如 `bt。
看 App 输出和设备日志
App 里 print、NSLog、os_log 的输出显示在「输出」面板的 KXBuild Console 通道,构建日志在 KXBuild 通道。输出面板用 Ctrl+Shift+U 打开,右上角下拉框切换。
App 没在调试、或者要看系统层面的日志时,用 kxdevice 直接读手机日志。ostrace 只输出 os_log,NSLog 和 Logger 都经过它,比 syslog 干净,还能按子系统和级别过滤。
1kxdevice ostrace --subsystem com.example.myapp --levels error,fault
syslog 输出整机的系统日志,量很大,配合 findstr 过滤。
1kxdevice syslog | findstr MyApp
抓取崩溃报告并定位
App 在手机上崩溃后,系统会生成一份崩溃报告。按 F5 时提示 didn’t start in time,通常也是 App 一启动就崩溃了。先列出手机上的报告,再下载到本地。
1kxdevice crash list MyApp*
2kxdevice crash pull .\crashes MyApp*
下载的 .ips 文件用文本编辑器打开,按下面的顺序看。
- 异常类型。
EXC_BAD_ACCESS多是访问了已释放的内存或空指针,EXC_CRASH (SIGABRT)多是未捕获的异常或断言失败,EXC_BREAKPOINT在 Swift 里常见于强制解包 nil、数组越界。 - Last Exception Backtrace。有这一段说明是 Objective-C 异常,这里的调用栈比崩溃线程的更有用。
- 崩溃线程的调用栈。找到第一帧属于你的 App 的位置,那里通常就是出问题的代码。
调用栈里你的 App 只显示地址、没有函数名时,最直接的办法是用 Debug 配置按 F5 复现一次。调试器会停在崩溃的那一行,调用栈和变量都能直接看,Objective-C 异常再配合上面的异常断点。
不想记命令时,执行 kxdevice web 后用浏览器打开 http://localhost:8780,崩溃报告、系统日志、进程列表都能在网页上查看。
附加到已经在运行的 App
有些问题要操作一段时间才出现,重新启动就不好复现。这时让 App 继续运行,把调试器附加上去。
在命令面板运行「KXBuild: 附加到运行中的 App」,会列出手机上正在运行的应用,选一个即可。附加后断点、变量、调试控制台和前面讲的一样。附加的 App 必须是用开发证书签名的,App Store 下载的 App 不能附加。
常见错误
| 提示 | 处理 |
|---|---|
| No iOS device selected | 在 KXBuild 视图中选一台设备 |
| Developer Mode is off | 手机上打开开发者模式并重启 |
| Developer Disk Image is not mounted | 重新插拔手机,或运行体检 |
| 设备尚未配对 / 信任 | 解锁手机并点信任 |
| did not report the app as launched in time | 解锁手机,检查数据线连接 |
| didn’t start in time | App 可能一启动就崩溃,按上一节抓取崩溃报告 |
| 断点不停,或只停在汇编上 | 换成 Debug 配置,等符号库下载完成 |
签名和安装类的报错见iOS 签名报错与 ipa 安装失败的解决办法,其他错误见排错与 FAQ。