大多数浏览器和
Developer App 均支持流媒体播放。
-
通过 Xcode 和 LLDB 进行高级调试
探索用于增强 Xcode 调试工作流程的高级技巧及提示和窍门。了解如何利用 LLDB 和自定断点来增强调试功能。充分利用 Xcode 的视图调试工具来更加高效地解决 app 中的 UI 问题。
资源
相关视频
WWDC22
WWDC21
WWDC19
WWDC18
-
下载
(使用XCODE和LLDB 进行高级调试) (演讲412)
大家好 欢迎来到“使用Xcode 和LLDB进行高级调试” 我是Chris Miles Xcode团队的工程经理之一 今天我非常高兴能够来到这里 我知道你们已经对稍后的啤酒狂欢节 早已迫不及待 所以谢谢你们的光临 我们会及时结束 但今天我们为你提供了 许多精彩的内容 所以我们马上来看看吧 我想先谈谈Swift调试的可靠性 这里的重点是我们带来了好消息 我们团队已在Xcode 10中 修复了很多可靠性问题
谢谢
编译器和调试器团队已经解决了 许多导致Swift调试 令人头痛的问题 我想告诉你们其中几个
在某些情况下 通常是在更复杂的项目或构建配置中 尝试po一个对象 或在控制台中执行一条表达式 可能会失败 并产生这样的错误信息 此错误所说的AST上下文 是一个表达式上下文 LLDB需要它来恢复你构建项目时 编译器的状态 而在某些情况下 比如当模块有冲突时 这时表达式上下文不能可靠地重建 你的表达式就会失败 在Xcode 10中 LLDB实现了回退机制 以处理这种情况 如果它不能重建上下文 它会回退到创建一个 当前帧的更简单的上下文 并使用它来评估你的表达式
一些开发者可能遇到的另一种错误 是由于调试器无法在调试时 实现某些变量类型 所造成的 这在Xcode中看起来像这样 在左侧 你可以看到变量视图 显示了当前帧中所有变量的名称 但不会显示任何类型信息或值 尝试打印出变量的值 可能会失败并产生像这样的错误
现在 再次感谢你们的漏洞报告 我们团队已经能够追踪并修复这些 调试信息不能可靠生成的边缘案例 所以我想再次代表团队感谢你们 在遇到调试问题时 能够提交问题报告 如果你在调试项目时 发现Xcode 10 还有其他问题 请继续为我们提交错误报告 对于参加这次演讲的你们来说 如果你发现项目存在问题 请到实验室来 明天早上我们有一个 Xcode调试和分析实验 从九点到十二点 带上你的项目并找到 Xcode调试工程师 或LLDB工程师 他们很期待看到你的项目
现在我想继续告诉你 一些我最喜欢的调试技巧和窍门 我喜欢用它们来增强我的调试流程 事实上 我不只是要告诉你 我还想为你演示一下
我们今天要使用的项目是一个叫做 Solar System的iOS app 你可能见到过 Solar System出现在 Keynote演讲 和“国情咨文”中 我们将要调试 Solar System中的一个部分 其被称为Moon Jumper Moon Jumper 允许用户握住手机 并跳到空中 该app可衡量你的跳跃力量 然后将其转换为月球重力 并向你展示如果你站在月球上 你刚才跳跃的高度 你可以设置一条栏来作为目标 然后你可以试着挑战自己 尽量在月球重力下 跳到该栏的高度
现在我们对Moon Jumper 进行一些改进 如一些视觉增强 和GamePlay模式 我们还没有准备好交付 我们已经做了一次测试 并得到一个漏洞列表 我们需要仔细看看它 所以这些都是 Solar System的漏洞 我会先解决iOS上的漏洞 稍后 Sebastian将上台 解决macOS端的漏洞 现在 正如它所说的 谁都不能离开去参加啤酒狂欢 直到我们修复所有这些漏洞 所以没什么比与2000人结对编程 更令人振奋的事了
让我们马上动手 并从第一个漏洞开始 跳跃失败动画与规范不符 那是在说什么? 我们切换到模拟器 因为我们正在使用模拟器 来加快调试和开发 我连接了点击动作来识别它们 所以每次我点击宇航员 他都会成功跳到栏的高度 现在这个漏洞说的是 当宇航员没有达到 栏的高度的情况 所以让我们重现一下 切换到编辑器 我将使用跳转栏导航到jump函数 并在该函数的开始处设置一个断点 现在我点击宇航员 我们将进行一次跳跃 现在我们在调试器中暂停了 要指出的第一件事是在标签栏中 这里有四个选项卡 这个调试选项卡是由Xcode 刚为我们创建的 对于那些像我一样 喜欢在选项卡中工作的人来说 这是你可以定义的Xcode行为
为了做到这一点 你需要使用 Xcode菜单来编辑行为 这将你带到Behavior 选项卡中的首选项 在这里你可以配置许多行为 在这个例子中 你需要配置Running部分中的 Pauses的行为 这就是 当Xcode在调试器中暂停时 所触发的行为 你可在此看到我已配置它 显示名为Debug的选项卡 当在调试器中暂停时 Xcode将始终 切换到该选项卡 对于像我一样喜欢在选项卡中 工作的用户 这非常棒 现在切换回代码 我们可以看到有一个条件语句 基于这个 didReachSelectedHeight属性 所以我想看看这个属性的值 我们切换到调试控制台 我可以使用po命令来查看 该属性的值 它当前的值为true
现在我想… 点击动作只是识别我已连接 并且总是 将其值设置为true 我想将其更改为false 以便重现该错误 现在我们在代码中对其进行修改 点击以识别它 并将其设置为false 但我不想仅为了调试目的 而对我的代码进行更改 如果我能避免的话 所以在这种情况下 我可以让调试器为我做这件事 我可使用 expression命令 我可以给它任何Swift表达式 例如 didReachSelectedHeight=false 它会评估并执行它 现在我们可以看到这个属性 确实已经变成了false 如果我使用调试器单步执行这行代码 可以看到 我们已进入了 分支的else语句块 当我们继续运行时 我们可以看到宇航员 无法跳到栏的高度并掉落下来 这正是我们试图重现的情况 我希望每次点击宇航员时 都会发生这种情况 但我不想每次都暂停 并输入这个表达式 所以我要做的就是配置这个断点 来为我做这件事 如果我右键点击断点 我可以选择 Edit Breakpoint 这将显示一个弹出窗口 用一些配置来定制断点的行为 我将Action设置为 Debugger Command 并输入表达式命令 即我在调试控制台中使用的命令
并使其成为一个自动继续断点 我们在这里配置的是一个断点 一旦执行到这个函数 该断点将被触发 为我们执行这个命令 更改属性的值 然后自动继续执行 现在每当我点击宇航员时 他都会执行这个不成功的跳跃并回落 现在我们需要解决什么问题呢? 规范说 跌倒后 宇航员应该再次站起来 现在我们来修复它 我将导航到此函数 updateUIForJumpFailed 我们可以看到这个函数 使用UIKit Dynamics 来模拟失败跳跃 它首先创建一个 UIDynamicAnimator 然后调用一个函数来添加行为 以创建物理效果 然后宇航员应该在 dynamicAnimatorDidPause代理回调中 被重新调整方向并定位到屏幕中间 现在我们可以向下滚动 并看到委托回调 已成功实现 这部分看起来很好 但我注意到这个对象没有设置委托 所以如果我在这里添加该代码 我认为这个更改可以解决我们的问题 现在我可以重新编译并运行 并尝试和验证我的修复 但如果可以的话 我想尽量缩短整个周期 所以我要做的是使用一个断点 来为我注入这个变化 以便我可以快速方便地看到 这是否可以解决问题 我在这里创建一个断点 双击打开编辑窗口 这是调出编辑窗口的快捷方式 然后再次使用值为 Debugger Command的Action 来注入一个表达式 输入我认为能解决问题的 那行代码 并使其成为一个自动继续的断点 我在这里进行配置 尽管我已经修改了代码 但我还没有重新编译过 我通过使用自定义断点 来为我注入更改 因此我可以使用 当前的实时app进行测试 如果我现在点击宇航员 他执行了这次不成功的跳跃并摔倒 然后重新站起来 我们似乎已经解决了这个问题 我喜欢这个效果 我打算再做一次
现在我们再看看笔记 我们可以勾掉第一个 漏洞表示已修复 没有比勾掉已修复的漏洞更好的事了 接下来的三个漏洞 是关于新的GamePlay模式的 我一直在研究它 我在模拟器中 点击Play向你展示它 这里的挑战是 尝试跳过栏10次以上 栏一开始较低 随后每次升高一些
你还可以看到 顶部添加了一些分数标签 如果我点击宇航员 你会看到他仍然被配置为 执行不成功的跳跃 这时顶部的Attempts标签 应该增加 但它没有 这是我们在此遇到的 第一个漏洞 这个标签闪了一下但没有改变 我们也遇到了游戏结束状态 处理不当的问题 及Scores标签 和Attempts标签布局问题 我们稍后再解决它们 回到这个漏洞 如果你没有注意到 让我再次点击宇航员 并密切关注顶部的 Attempts标签 你会看到它闪了一下但没有更新 所以这表明标签正在获取一个值 因为我们看到了过渡动画 然而值不正确 所以我想找到 修改此标签的代码 来看看它的逻辑是什么 我们注意到用户界面标签正在尝试 改变文本属性 所以我要做的是切换到断点导航器 并点击底部这里的加号按钮 来创建这些专门断点之一 你可以看到这里有 Swift Error Breakpoint 和Exception Breakpoint 甚至Test Failure Breakpoint 但在这个例子中 我将使用Symbolic Breakpoint 这样就创建了一个新的断点 并启动了编辑器 在这里我们可以输入任何函数 或方法名称 如我们在这里用到的 UILabel setText 且我们用Objective-C格式输入 因为UIKit是一个 Objective-C框架 需要指出的是断点下面 有一个子行 这是来自调试器的反馈 它告诉我们 它能够在UIKit Core中的 一个位置 解析这个断点 某些符号可能会解析到多个位置 它们都会显示在这里 如果你没有看到子条目 这表明调试器无法解析你的断点 因此它永远不会命中 准备好这些以后 我再次点击宇航员 现在可以看到 我们在 UILabel setText中命中了该断点 然而我们没有UIKit的源代码 所以我们看到的是汇编代码 但没有必要留在黑暗中 即使你处于一个框架的汇编代码中 并且是系统中的的一个框架 我们可以检查传入函数的参数 你只需要知道该架构的调用约定 就可以检查寄存器以查看参数 我承认我永远记不住 那些寄存器是什么 但幸运的是我不必这样做 因为调试器提供了 伪寄存器 现在 $arg1代表保存了 第一个参数的寄存器 现在我们可以看看 该Objective-C 消息的接受者 这是一个UILabel实例 我们看到它的值为“17 ft” 这表明它是这里的这个高度标签 因此它不是我们感兴趣的标签 我们再来看看其他的参数 如果你熟悉 objc_msgSend 你可能会记得第二个参数 应该是选择器 可我们并没有看到它 那是因为 LLDB不会自动知道 这些参数的类型 因此在某些情况下 我们需要对它进行类型转换 现在我们看到了这个消息的选择器 第三个参数是传递到方法中的 第一个参数 换句话说 它是传递给setText的字符串 所以这对于在一个框架的汇编帧中 检查参数来说非常方便 这也不是我们感兴趣的标签 所以让我们点击继续 现在我们再次命中断点 我们可以检查$arg1 看看接收者是什么 看起来它是同一个高度标签 其值现在为“0 ft” 现在我知道我的策略出了什么问题
当宇航员跳跃时 代码实时更新高度标签 所以断点会频繁地命中这个对象 这将花费我们很长时间 要命中一个Attempts标签的 UILabel setText 断点是非常困难的 所以我认为我应该做的 只是将这个符号断点 设置在跳跃动画完成后 有一个方法可以做到这一点 我将切换到断点导航器 如果我双击符号断点的指示符 就可以重新调出编辑器 我们可以使用这个 Condition字段 我们可以在这里输入一个 返回true或false的表达式 只有当该表达式计算结果 为true时 才会触发断点 因此如果我们有一个属性 如jumpAnimationInProgress 我们写一个表达式 测试它是否为false 然后断点就会被触发 然而我没有该属性 我要告诉你一个不同的方法 我将删除该符号断点 并向下滚动到此函数 jumpCompleted 并在这里设置一个断点 jumpCompleted函数 将会在动画完成后被调用 以便它可以更新用户界面 并更新游戏状态 然而我们不希望在这个函数中中断 我想要做的只是配置这个断点 并让它在UILabel setText处 替我设置一个符号断点 我可以通过添加一个 Debugger Command的Action来做到这点 命令为breakpoint set 参数为--one-shot true 这个一次性断点是一个临时断点 它一旦触发后就会被自动删除 我们给它起一个有意义的名字 UILabel setText 并使其成为自动继续断点 这里我们创建了这样一个断点 即当执行进入 jumpCompleted函数时 它会替我们在我们感兴趣的位置 设置一个临时断点 然后继续执行 从而使我们只会在进入这个函数之后 才命中setText断点 让我们点击继续 我们看到跳跃动画 在模拟器中完成 并且现在我们在UILabel setText处 触发了断点 现在我们可以 通过使用po $arg1命令 看看该消息的接收者 我们看到这确实是 一个不同的UILabel实例 其值为0 因此它很可能是 顶部标签其中的一个 看起来我们找到了正确的对象 我们来看看修改这个标签值的代码 我们可以在调试导航器中执行此操作 通过选择栈中的下一帧 现在我们已经找到了 修改标签值的代码 它当前使用labelText变量 传入一个“0”字符串 往上看 我们看到 labelText始终被设置为 标签的当前值 难怪它不会改变 看起来valueText才是 包含新值的变量 所以可能只是打错了 让我们解决它
我们将它改为valueText
接下来我想做的 不是重新编译并运行以测试此更改 我想像之前一样 在当前运行的app上下文中 直接测试这个改变 所以我要在当前行的下面 添加一个断点 请记住 我们已经更改了代码 但我们没有重新编译 所以标签仍会被设为原来的样子 我们在下面添加我们的代码 将其设置为我们认为正确的值 设置另一个自定义断点 我们可以再次使用表达式 来注入该代码并使其自动继续
现在如果我点击继续 代码执行将经过这里 我们看到Attempts标签 确实已更新 我想确保它… 谢谢大家
我想确保它在一次成功跳跃后 也适用于Score标签 让我们回到这里 先移除jumpCompleted中的 替我们创建 一次性断点的这个断点 因为我们不再需要它了 我还要禁用jump函数中的断点 因为我们不想再改变 didReachSelectedHeight了 现在当我点击宇航员时 他进行一次成功的跳跃 我们可以看到所有标签都正确更新 这样看起来很棒
让我们回头把它勾掉
好的 接下来的漏洞是一个 关于游戏结束状态的问题
游戏本应该在10次尝试后结束 我可以点击宇航员并等待动画播放 持续这个过程直到那个状态 以尝试重现它 但播放动画需要一些时间 当测试并验证我的修复程序时 我可能需要多次这样做 因此我想跳过所有的跳跃动画 我们来看看如何做到这一点 我们导航到 updateUIForJumpSucceeded函数 我们可以看到这个函数 修改了一些颜色 然后调用了 jumpAstronaut(animated:true) 所以看起来我只需要调用 jumpAstronaut(animated:false) 我可以更改代码并重新编译 但正如我之前所说的 我不喜欢为了调试目的 而改变我的代码 如果我能避免这样做的话 所以我们来看看我将使用的技术 我在这一行上设置一个断点 让我们清除调试控制台 并通过点击宇航员 进行一次新的跳跃 现在我们在这一行暂停 我需要让调试器 将这行代码替换为 jumpAstronaut(animated:false) 然而代码已经编译过了 我们无法取代它 但我们可以做的是 让调试器跳过这一行 不执行它 而是跳过它 然后我们可以使用表达式来注入 我们所希望的调用 那么我们如何跳过一行代码呢 我们把注意力放在这个绿色的 Thread 1 注解标签上 我们称之为指令指针 它指向包含 接下来会执行的指令的行 这里的这个看起来像握柄的图标 实际上就是一个握柄 按住鼠标就可以移动它 这样我就可以在暂停时更改指令指针 我可以把它往下移动一行 然后放手 接着Xcode 会显示一条可怕的消息 它所说的基本就是 巨大的权力会带来巨大的责任 说实话 这是我今天告诉你们的 最危险的功能 这项操作是有风险的 因为调试器允许你移动指令指针 到你想要的任何地方 它并不关心 但它不能保证app的状态 完好无损 例如 这可能会造成内存管理问题 当你试图引用尚未初始化的对象 或过早释放对象时 但既然这是高级调试演讲 所以我们知道 我们在做什么 让我们按下蓝色按钮
现在我们跳过了那行代码 在控制台中 我们可以使用expression命令调用 jumpAstronaut(animated:false)
现在我们点击继续 来看看这一切是否奏效 游戏状态的确已更新 而且我们跳过了所有跳跃动画
我希望每当我点击宇航员时都能这样 所以我要配置这个断点 来为我做这件事
首先我们需要一个 跳过一行代码的调试器操作 它的命令是 thread jump 再给它一个参数 --by 1 这告诉调试器为当前线程 跳过一行代码 然后我们只需要调用表达式 我们可以通过点击加号按钮来 添加另一个命令操作
expression jumpAstronaut(animated:false) 然后将其设置为自动继续 我们这里有一个断点 当运行到此行 但在执行此行之前 断点将被触发 它将执行跳过该行的命令 然后使用表达式来调用 我们想要调用的函数 现在 如果我们点击宇航员 我们可以快速推进游戏进展 并跳过所有动画 从而轻松地重现我们的漏洞 所以正如我所说的那样 游戏本应该在10次尝试后结束 我们已经远远超过了它 让我们来看看游戏状态 它们都被保存在上面这个属性中 即gamePlay 所以我会在该属性上设置一个断点 并执行一次新的跳跃 现在我们在对该属性的 下一个引用处暂停 我将使用po命令来查看 该对象的当前状态 这里我们看到对这个 GamePlay对象的调试描述 这里有一个自定义的调试描述 所以值得指出的是 po命令能够获得 对象的编程调试描述 而且你可以自定义该描述 来看看我们对GamePlay 做了什么 如果我切换到源代码 并滚动到底部 你可以看到我们添加了一个扩展 以使GamePlay符合 CustomDebugStringConvertible约束 该约束要求你实现 一个名为debugDescription的属性 并返回一个字符串 你可以返回你喜欢的 任何用于调试的字符串 来表示这个对象 你也可以通过实现 debugDescription 来对Objective-C对象 执行相同的操作 将其与命令 p GamePlay对比
p是另一种… 或LLDB命令 它使用LLDB的内置格式化程序 来表示对象 因此这里我们看到了 同一个对象的两个表示 并且默认格式化程序向你显示 完整限定类型名称 内存地址 以及所有属性及其值的列表 我们可在此看到 有一个maxAttempts属性 其值被正确设置为10 因此可能某处存在逻辑错误 在attempts的值增加后 没有正确判断出 它已经超过了最大值 所以我想找到修改 attempts值的代码 来看看其中的逻辑 我将打开变量视图并展开视图控制器 以查看其所有属性 在底部我将在过滤器中 输入GamePlay 来找到这个属性 展开它的属性列表 然后选择attempts属性 在这里打开其上下文菜单 并选择 Watch “attempts”
它所做的是创建一个所谓的观察点 在断点导航器中 在所有断点下面 你会看到有一个叫做 Watchpoints的新组 我们有一个attempts观察点 观察点就像一个断点 但它会在下一次变量的值改变时 暂停调试器 现在我们可以移除这个属性断点 因为我们不再需要它 然后点击继续 现在我们已经在这个观察点停了下来 并且我们找到了 修改attempts变量的代码
我现在禁用这个观察点 因为我不再需要它 我们看看这里的代码 当游戏正在进行时 增加attempts 如跳跃成功则增加score 我没有看到任何会检测 attempts是否已超过最大值 并转换到游戏结束状态的逻辑 所以我认为这就是需要改的地方 但在我实际更改任何代码之前 我想先测试我的假设 所以在我更改任何代码之前 我要创建一个断点 并配置它注入该更改 以查看它是否修复了问题 我可以再次添加一个 调试器命令行为 和一个表达式 我们要的是如果attempts 大于等于maxAttempts 则将游戏状态更改为已结束 并将其设为自动继续 所以现在只需按下继续按钮 来测试一下 是否真正解决了问题 程序执行到这个断点 注入代码 我们可以看到 它确实修复了问题 我想从游戏开始时验证一下 我可以点击Play again 快速轻松地完成此操作 并迅速进行10次尝试 在第十次我们看到它确实检测到 游戏结束状态 看起来这正是我们需要的修复
别忘了将更改应用于你的代码 我将代码复制出来 拖动断点以删除它 然后将代码粘贴进去 看起来不错 我们来勾掉这一项 这个部分我们只剩下一个漏洞了 即Attempts 和Score标签的布局 现在这个app的布局工作 被留给了工程师们 就像优秀的工程师一样 我们找到了一个好位置 将它们填到顶角 但是团队认为这不太合适 他们将app退回 并要求我们再试一次 所以我想模拟出 这些分数标签的新布局 现在我可以打开我的图形app 并开始建模了 但我是一名工程师 我喜欢用代码建模 我其实是调试工程师 所以我喜欢用实时app 和真实数据在调试器中建模 我们看看如何做到这点
让我们导航回去 并在jump函数中设置断点 我们首先需要找到一个 可与其交互的视图的引用 所以我要清空这里并打开它 进行一次新的跳跃 然后调试器在 视图控制器内的这个 jump函数中暂停 所以如果你有一个视图的 属性或输出口 那么这是会一个很好的引用 但如果你没有 那你需要获得视图的内存地址 因此让我向你展示一些 查找内存地址的方法 以及如何仅通过内存地址来操作视图 像我们之前说的那样
调试描述包含一个自定义描述 所以看一下视图控制器的视图 我们可以看到UIView的 默认调试描述 其中包含视图的类和内存地址 所以一种方法就是 获取对象的调试描述 由于我们有一个属性指向它 所以很容易得到这个信息 但这个视图控制器视图 下面的所有视图该怎么办呢? 我们需要查看视图层次结构 一种方法是使用这里的这个按钮 它会调用Xcode的 可视化视图调试器 它将为层次结构拍摄快照 并为你提供3D分解视图 你可以用它来检查视图 Sebastian稍后 会详细讨论这个问题 所以我们来看看另一种方式 这对于更简单的层次结构很有用 并且只需在调试控制台中操作 这就是使用UIView上的调试函数 recursiveDescription
我们应该可以调用 po self.view. .recursiveDescription()
但这行不通 为什么呢? recursiveDescription 仅用于调试目的 它不是公共API的一部分 因此也不会被Swift扫描 所以… Swift是一种严格的语言 不允许你调用 尚未严格定义的函数 然而Objective-C不同 在Objective-C世界中 代码可以自由放纵的运行 你可以做任何你想做的事 我的意思是它是一种动态语言 所以你可以调用这样的函数 所以我们要做的 是告诉调试器使用 Objective-C语法 来评估这个表达式 实现这点的方法是 使用expression命令和选项参数-l objc 这表明你即将 给expression命令 一段Objective-C代码 即使你处于Swift框架中
我们再加上-O 来告诉它我们也想要调试描述 就像用po一样 再加上--以表示没有更多选项 该行的其余部分仅仅是 原始表达式输入 所以我们应该能够给它 这个方法调用的 Objective-C格式 不幸的是 这并不奏效 其原因是这个表达式 将为Objective-C 编译创建一个临时表达式上下文 并且它不会继承 Swift框架中的所有变量 尽管如此 还是有办法的 如果我们把self.view 放到反引号中 反引号就像预处理器一样 它表示先评估其在当前帧中的内容 并插入结果 然后我们可以评估其余部分 现在我们可以得到递归描述了
现在我们就可以看到所有视图的 调试描述 我对ScoreboardView 很感兴趣 它承载着这些标签 我们可以找到其中之一的内存地址 现在我们可以使用 po内存地址 如果你是Objective-C开发者 你可能熟悉该命令 这也行不通 那是因为Swift 不会将数字视为指针 并为你解引用 所以需要在Objective-C 上下文中做到这一点 我们可以做跟刚才同样的事情 但我觉得这样更方便 我喜欢将其简化为 一个简单的命令 所以我打算通过使用命令别名 来做到这一点 我把这个命令称为poc 我已创建了一个别名 我能简单地poc那个内存地址 并查看该对象的调试描述 我想向你展示另一种方式 来查看对象的描述 当你只有其内存地址的时候 在Swift中你可以使用一个叫做 unsafeBitCast的函数 给它一个内存地址 它不安全是因为 它依赖你来提供正确的类型 因此我给它 ScoreboardView.self 现在我们看到我们可以使用 unsafeBitCast函数来查看一个对象的 调试描述 unsafeBitCast函数 的一个好处是 它返回一个经过类型转换的结果 所以我们可以调用它的函数 和属性名称 例如.frame 我想看一下它的中心点 然后修改该中心点 我们将其改为300 现在我们可以看到 它已经变成了300 但模拟器中的视图并没有移动 为什么会这样呢? 我们在调试器中暂停了 所以Core Animation 目前不会将任何视图模块更改 应用到屏幕的帧缓冲区 但我们可以要求Core Animation 为我们做这件事 只需使用表达式 CAtransaction.flush()
这告诉Core Animation 更新屏幕的帧缓冲区
现在我可以用这两行代码 来修复新的位置 继续刷新屏幕 并四处移动该视图 实际上 我觉得将这些都包装到 一条命令中来移动视图会很方便 所以我将这么做 我们来看看 我将切换到终端 并打开一个Python文件 为什么使用Python文件呢? LLDB可以使用 Python编写脚本 你可以完全访问LLDB API 我创建了LLDB Python 脚本实现一个nudge命令 它需要一个x偏移量、一个y偏移量 和一个视图表达式 你可以在调试器中暂停时 使用它来移动视图 它可能看起来很长 但大部分都是参数解析 其位于中间的核心部分只是在调用 我们原来手动调用的表达式 不幸的是我们没有时间 详细研究这个脚本 但我们会将其放到网上以供下载 以便你可以看到它是如何工作的 并以此为基础 开发你自己的自定义调试命令
让我向你展示如何
启用这样的脚本 只需编辑你的主目录中的 .lldb文件… 并添加一行 command script import 我也想添加一些我觉得方便的别名 比如我之前创建的poc别名 和一个用于刷新事务的别名 我想我会记住这个
我将复制command script import 以便我们可以直接将其粘贴到 调试会话以免重新启动该会话 现在我们有了一个 叫做nudge的命令 现在我可以 比方说 水平移动0点 垂直-5点 为其提供一个视图的内存地址 并开始在模拟器中四处移动它
关于LLDB的一个很棒的地方在于 如果你只是在空行上按回车 它会重复执行上一行 所以非常适合移动 我可以将它稍微推向右侧 让它看起来更好一点 然后我们来移动另一个视图 我们可以给它任何视图表达式 比如这个attemptsView nudge的另一个特点是 一旦你给它一个视图表达式 下次你就不必重复它
它会记住该视图并将命令应用于
你上一次指定的视图 这样看起来很好 这是一个比以前更好的布局 现在我能做的就是获取 nudge提供的信息 例如应用于该视图的相对于 其原始中心点的总偏移量 然后是你的框架值 我可以回到代码并修改布局代码 或我的自动布局约束 我轻松地为自己的场景 构建了一种新的布局
最后要做的事情是 首先别忘了勾掉该漏洞 这非常重要
然后在重新启动或编译运行之前 要做的最后一件事 是禁用或删除任何 注入表达式的断点 因为你不希望这些代码被执行两次 选择它们或它们所在的组 并点击删除 是删除这些内容的快速方法 这些都是一些调试技巧 我喜欢用它们来增强 我的调试工作流程
请注意我们如何诊断和修复 所有四个错误 而无需重新编译或重新运行 这可以节省大量的时间 特别是对于复杂的项目 并且在尝试解决难以重现的漏洞时 可能至关重要 非常感谢你们与我结对编程 希望你们喜欢它 并且可以在调试中 使用这些技巧
我想快速回顾一下 刚才谈到的所有的 功能和技巧 首先 我们看到了 如何使用Xcode行为 来调出一个专门调试选项卡 以及如何使用LLDB表达式 修改程序状态 我们可以在调试器命令中 使用自动继续断点 来实时注入代码 我们还可以使用命令 breakpoint set --one-shot 创建依赖断点配置 以作为设置另一个断点的 调试器命令行为 即使在汇编帧中 我们可以很容易使用 po $arg1 $arg2等 来检查函数的参数 我们也可以通过拖动指令指针 或使用命令thread jump 来跳过代码行 我们可以在使用观察点的变量 被修改时 请求调试器暂停
我们甚至可以使用 表达式expression -l objc 来在Swift框架中 执行Objective-C代码
我们可以使用 expression CATransition.flush() 将视图更改直接刷新到屏幕上 即使是在调试器中暂停时 你还可以添加自定义LLDB命令 无论是给常用命令设置别名 还是使用 LLDB Python脚本 完全自定义并创建自己的命令 别忘了访问我们的演讲网站 我们会尽快发布这个nudge脚本 以便你可以下载它 研究它并将其用作 你自己的命令的基础
还有一件事我想和你们提一下 就是当前的LLDB打印命令 你可能已经很熟悉po了 在演示中我们多次用到它 我们看到po 会请求对象的调试描述 而且你可以自定义该描述 那是因为po只是一个 以下命令的别名 expression --object-description
或expression -O 与p命令相比 p只是expression命令 的别名 它使用LLDB的内置格式化程序 来显示对象的表示
需要知道的第三个命令是 frame variable 它与前两个的不同之处在于 它完全不需要编译 和评估表达式 它只是直接从内存中 读取变量中的值 然后使用LLDB的内置格式化程序 所以选择使用哪个命令 其实取决于个人喜好 以及你希望在调试时看到的信息类型 但重要的是要记住 如果你遇到这种情况 即当表达式失败时 po和p可能无法工作 如果你需要检查当前帧中的变量 那么frame variable 应该还能正常工作
接下来 我想将话筒 交给Sebastian 他会告诉你一些高级视图调试技术 谢谢
谢谢你 Chris
我很高兴能够向你展示… 关于如何充分利用 Xcode视图调试器的提示和技巧 我们也会看看我们 为Xcode 10所做的改进 为采用macOS Mojave 暗色外观的Mac app 的调试提供更好的体验 我们将在一个演示中看到这点
让我切换到演示机
我将使用与Chris刚才使用的 相同的项目 并且你已经看到 我们还有两个错误需要解决
但是我不打算使用iOS app 我将使用Mac app 我们可以在这里看到 Solar System app的Mac版本 它在暗色模式下看起来很不错
但有两个漏洞我们需要解决掉 首先 星球图像没有正确水平居中 这是一个非常明显的漏洞 你可以看到在右边 这张地球图片 被移到右侧 我们将会看看这个问题 第二个漏洞是 在暗色模式下 弹出框中的文本不可读 我们来看看这是指的是什么 当我切换到这个app时 我可以显示轨道细节 信息显示在弹出框中 你可以看到顶部的标签清晰可见 然而底部的标签很难阅读 我必须通过选择文本来阅读它 所以这些是 我们必须要解决的两个漏洞 让我隐藏待办事项列表 然后我们开始吧 所以我要使用 Xcode捕获该app的 视图层次结构 然后检查它 我们会定位问题 然后希望能够修复它 我们就能去喝啤酒了 问题是当我切换到Xcode 以捕捉视图层次结构时 这个弹出框将会消失 因为app进入了后台 因此我们将无法捕获其视图层次结构 我们必须要在app处于活动状态时 来对app进行捕捉 我会向你展示做到这点的两种方法
你可以看到当我切换到Xcode时 这个弹出框如何消失
首先 我们可以使用触摸栏 我会通过从Xcode的窗口菜单 调出触摸栏模拟器 来向你展示它的样子 我将切换回 Solar System app 并再次调出弹出框 然后看看触摸栏 你可以看到这里有个喷雾罐图标 当我在触摸栏上点击它时 你可以看到Xcode在其调试栏中 提供的该调试选项的子集 因此从触摸栏访问这些工具非常方便 如你所见 我可以在Xcode 处于背景中的情况下调出它们 因此你甚至可以在以全屏模式 开发app时访问它们 这其中一个选项允许我捕捉 视图的层次结构 我不打算这样做 因为我知道不是每个人都有 带触摸栏的Mac 所以我会告诉你另一种方法
我要关闭模拟器 我将使用命令点击 来点击Xcode的调试栏中的按钮
命令点击是一个系统手势 它允许你在不激活app的情况下 执行鼠标事件
所以这允许我们 调用对视图层次结构的捕获 调试器在app仍处于活动状态时 将其暂停 我们可以看到用户界面仍然显示 就好像app还在屏幕最前面 并且弹出框没有消失 如果你想知道 为什么那个旋转的光标会出现 这是因为app在调试器中暂停 并且不再响应鼠标事件
现在你可能在想 如果我们看看这里的视图调试器 为什么弹出框不可见呢 别担心 视图层次结构已被捕获 当我们谈到那个漏洞的时候 再看看该如何查看弹出框 但首先我想看看 这个ImageView的 布局问题 我先选择这个图像视图 让我放大一点 当我们看一下右侧的内容时 我们可以看到这是一个 _NSImageViewSimpleImageView 这个下划线前缀 通常暗示这是来自系统框架的内部类 而不是我们在代码或界面构建器中 设置图像视图时所使用的类 我们来看看这个对象 在视图层次结构中的样子 我可以通过点击 Navigate菜单并选择 Reveal in Debug Navigator 来做到这一点 我们现在可以在左侧 看到它以及它的父视图和子视图
我们能看到其父视图为 NSImageView 这就是我们正在寻找的 我们也看到其父视图 是PlanetGlobeView 以及PlanetGlobeView的父视图 是NSVisualEffectView
现在我要选择这个图像视图 我们可以在右侧看到 该图像视图的其他属性 我们来看看这个视图的布局 我在这个app中使用了自动布局 所以我想看看其自动布局约束 我可以使用这个按钮来显示约束
现在我们可以看到 会影响此视图布局的所有约束
你可以看到 例如 此处的宽高比约束 还有这条垂直线 这是一个对齐约束 当我选择这个约束时 我们可以在右侧看到其中所有属性 现在 如果你想知道 为什么视图调试器只显示线框 这是因为它只显示特定视图的内容 该视图需要处于所选视图的布局中 由于所有这些视图本身都没有内容 所以我们目前只显示线框
选择约束后 让我们来看看右侧的信息 我们可以看到 它与图像视图的水平中心对齐 也与PlanetGlobeView的 水平中心对齐 并且它以常量0进行对齐 所以它在其父视图中水平居中对齐 现在我们从调试导航器中 选中PlanetGlobeView 我们可以看到它向左侧稍大一些 但它在右侧对齐 所以这有点奇怪 因为我们刚刚看到了 能够正确对齐的约束 即准确的水平居中 但我们在视图调试器中 看到的却不是这样 我们来看看 PlanetGlobeView的约束 希望能够理解正在发生的事情 我在这里选择前沿约束 再次看看其信息 我们可以看到 PlanetGlobeView的前沿 与NSVisualEffectView 的前沿对齐 而后者是在父视图中的 所以它只是相对于它的父视图 插入进去 并且它以常量30这样做 这还算合理 然后这个后沿约束
将PlanetGlobeView 的后沿 与父视图的后沿对齐 它也以常数30这样做 现在这个约束 没有附加到右侧的任何内容 这有点值得怀疑 这让我在想 我们是否在此看到了整个图景 在这种情况下 通常有一个好主意 即查看是否有任何内容被裁剪了 而我们默认看不到它们 你可以使用这里的这个按钮 来显示裁剪内容 现在当我启用此功能时 你可以看到PlanetGlobeView 实际上向右延伸 超过了窗口范围 现在水平居中约束变得合理了 因为它实际上正确的在父视图中 水平居中了 只是父视图延伸到了窗口之外 这是一个很常见的问题 如果你在代码中设置约束 你不小心交换了第一个和第二个项目 从而导致布局方向错误 或者你不小心弄反了常量 在这个例子中 我们使用了30而不是-30 来将其插入左侧 所以我想要做的就是尝试反转常量 进行修复 我将使用 与Chris使用的相同技巧 简单的在LLDB中应用它
选中这个约束
再选择Edit->Copy 再调出底部的控制台区域 刚才我复制的内容 是所选对象的经过类型转换的指针 这适用于你在视图调试器中 以及内存图形调试器中 选择的所有对象 这使得在控制台中使用它们非常方便 让我们…
谢谢 让我们打印调试描述 我们可以确定这个常数的确是正30 这也是我们在信息栏中看到的 所以让我们将常数设置为-30 输入e 这是expression超短缩写 转换指针的类型 然后setConstant
为-30 我们遇到与Chris 在上个app中看到的相同问题 即app没有及时更新 所以我必须要做的是 执行一个命令 来让暂停的app
调度其用户界面的更新 我可以使用 Chris刚才添加的方便命令
我可以在这里找到命令 现在我们可以看到地球图像 正确地水平居中了 所以反转常量是正确的修复方法 现在我们能够确认这一点 所以让我们在代码中应用这个改变
选中约束后 你可以在右侧看到 这个回溯信息 那就是约束的分配回溯路径 它准确告诉我它被分配在哪个框中 所以它可以让我直接找到 创建该约束的相应代码 为了显示这个回溯信息 你必须启用 Malloc Stack日志 让我告诉你如何启用它 在此找到你的方案 然后选择Edit Scheme
并在Scheme Options的 Diagnostics选项卡中 在Logging选项处 可启用Malloc Stack日志 并选中 All Allocations and Free History 这将为你提供这些方便的 分配回溯信息 即视图调试器 和Xcode的内存图像调试器中的 所选对象的分配回溯信息
现在当我将鼠标悬停在这个堆栈上时 我们可以看到该框的全名 我们可以看到 这是SceneViewController中的 setupPlanetGlobeLayout方法 我可以使用此跳转箭头 跳到这部分代码
我通过按住Shift-Control-Option键 来做到这点 这将打开这个导航框 并允许我在新窗口中打开此文件
现在你可以看到分配约束的这行代码 被高亮显示了 我们看到值为30的常量 我可以将其反转为-30 保存该文件并关闭它 我们解决了第一个漏洞
完美 第二个漏洞是我们无法看清 弹出框中的描述 让我们来看看这个
首先我想禁用约束模式和裁剪 以便我们可以看到内容
我也将清除控制台
我一开始向你展示了如何在这款 Mac app处于活动状态时 进行捕捉 这样我们就可以在弹出窗口打开时 看到其视图层次结构 然而我们没有看到它 这是因为视图调试器 一次只显示一个窗口 我们来看看如何查看其他窗口 当我在视图层次结构中向上滚动 并最终找到当前窗口时 我们可以看到它在窗口控制器下面 如果我在这里折叠这个根项目 我们可以看到实际上有另一个根项目 这正是我们正在寻找的 我们的弹出窗口 所以如果你的app有多个窗口 macOS和iOS也是如此 它们会在左侧的导航栏中 显示为多个根级别的对象 所以如果你认为你的app 包含多个窗口 那么就看看那里 它们会被视图调试器所捕获
我们可以在3D模式下看看它 我们可以看到这个大视图 阻挡了我们对标签的点击 我想看看这些标签以检查它们 让点击穿过视图调试器中的视图 有一个技巧 你只需按住Command键 现在我可以穿过前面这个视图 并点击选中这个蓝色标签 我们来看看这个标签的文字颜色 我想先看看在暗色中 看起来很棒的那些标签 以便我们可以为底部的问题标签 找出解决方案
我们来看看文字的颜色 我们可以看到这是一种RGB颜色 效果为这种蓝色 我们也可以看到信息栏向我们提供了 这种颜色的名称 这表明这种颜色 来自我们项目中的素材目录 在Xcode 10中 你可以提供你定义的 单个颜色的多种颜色变体 例如 你可以提供一种用于浅色 另一种用于暗色 并且使用哪种颜色变体 取决于视图的外观 你也可以 在信息栏中得到这些信息 向下滚动信息栏 到视图部分 你可以看到有Appearance 和Effective外观选项 现在Appearance选项 没有在这个视图上明确设置 这是一种非常常见的情况 因为大多数视图继承了 其任意一个父视图的外观 比如窗口或app 但是你可以在这里看到 所继承的有效外观 那就是VibrantDark 这将决定你在素材目录中 定义的哪种颜色会被使用
好的 事实上在信息栏的此处 我想指出 Description属性 这是该对象的调试描述 Chris先前向你展示了 如何为你的对象提供 自定义调试描述 所以你不仅受益于 当你在对象上使用po时 你的控制台会显示清晰的调试描述 你也可以从视图调试器中获益 因为它在信息栏中也会显示
好的 让我们回到文本颜色 我想选择第二个标签 因为它在暗色中看起来也还不错 这次我们仍可以看到 一个带有名称的颜色 它是labelColor 但它以System为前缀 这表明它不是 来自我们自己的素材目录 而是来自系统 当然 系统颜色 也会自动适应外观变化 现在看看这个有问题的标签 我们可以看到它是非常深的灰色 并且没有名字 这意味着它是一种自定义颜色 它不会适应外观变化 所以我们想要做的是改变它的颜色 将其文本颜色设置为系统颜色
所以选中这个对象后 我按下Command-C键 键入e命令 粘贴类型转换后的指针 并设置setTextColor
为NSColortextColor
我必须再次刷新才能看到效果 我们可以看到弹出框现在更新了 字体很好看并且可读性强
现在 我不打算在我的故事本文件中 应用此修复 但我想指出的一点是 你这么做很重要 即当你对用户界面进行更改时 你需要验证你的app 在所有外观中都仍然很好看 因为现在存在多种系统外观 我会告诉你如何做到这一点 我将继续运行
我并不需要将我的整个系统外观 切换为浅色 以查看标签在浅色背景下 是否仍然看起来不错 Xcode 10 为你提供了一种方法 来仅仅改变目标app的外观 你可以在调试栏中使用这个按钮 我可以在这里选择Light 你可以看到app 现在以浅色外观呈现 我可以调出弹出框 从而验证 其文本清晰可读 这证实我们的确 解决了我们的问题 由于以不同的外观查看你的app 是一个非常普遍的行为 我们实际上在触摸栏中提供了该选项 我马上向你演示一下 我将再次调出触摸栏模拟器 并打开弹出框
我可以选择这个选项
你现在可以在触控栏中 看到所有的覆盖选项 所以即使你的app处于全屏模式 你也可以访问它们 让我切换到 High Contrast Light模式 它可以实现高对比度的无障碍功能 同时覆盖外观为浅色 这样你可以确保你的app 在该配置中也看起来不错 当然我也可以回到系统外观 我们确认了我们的问题已经解决
现在我可以在漏洞列表中勾掉它 就这样 我们完成了我们的演示 我会回到幻灯片
让我们回顾一下刚刚看到的内容 我向你展示了如何使用 Reveal in Debug Navigator选项 在左侧的层次结构中 定位你当前选中的对象 我向你展示了如何显示裁剪内容
并且我们使用了自动布局调试功能 来定位我们的约束问题 我向你展示了如何使用对象指针 要使用它你只需复制选定对象 并在LLDB中使用 我们看了一下构建过程回溯 如果你在Scheme Options中 启用了 Malloc Stack日志 就可以使用它 来直接跳转到代码 并应用约束所需的更改 我们看了一下调试描述 它在视图调试器的信息栏中 用起来很方便 我们使用了点击量功能 来选中位于一个视图后面的视图
关于调试暗色模式 我们看到你可以通过直接使用 Xcode的调试栏或触摸栏 轻松更改目标app的外观 我向你展示了如何捕捉 处于活动状态的Mac app 我们也看了一下命名颜色信息 及其NSAppearance信息 它们在Xcode的视图调试器的 信息栏中可见
若你想了解更多 关于在你的Mac app中 使用暗色模式的信息 请观看这两场演讲的视频
我们这次的演讲就到此结束了 若你想了解更多此次演讲的信息 包括Chris先前向你展示的 nudge脚本 它们会被发布在演讲网站上 如果你对本次演讲的任何内容 或一般性调试问题 有任何疑问 明天上午9点将有一个 分析和调试实验室 Chris和我会在那里 我们很乐意回答 你可能遇到的任何问题 另外如果你对内存调试感兴趣 还有一个“iOS内存深潜”演讲 它也在明天
最后 我希望你在啤酒狂欢节中 度过美妙的时光 并享受此次大会的其余部分
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。