Swift 使用 LLDB 调试命令

瘦欲@ 提交于 2020-03-19 04:35:17

打印和赋值,观察数值变量和view对象属性

  • p指令可打印其对象类型、内存地址以及该对象的值等具体信息,
  • po指令则是打印其调用description方法得到的值。
  • e 赋值指令(后面有例子详解)

流程控制

n 命令,代表 Step Over 操作。
s 命令,代表 Step Into 操作。
finish 命令,代表 Step Out 操作。
c 命令,代表恢复程序执行操作。

修改指针变量的值,观测程序不同变化

程序中: testThreadReturn = “testThreadReturn”

(lldb) p testThreadReturn
(String) $R2 = "testTheardReturn"
(lldb) e testThreadReturn = "zaozuo"
(lldb) p testThreadReturn 
(String) $R4 = "zaozuo"

动态修改view的属性

Thread Return,函数设置断点返回值(swift不兼容)

  • 这个在OC一切ok,在swift还不能用会有以下错误
(lldb) thread return "zaozuo-return"
error: Error returning from frame 0 of thread 1: We only support setting simple integer and float return types at present..

调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

直接设置返回值,不用写死代码破坏代码结构

(五星推荐):编辑断点

设置断点进入条件,condition

断点行为 (Action):

可以设置或不设置,断点进入的条件,设置进入条件后还可以,自定义后续的动作,比如,打印值,修改值,执行shell命令,执行lldb命令,打印log等等很多自定义的动作,这一点确实很强大,这里就可以和shell 脚本联动,测试一些不好测试的点,和需要提前处理外部因素的案例

(五星推荐):在lldb中初始化值,动态赋值,执行多行语句

  • 简单变量赋值,仅仅在调试环境赋值了一个值,是局部变量,可以用print打印
(lldb) e let hello = "Hello"
you can simply use:
(lldb) p let hello = "Hello"
  • 使用"$"符号定义全局常量和变量
OC中为:
(lldb) e NSString *$a = @"c" (lldb) po $a c 

Swift中变为:
(lldb) e let $a = "a" 
(lldb) po $a "a"  
  • Swift中多行语句:输入完成敲一个空行即可开始执行
(lldb) p
Enter expressions, then terminate with an empty line to evaluate:
struct compass{var direction = "N"; var angle = 16.5}
var c = compass()
print(c)
(lldb)
  • 同样的你也可以向当前view添加一个layer
(lldb) p
Enter expressions, then terminate with an empty line to evaluate:
1 let layer = CALayer()
2 layer.backgroundColor = UIColor.yellow.cgColor
3 layer.bounds = CGRect(x:0, y:0, width:100, height:100)
4 layer.position = CGPoint(x:250, y:300)
5 layer.cornerRadius = 12.0
6 view.layer.addSublayer(layer)
7 
(lldb)

  • 修改函数中成员变量的值

引入模块 Importing modules

(lldb) p import MapKit

打印UI层级关系及更新UI

  • oc,试了下这个在swift也可以运行:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7ffcd2f0f1e0; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x7ffcd2f10170>; layer = <UIWindowLayer: 0x7ffcd2f0ea80>>
   | <UIView: 0x7ffcd2c6dc10; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7ffcd2c17f10>>
   |    | <UIButton: 0x7ffcd2c6dfc0; frame = (20 62; 78 30); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x7ffcd2c6bc10>>
   |    |    | <UIButtonLabel: 0x7ffcd2f15af0; frame = (16 6; 46 18); text = 'Button'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7ffcd2f16120>>
   |    | <_UILayoutGuide: 0x7ffcd2c6fae0; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x7ffcd2c6faa0>>
   |    | <_UILayoutGuide: 0x7ffcd2c70740; frame = (0 667; 0 0); hidden = YES; layer = <CALayer: 0x7ffcd2c6d3d0>>
  • swift:
expr -l objc++ -O -- [[[UIWindow keyWindow] rootViewController] _printHierarchy]
  • view指针赋值
(lldb) e id $view = (id) 0x7fbd71432590 
  • 使用指针进行操作
(lldb) e (void) [$view setBackgroundColor:[UIColor redColor]]
  • 当然,我们运行完这条命令,界面上不会马上反应出来,我们还需要
    调用这个命令刷新一下:
(lldb) e (void)[CATransaction flush]

Push 一个 View Controller

  • 第一步我本地实验了下没有成功,报错了就,可能我代码是swift的所以得用swift 代码,下面仅仅oc 可以
(lldb) e navigationController?.pushViewController($vcc, animated: true)
MainNavigationController.swift[36], pushViewController: [pushViewController=====><zaozuo.ProductCategoryController: 0x7ffce5147600>]
(Swift.Void?) $R4 = nil

然后执行:
(lldb) caflush // e (void)[CATransaction flush]
或是
(lldb) c
都可以

查找按钮的 target(这个方法还未尝试,留在这里开阔下思路)

想象你在调试器中有一个 $myButton 的变量,可以是创建出来的,也可以是从 UI 上抓取出来的,或者是你停止在断点时的一个局部变量。你想知道,按钮按下的时候谁会接收到按钮发出的 action。非常简单:

(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

观察实例变量的变化

假设你有一个 UIView,不知道为什么它的 _layer 实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。

首先,我们需要找到 _layer 这个变量在对象上的相对位置:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
现在我们知道 ($myView + 8) 是被写入的内存地址:

(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
    new value: 0x0000000000000000
这被以 wivar $myView _layer 加入到 Chisel 中。

非重写方法的符号断点

假设你想知道 -[MyViewController viewDidAppear:] 什么时候被调用。如果这个方法并没有在MyViewController 中实现,而是在其父类中实现的,该怎么办呢?试着设置一个断点,会出现以下结果:

(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

因为 LLDB 会查找一个符号,但是实际在这个类上却找不到,所以断点也永远不会触发。你需要做的是为断点设置一个条件 [self isKindOfClass:[MyViewController class]],然后把断点放在 UIViewController 上。正常情况下这样设置一个条件可以正常工作。但是这里不会,因为我们没有父类的实现。

viewDidAppear: 是苹果实现的方法,因此没有它的符号;在方法内没有 self 。如果想在符号断点上使用 self,你必须知道它在哪里 (它可能在寄存器上,也可能在栈上;在 x86 上,你可以在 $esp+4 找到它)。但是这是很痛苦的,因为现在你必须至少知道四种体系结构 (x86,x86-64,armv7,armv64)。想象你需要花多少时间去学习命令集以及它们每一个的调用约定,然后正确的写一个在你的超类上设置断点并且条件正确的命令。
幸运的是,这个在 Chisel 被解决了。这被成为

bmessage:
(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c

让代码停在Swift Error 或者Objective C异常

  • 停在Objective C异常
(lldb) br s -E  objc
Breakpoint 6: where = libobjc.A.dylib`objc_exception_throw, address = 0x000000010dededbb
  • 停在Swift Error
(lldb) br s -E swift
Breakpoint 7: where = libswiftCore.dylib`swift_willThrow, address = 0x000000010e55ccc0
  • 停在某一种类型的Swift Error
(lldb) br s -E swift -O EnumError
Breakpoint 8: where = libswiftCore.dylib`swift_willThrow, address = 0x000000010e55

参考
http://lldb.llvm.org/
https://lldb.llvm.org/lldb-gdb.html
https://www.invasivecode.com/weblog/lldb-expression-for-code-injection-in-xcode
http://www.theappbusiness.com/blog/debugging-in-swift
iOS 开发者旅途中的指南针 - LLDB 调试技术,swiftcafe
与调试器共舞 - LLDB 的华尔兹
Chisel-LLDB命令插件,让调试更Easy
Chisel常用命令总结
Swift 代码调试核武-LLDB调试基础
Basic LLDB tips
udacity 的视频教程

结尾

LLDB 调试命令强大,但是有些语法不大好书写,Chisel 做了封装和一些扩展,使得调试App 不在打印各种log,我们可以快捷的查看或是修改代码内的变量,常量,数组,对象,在断点位置执行log,shell等但这都是调试代码内部信息,如何更加灵活的调试UIView和约束信息呢?

这就有了下文:

Reveal 9 iOS 界面调试

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!