LLDB 小技巧:学习 po、p 和 v

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

作者: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 <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[1] 开始引入的alias,之前的版本需要使用frame variablev并不像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(桥接单元)

在使用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

  • beginend也是两个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"
  • 因为beginendSBValue,并不是我们想要的字符串,这里拿实际的字符串进行格式化

  • 至此我们知道如何用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] 中相关部分

总结

  • 使用vppo打印变量

  • 使用过滤器,字符串描述,人工子节点去自定义格式化输出

  • 使用Python 3脚本帮助

  • 多用巧用LLDB提供的现成能力可以更轻松的帮助我们调试

  • 更多技巧请参阅LLDB文档

参考资料

[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

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 为何说IT科技公司应该留住35岁员工?

 工友们!大家好,今天你摸鱼了吗?

 缓存穿透,雪崩,击穿以及解决方案分析

 图文详解:如何给女朋友解释什么是微服务?


在看点这里好文分享给更多人↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值