Ea5430b75b2c0c7379b3b31d43e158f8
Another 'LLDB: Beyond "po"'

简要介绍了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 <expr>

Command Options Usage:
  po <expr>

'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的过程中又发生了什么呢?

实际上一直到取到结果这一步,ppo的行为是一模一样的。不同的是

p使用了dynamic type resolution(动态类型推断)。

让我们把例子稍微改一改:

在这个例子里,cruise静态的类型是Activity,运行时的实际类型是Trip

这时候如果我们p cruise,得到的结果和修改例子之前并没有区别。
因为LLDB读取了代码的metadata(元数据),去判断在特定时间点,特定变量的类型。

但动态类型推断只会发生在表达式的结果部分,所以如果尝试直接p cruise.name,并不会成功:

之前提到过,得是一个能编译通过的代码。所以如果真的想要访问,只能显式类型转换以后,再访问。

其实,在动态类型推断之后,还有一步格式化:

这步会把从动态类型推断拿到的对象转换成人类可读的字符串。

expression --raw -- cruise.name得到的就是去除formatter的输出。

LLDB提供了一些常见类型的格式化,我们也可以自定义格式化,这点下文再述。

v

这是一个最早从 Xcode 10.2 开始引入的alias,之前的版本需要使用frame variable

v并不像ppo一样,v并没有编译执行的能力,但因此速度也更快。它能访问的是当前栈帧能访问到的数据。如果需要一些更复杂的执行代码或是计算一些值,建议还是使用ppo

过程

那么,内部是如何运作的呢?

当执行v variable的时候,会检测当前程序状态,从内存中读出数据,进行(之前说过的)类型推断。

如果有访问变量的子属性,例如v variable.field1.field2,则会不断的重复读内存和类型推断的行为,最后再走到(之前在p说过的)格式化。

p有什么区别?

还记得这个例子吗?

因为访问是内存中运行时的数据,v可以直接访问cruise.name

总结

  • 只有po有描述的过程
  • pv都有格式化参与
  • 因为pop有编译执行的能力,所以可以更随意的执行一些逻辑
  • 因为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(桥接单元)

top Created with Sketch.