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

Stella981
• 阅读 926

作者:Puttin iOS开发者 在字节跳动工作

Sessions: https://developer.apple.com/videos/play/wwdc2019/429/

本文发表于《WWDC19 内参》 2019/07/01

简要介绍了 po, p 和 Xcode 10.2 新加的 alias v 内部的逻辑,以及作为开发者,如何自定义输出帮助你更好的调试。并额外附加一些技巧。

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

大多数 iOS 开发者都很熟悉在LLDB中使用po,把变量输出到 console 中。

实际上在较新版本的 Xcode 中有三种方式输出,每一种方式都有一些权衡。

三种输出方式

po

可以直接输出变量

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

也可以输出计算过后的

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

一般而言,只要能编译通过的表达式,都可以作为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会先把语句生成一小段代码

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

然后编译并执行,再生成取结果的代码

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

然后再编译并执行,拿到对应的结果,并显示出来

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

可以看到这个流程是相对较长的。

p

同样的例子,这次使用p

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

大多数内容和之前的po并没有什么本质区别,但注意到有个$R0,这是LLDB给我们的结果设置了一个自增的名字。我们可以直接使用起了名字的变量:

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

po类似,p也是一个alias,通过help p可以查看。

过程

那么p的过程中又发生了什么呢?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

v

这是一个最早从 Xcode 10.2[1] 开始引入的alias,之前的版本需要使用frame variableLLDB 小技巧:学习 po、p 和 v v并不像ppo一样,v并没有编译执行的能力,但因此速度也更快。它能访问的是当前栈帧能访问到的数据。如果需要一些更复杂的执行代码或是计算一些值,建议还是使用ppo

过程

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

当执行v variable的时候,会检测当前程序状态,从内存中读出数据,进行(之前说过的)类型推断。LLDB 小技巧:学习 po、p 和 v 如果有访问变量的子属性,例如v variable.field1.field2,则会不断的重复读内存和类型推断的行为,最后再走到(之前在p说过的)格式化。

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

p有什么区别?

还记得这个例子吗?

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

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

总结

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

  • 只有po有描述的过程

  • pv都有格式化参与

  • 因为pop有编译执行的能力,所以可以更随意的执行一些逻辑

  • 因为v访问的是内存中实际的值,类型推断可以不断执行,最终再到格式化逻辑

所以,实际使用还是需要根据情况,需求,选择适合的指令帮助调试。

定制特定类型格式化输出

po的描述

CustomDebugStringConvertible

Swift 对于每种类型都提供了默认的描述,但可以通过实现CustomDebugStringConvertible修改这个描述。LLDB 小技巧:学习 po、p 和 v

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 小技巧:学习 po、p 和 v 可以使用过滤器过滤,从而只展示想要显示的属性。

这将会影响LLDB在console里的输出和Xcode中的Variables View中的显示。help type filter add有更细节的一些使用场景。

String Summaries(字符串描述)

数据的字符串形式的描述。在Xcode Variables View里面展示。字符串描述属于格式化的一部分。

举个具体的例子:

LLDB 小技巧:学习 po、p 和 v 这个旅途的名字有个浅显易懂的描述,但是具体的目的地(“3 values”)并不能让人快速理解。

简单粗暴的的解决办法是:

LLDB 小技巧:学习 po、p 和 v 使用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文档

推荐阅读

iOS 性能优化:用 Xcode Organizer 诊断性能问题

iOS 性能优化: 如何让使用 XCode Debugger 优化 Metal App

国际化适配:使用 Xcode 构建有助于本地化的布局让触达更广更快

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

LLDB 小技巧:学习 po、p 和 v 这篇文章的内容来自于《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源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这