作者:Puttin iOS开发者 在字节跳动工作
Sessions: https://developer.apple.com/videos/play/wwdc2019/429/
本文发表于《WWDC19 内参》 2019/07/01
简要介绍了 po, p 和 Xcode 10.2 新加的 alias v 内部的逻辑,以及作为开发者,如何自定义输出帮助你更好的调试。并额外附加一些技巧。
大多数 iOS 开发者都很熟悉在LLDB
中使用po
,把变量输出到 console 中。
实际上在较新版本的 Xcode 中有三种方式输出,每一种方式都有一些权衡。
三种输出方式
po
可以直接输出变量
也可以输出计算过后的
一般而言,只要能编译通过的表达式,都可以作为po
的参数。
别名
po
可以理解成 print object description(打印对象描述)。实际是一个alias
(别名),可以通过help po
查看:
`(lldb) help po
Evaluate an expression on the current thread. Displays any returned value with formatting controlled by the
type's author. Expects 'raw' input (see 'help raw-input'.)
Syntax: po
Command Options Usage:
po
'po' is an abbreviation for 'expression -O --'
`
help expr
可以看到 -O 是缩写:-O ( --object-description )
。所以,你也可以轻松自定义一个自己的 po :
command alias my_po expression --object-description --
过程
那么po
过程中发生了什么呢?
LLDB
会先把语句生成一小段代码
然后编译并执行,再生成取结果的代码
然后再编译并执行,拿到对应的结果,并显示出来
可以看到这个流程是相对较长的。
p
同样的例子,这次使用p
大多数内容和之前的po
并没有什么本质区别,但注意到有个$R0
,这是LLDB
给我们的结果设置了一个自增的名字。我们可以直接使用起了名字的变量:
和po
类似,p
也是一个alias,通过help p
可以查看。
过程
那么p的过程中又发生了什么呢?
实际上一直到取到结果这一步,p
和po
的行为是一模一样的。不同的是
p
使用了dynamic type resolution
(动态类型推断)。
让我们把例子稍微改一改:
在这个例子里,cruise
静态的类型是Activity
,运行时的实际类型是Trip
。
这时候如果我们p cruise
,得到的结果和修改例子之前并没有区别。因为LLDB
读取了代码的metadata
(元数据),去判断在特定时间点,特定变量的类型。
但动态类型推断只会发生在表达式的结果部分,所以如果尝试直接p cruise.name
,并不会成功:
之前提到过,得是一个能编译通过的代码。所以如果真的想要访问,只能显式类型转换以后,再访问。
其实,在动态类型推断之后,还有一步格式化:
这步会把从动态类型推断拿到的对象转换成人类可读的字符串。
expression --raw -- cruise.name
得到的就是去除formatter的输出。
LLDB
提供了一些常见类型的格式化,我们也可以自定义格式化,这点下文再述。
v
这是一个最早从 Xcode 10.2[1] 开始引入的alias,之前的版本需要使用frame variable
v并不像p
和po
一样,v
并没有编译执行的能力,但因此速度也更快。它能访问的是当前栈帧能访问到的数据。如果需要一些更复杂的执行代码或是计算一些值,建议还是使用p
和po
。
过程
那么,内部是如何运作的呢?
当执行v variable
的时候,会检测当前程序状态,从内存中读出数据,进行(之前说过的)类型推断。如果有访问变量的子属性,例如v variable.field1.field2
,则会不断的重复读内存和类型推断的行为,最后再走到(之前在p
说过的)格式化。
和p
有什么区别?
还记得这个例子吗?
因为访问是内存中运行时的数据,v
可以直接访问cruise.name
。
总结
只有
po
有描述的过程p
和v
都有格式化参与因为
po
和p
有编译执行的能力,所以可以更随意的执行一些逻辑因为
v
访问的是内存中实际的值,类型推断可以不断执行,最终再到格式化逻辑
所以,实际使用还是需要根据情况,需求,选择适合的指令帮助调试。
定制特定类型格式化输出
po的描述
CustomDebugStringConvertible
Swift 对于每种类型都提供了默认的描述,但可以通过实现CustomDebugStringConvertible
修改这个描述。
CustomReflectable
而实现CustomReflectable
可以自定义节点的反射 可以用于
隐藏你不希望暴露的节点
更可读的改变你对节点描述
举个例子,还是这个Trip
`struct Trip {
var name: String
var destinations: [String]
}
let cruise = Trip(
name: "Mediterranean Cruise",
destinations: ["Sorrento", "Capri", "Taormina"]
)
print(cruise)
//Trip(name: "Mediterranean Cruise", destinations: ["Sorrento", "Capri", "Taormina"])
extension Trip: CustomReflectable {
var customMirror: Mirror {
return Mirror(self,
children: [
"trip": self.name,
//"dest": self.destinations,
],
displayStyle: .struct)
}
}
print(cruise)
//Trip(trip: "Mediterranean Cruise")
`
Objective-C
对于 Objective-C,可以覆盖debugDescription
或者description
方法来自定义输出内容。
Formatter(格式化输出)
常见类型都有默认的格式化输出,通常情况,默认的就足够使用了。
Filter(过滤器)
可以使用过滤器过滤,从而只展示想要显示的属性。
这将会影响LLDB
在console里的输出和Xcode中的Variables View
中的显示。help type filter add
有更细节的一些使用场景。
String Summaries(字符串描述)
数据的字符串形式的描述。在Xcode Variables View
里面展示。字符串描述属于格式化的一部分。
举个具体的例子:
这个旅途的名字有个浅显易懂的描述,但是具体的目的地(“3 values”)并不能让人快速理解。
简单粗暴的的解决办法是:
使用type summary add
定义一个Trip的描述样式,LLDB
会遵循这种样式去输出。
但你也可以很明显的看出来,访问数组的部分是写死的硬编码,这显然不是很可靠。
好在我们可以用Python去写格式化输出器,并能完整访问LLDB
的Python接口。
lldb bridge unit(桥接单元)
在使用Python写格式化输出器之前,先了解一下LLDB
的一些类型
SBTarget 正在被调试的程序
SBProcess 和程序关联的具体的进程
SBThread 执行的线程
SBFrame 和线程关联的一个栈帧
SBVariable 变量,寄存器或是一个表达式
需要注意的是Xcode 11
以后开始使用的是Python 3
使用Python脚本实现字符串描述
`(lldb) script
cruise = lldb.frame.FindVariable("cruise")
print(cruise)
(Travel.Trip) cruise = {
name = "Mediterranean Cruise"
destinations = 3 values {
...
}
`
script 指令进入交互式脚本模式
lldb.frame
返回的是SBFrame
,表示当前栈帧我们知道有一个叫
cruise
的变量,所以直接使用FindVariable
去拿到SBValue
`>>> destinations = cruise.GetChildMemberWithName("destinations")
print(destinations)
([String]) destinations = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
count = destinations.GetNumChildren()
begin = destinations.GetChildAtIndex(0)
print(begin)
(String) [0] = "Sorrento"
end = destinations.GetChildAtIndex(count - 1)
print(end)
(String) [2] = "Taormina"
`
这里的逻辑相对比较常规
我们知道有一个子属性叫
destinations
,使用GetChildMemberWithName
直接拿到SBValue
begin
和end
也是两个SBValue
`>>> print("Trip from {} to {}".format(begin, end))
Trip from (String) name = "Sorrento" to (String) name = "Taormina"
print("Trip from {} to {}".format(begin.GetSummary(), end.GetSummary()))
Trip from "Sorrento" to "Taormina"
`
因为
begin
和end
是SBValue
,并不是我们想要的字符串,这里拿实际的字符串进行格式化至此我们知道如何用
Python
脚本和LLDB
结合拿到合理的字符串了
LLDB
加载Python
独立脚本
把自定义的脚本写到独立的文件中,利于重复利用
// Trip.py def SummaryProvider(value, _): destinations = value.GetChildMemberWithName("destinations") count = destinations.GetNumChildren() if count == 0: return "Empty trip" begin = destinations.GetChildAtIndex(0).GetSummary() end = destinations.GetChildAtIndex(count - 1).GetSummary() return "Trip with {} stops from {} to {}".format(count, begin, end)
(lldb) command script import Trip.py (lldb) type summary add Travel.Trip --python-function Trip.SummaryProvider (lldb) v cruise (Travel.Trip) cruise = Trip with 3 stops from "Sorrento" to "Capri"
Synthetic Children(人工子节点)
使用type synthetic add
可以给类型自定义展示哪些子节点
type synthetic add Travel.Trip --python-class Trip.ExampleSyntheticChildrenProvider
// Trip.py class ExampleSyntheticChildrenProvider: def __init__(self, valobj, internal_dict): this call should initialize the Python object using valobj as the variable to provide synthetic children for def num_children(self): this call should return the number of children that you want your object to have def get_child_index(self,name): this call should return the index of the synthetic child whose name is given as argument def get_child_at_index(self,index): this call should return a new LLDB SBValue object representing the child at the index given as argument def update(self): //optional this call should be used to update the internal state of this Python object whenever the state of the variables in LLDB changes. def has_children(self): //optional this call should return True if this object might have children, and False if this object can be guaranteed not to have children. def get_value(self): //optional this call can return an SBValue to be presented as the value of the synthetic value under consideration.
Bonus Tips(附加技巧)
这些技巧已存在很久,可以放心用在近期的LLDB
中 (部分技巧来自2018 412)
快速输出调用的参数
无须记忆不同架构下寄存器,只需要使用
$arg1
类似的,LLDB
会帮我们找到对应的寄存器一般而言,对于
ObjC
,$arg1
是object,$arg2
是selector可以通过
po (SEL)$arg2
打印
想插入调试代码,应如何避免重新编译运行
在需要插入调试代码的地方,下一个断点,命中该断点以后:
expr variable = false
就可以借助expr
会编译执行的能力,可以有效避免漫长(带薪)编译等待,对大型复杂项目会很省时间。如有必要还可以编辑断点,设置好命令后自动执行。
可以用于:
执行一些临时调试的逻辑
修改错误逻辑
跳过一行/多行命令
thread jump --by 1
可以搭配上文提及的expr来免编译修改程序
快速预览一个任意值
之前提到过LLDB
会给我们p
命令的结果取个别名。当然我们也可以自己取个名字,首先
p UIView *$view = your-problem-view
然后在Xcode的Variables View
处,右击鼠标,选择Add Expression…
并输入$view
。
现在你就可以选中这个刚添加的值,点击底部的quicklook
按钮进行预览。
设置一个只生效一次的断点
breakpoint set --one-shot true --name "-[UILabel setText:]"
适合用在知道某个函数执行之后的时间里,特定的方法会被调用,但同时如果设置成普通断点会有太多干扰项。
切换表达式的语言
expr -l objc [-O] -- [object ivarDescription]
如果切换语言时遇到报错可以考虑加`
:
expr -l objc -O -- [`self.view` recursiveDescription]
xxx
表示先在当前语言环境下解析表达式
如果发现经常需要这么干,可以创建个别名:
command alias poobjc expression -l objc -O --
watchpoint
watchpoint
和断点(breakpoint
)很像
命令行使用
watchpoint set
也可以在Xcode的Variables View
中右击想要监控的目标选择Watch xxx
更多用法建议help watchpoint
或者参考 小笨狼与LLDB的故事[2] 中相关部分
总结
使用
v
,p
,po
打印变量使用过滤器,字符串描述,人工子节点去自定义格式化输出
使用
Python 3
脚本帮助多用巧用
LLDB
提供的现成能力可以更轻松的帮助我们调试更多技巧请参阅
LLDB
文档
推荐阅读
iOS 性能优化:用 Xcode Organizer 诊断性能问题
iOS 性能优化: 如何让使用 XCode Debugger 优化 Metal App
国际化适配:使用 Xcode 构建有助于本地化的布局让触达更广更快
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
这篇文章的内容来自于《WWDC19 内参》。
关注【老司机技术周报】,回复「2020」, 即可领取。
参考资料
[1]
Xcode 10.2: https://developer.apple.com/documentation/xcode\_release\_notes/xcode\_10\_2\_release\_notes
[2]
小笨狼与LLDB的故事: https://www.jianshu.com/p/e89af3e9a8d7
本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。