Debug Without Build & Run
- 直接启动已安装的 app:Debug -> Attach to Process
- 编译一次,在多个设备调试:Product -> Perform Action -> Run without Buinding
- 将 Product 的 ipa 编译产物直接拖入模拟器安装
断点
断点类型
1. 普通断点
过于普通以至于根本不需要介绍
2. 条件断点
在指定条件下的断点
3. 符号断点
针对某一个特定函数的断点,可以是一个 OC函数,也可以是 C++函数
# 所有 UIViewController 的 viewDidLoad 都会触发
viewDidLoad
# 特定 VC 的 viewDidLoad 触发,这里要注意带参 method 的 :
[HomeViewController viewDidLoad:]
[UILabel setText:]
# 一个实用的例子:unrecognized selector sent to instance 0xxxxxx
[NSObject(NSObject) doesNotRecognizeSelector:]
4. 异常断点
调试时如果程序抛出异常,导致程序退出,就会暂停,能立马定位问题
Exception: # 选择抛出异常对象类型:OC 或 C++
Break: # 选择抛出异常来源: Throw 或 Catch 语句
5. 内存断点(Watch 断点)
观察某个变量值的变化,变化时触发。类似 KVO ,会打印出新值和旧值
注意:内存地址每次 run 都会变化,需要重新 watch
编辑断点
断点参数
参数 | 说明 |
---|---|
Condition | 输入条件表达式,满足条件时触发断点 |
Ignore | 设置忽略断点次数(从第几次之后正式触发断点) |
Action | 断点触发时的操作 |
Options | 勾选 Automatically continue after evaluating actions 之后程序会在断点产生后继续运行 |
断点 Action
断点+Action 可以让调试半自动化,避免每次触发断点,手动 po 的过程
支持多个 Action,实现多个 po 或者 call 多行代码
调试时正确的 Log 方式
(断点条件)+(忽略次数)+ Log Message + 自动跳过断点(只打印 Log)
Log 的好搭档
NSStringFromRange
NSStringFromClass
NSStringFromSelector
NSStringFromCGRect/CGSize/CGPoint
NSStringFromCGVector
NSStringFromUIOffset/UIEdgeInsets
NSStringFromCGAffineTransform
NSStringFromDirectionalEdgeInsets
NSStringFromProtocol
类型 | 说明 |
---|---|
Debugger Command | 常用,触发断点时执行 lldb 命令(po/e/call) |
Log Message | 常用,触发断点时打印一条 Log(当然也可以用 po),好处是不用重新 build、run |
Apple Script | 执行脚本 |
Shell Command | 执行 shell 命令,比如 say hello 之类的... |
Capture GPU Frame | OpenGL ES 调试,捕获断点处 GPU 当前绘制帧 |
Sound | 触发断点时播放一个声音(解决 console 打印 log 淹没所需信息) |
断点共享
默认断点配置是在个人配置下的,不会加入 git,如果需要组员间共享断点(比如异常断点),可以设置断点为 Share Breakpoint
LLDB 命令
导读
传统的编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd)。
前端:主要负责词法和语法分析,将源代码转化为抽象语法树,生成中间码
优化器:优化中间代码
后端:将中间代码转化为各自平台的机器代码。
-
GCC
GNU Compiler Collection,GNU编译器套件,支持 C、C++、Objective-C、Java、Go、汇编等
-
LLVM + Clang
Low Level Virtual Machine(底层虚拟机),现代化模块化的编译器框架
Clang 只需要完成词法和语法分析,代码优化和机器代码的生成工作由llvm完成。
Clang是LLVM的前端,可以用来编译C,C++,ObjectiveC等语言。Clang则是以LLVM为后端的一款高效易用,并且与IDE结合很好的编译前端。
有点:比 GCC 更快的速度,更人性化的错误提示(错误信息可读性更高,提示出错位置,能出错高亮,还提供修复建议),还支持静态代码分析
-
GDB 和 LLDB
GNU Debugger:GCC 的调试器
Low Level Debugger:LLVM 的调试器
-
GCC 到 LLVM
GCC 定制性、可拓展性很差、Objective-C 在 GCC 得不到良好支持
-
Chris Lattner(克里斯·拉特纳)
LLVM、Swift 之父。
苹果公司开发者工具部门的主管,领导Xcode、Instruments和编译器团队。
-
2000 年,研发 LLVM(伊利诺伊大学期间)
-
2005 年,加入 Apple(LLVM 在 05-10 年期间得到了快速发展)
-
2007 年,研发 Clang
-
2010 年,研发 Swift 语言
Swift was designed to scale from hello world to an entire operating system。
Swift 的设计目标是从 Hello World 到操作系统,无论是作为脚本语言还是底层的系统设计,都能应付自如。Chris 相信 Swfit 将超越 Python,取代 C、Java,同时认为 Web 端 Javascript 发展已经够好,没有 Swfit 什么事。
-
2017 年 1 月,加入 Tesla,负责 Autopilot
Swift 项目交给了 Ted Kremenek,Ted 独自完成了 Clang 的静态分析器。
-
2017 年 6 月,离开 Tesla
Chris Lattner:Swift 只是我在苹果工作的一小部分,我花了大量的时间在其他事情上。实际上在苹果我也就晚上或者周末有空写写 Swift。我希望到了特斯拉之后我还能花同样的精力和时间在 Swift 上,毕竟我对这门语言统治世界充满期待。
-
-
Xcode 编译器过渡历史
版本 阶段 Xcode 2 及以前 GCC Xcode 3 推荐 LLVM-GCC 混合编译器,但还不是默认编译器 Xcode 4 LLVM-GCC 逐步过渡到 LLVM Xcode 5 LLVM-GCC被遗弃,只保留 LLVM 5.0 Xcode 4.x 的编译选项中出现过两种编译器:
Apple LLVM Compiler 4.2:LLVM编译器,前端用的是 Clang。
LLVM GCC 4.2:编译器核心是LLVM,前端是 GCC 4.2 编译器。
设置断点
# 给所有名为xx的函数设置一个断点
breakpoint set —name xx
br s -n xx
b xx
# 在文件F指定行L设置断点
breakpoint set —file F —line L
br s -f F -l L
b F:L
# 当执行到该线程时,调试器才会断下来。
# 一段代码可能开了多线程,要在不同线程之间切来切去。这时就可以使用线程断点了
breakPoint set –f 文件名 –l 行号 –t 线程id
# 条件断点
breakpoint set -f viewController.m -l 362 -c "width > 68"
# 给所有名为xx的C++函数设置一个断点(希望没有同名的C函数)
breakpoint set —method xxx
br s -M xxx
# 给一个OC函数[objc msgSend:]设置一个断点
breakpoint set —name “[objc msgSend:]”
b -n “[objc msgSend:]”
# 给所有名为xx的OC方法设置一个断点(希望没有名为xx的C或者C++函数)
breakpoint set —selector xx
br s -S count
# 给所有函数名正则匹配成功的函数设置一个断点(留个正则熟练的同学尝试)
breakpoint set --func-regex regular-expression
br s -r regular-expression
# 给指定函数地址func_addr的位置设置一个断点
br set -a xxx
# 一次性断点
# -- 是命令选项结束符,如果没有选项,可以省略
breakpoint set --one-shot true --name "[UILabel setText:]"
# 断点列表
breakpoint list
br l
# 断点删除
breakpoint delete xxx # index指明断点的序号,如果为空则删除所有断点
breakpoint delete xxx -f # 强制删除(不需要确认)
br del index
# 断点触发时自动执行下去
--auto-continue # 参数
thread
# 流程控制 c n s f
process continue/continue/c # 继续,到下一个断点
thread step-over/next/n # 步进,到下一行代码
thread step-in/step/s # 跳入所调用的函数
thread step-out/finish/f # 跳出所调用的函数
thread return # 立刻返回(用于隔离某个函数,伪造返回值)
thread select # 跳转到所选线程
thread info # 当前线程信息
thread list # 所有线程信息
thread jump # 跳到指定内存地址、或某一行
thread jump --by 1 # 跳过一行代码,也可以拖动滑块(只能往后跳,不可回跳)
# 由于ARC下编译器实际插入了不少retain,release命令。
# 跳过一些代码不执行很可能会造成对象内存混乱发生 crash。
bt/thread backtrace # 打印当前线程的堆栈信息(crash 发生的时候使用)
bt all # 打印所有线程的堆栈信息
p/po
实际是 expression 的 alias
p/expression -- # 打印基本数据类型、结构体
po/expression -O -- # 打印对象(print object),调用 NSObject 的 description 方法打印对象
# 特别的(可以直接用 lldb 进制换算)
p/x # 十六进制
p/o # 八进制
p/t # 二进制
p &_toolView # 打印地址
po [self.view recursiveDescription] # 打印视图层级结构
重写 debugDescription 方法
debugDescription方法是在使用po命令时调用的,实际上也是调用了description方法。
#import <objc/runtime.h>
@implementation TestModel
- (NSString *)debugDescription
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
NSInteger count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (NSInteger i = 0; i < count; i++){
objc_property_t property = properties[i];
const char* cPropertyName = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:cPropertyName];
id value = [self valueForKey:propertyName]?:@"nil";
[dictionary setObject:value forKey:propertyName];
}
free(properties);
return [NSString stringWithFormat:@"<%@: %p> -- %@",[self class],self,dictionary]; }
@end
frame(栈帧)
配合 bt 使用,进入任意作用域
frame select xxx # 进入从 bt 列表选中的作用域
frame variable # 可以打印出当前 frame 调用栈的的所有变量。只接受变量作为参数,不接受表达式。
e/expr
expression 动态的执行赋值表达式改变变量的值,同时打印出结果
# 重新绘制界面, 也可以用 call
expression [CATransaction flush]
# 当使用 expr 调试界面颜色、坐标之类的时候,执行 flush 就可以看到效果
# 注意:如果在动画过程中执行这个命令,就直接渲染出动画结束的效果。
# 定义并打印一个对象
e NSArray *$array = @[@"one", @"two"];
po $array
call
不修改代码,不重新编译的情况下,调用函数改变程序运行流程
call一般只在不需要显示输出,或是方法无返回值时使用。
Watchpoint
# 查看当前机器支持的最大 watchpoint 数量(开发机器是4个)和已经设置的 watchpoint
watchpoint list
# 监视变量 xxx (缺省 -w 参数是 write,就是说写入的时候会触发断点)
watchpoint set variable 0x00ff
# self.string 实际是 setter method,这里不支持 method
watchpoint set variable self->name
watchpoint set variable -w read -- 0x00ff
watchpoint set variable -w write -- 0x00ff
# 条件断点 当所在内存地址 int 值为 20
watchpoint modify -c '*(int *)0x1457fa70 == 20'
Image
image 是 target modules 的 alias
image list # 列出当前App中的所有module
image lookup --address 0x00ff # 通过内存地址定位出错代码所在行
image lookup –name viewDidLoad # 查找一个方法或者符号的所在文件位置
image lookup -type HLSameThemeViewController # 会列出这个类的所有属性和成员变量,用于了解一个类
用法
# Example
image lookup --address 0x0000000104c25550
# 出错位置:ViewController.m:30
Address: BGMultimediaDemo[0x0000000100001550] (BGMultimediaDemo.__TEXT.__text + 192)
Summary: BGMultimediaDemo`-[ViewController viewDidLoad] + 192 at ViewController.m:30
# 有时候会遇到的 [__NSArray0 objectAtIndex:] 数组越界,但是编译器没有直接给出具体位置
# 可以尝试 image lookup -address
比较实用的指令
# 查询所有 executable、 framework 的装载情况
call (void)CFShow((void *)CFBundleGetAllBundles())
注意
调试状态下,对于点语法支持不佳。经常有符号未找到的情况。
调试状态下,还有注意类型转换、和返回值。
建议:声明类型 + 使用发送消息的方式,必要时强制转换类型,申明返回值
不输入命令的时候直接按回车,就会执行上一次执行的命令。
dSYM
如果没有 dSYM 文件,我们在源代码上设置断点就无法打到二进制文件上,就无法调试。
通常只要拿到 ipa 和 dSYM 就可以 Attach to Process 进行调试了,不需要工程源码。
# 手动添加一个 dSYM 文件
target symbols add/add-dsym
这一部分整理较少,之后会进行补充
Chisel
安装
# 我自己是 brew reinstall chisel 才成功的
brew install chisel
# 创建配置
touch ~/.lldbinit
open ~/.lldbinit
# 添加配置
command script import /usr/local/opt/chisel/libexec/fblldb.py
LLDB 具有完备的 Python 支持,Chisel 就是 Python 实现脚本。
对于自定义脚本也可以添加到
~ /.lldbinit
中去对于 alias 起了别名的命令也可以加进去
command alias bpl breakpoint list
打印相关
命令 | 说明 |
---|---|
pviews | 递归打印 key window 中的 view,前面介绍过了,相当于 [self.view recursiveDescription]
|
pvc | 递归打印 key window 中的 view controller |
presponder | 打印指定对象的响应链 |
pinternals | 打印一个对象内部结构,用于查看属性、成员变量。 |
pclass | 打印一个对象的继承关系 |
paltrace | 打印 View的 autolayout 详细信息,相当于调用_autolayoutTrace |
ptv | 打印 View 层级中最上面的一个 tableView,主要和 pcells 配合使用 |
pcells | 打印 View 层级中最上面的一个 tableView 的可见 cells |
pdata | 需要指定 encoding 类型,相当于 [NSString initWithData:encoding:] |
pkp | 打印 key path 对应的值,相当于 valueForKeyPath: |
pivar | 打印对象成员变量, 注意:如果是属性,对应成员变量的名字默认有_前缀。 |
查找相关
命令 | 说明 |
---|---|
fv | 根据类名模糊匹配 UIView,支持正则 |
fvc | 根据类名模糊匹配 UIViewController,支持正则 |
taplog | 输入 taplog 后,可以进行一次 UI 交互,打印点击的 View |
flicker | 将 View 闪烁一下,以便于查找 View 的位置。 |
vs | 在 View 层级中搜索 View,并改变背景色显示出来。 |
显示相关
命令 | 说明 |
---|---|
visualize | 使用 Mac 的预览.app 来预览 UIImage, CGImageRef, UIView, CALayer, NSData (of an image), UIColor, CIColor, or CGColorRef |
show/hide | 显示/隐藏指定的 View |
mask/unmask | 为 View 添加/取消遮罩 |
border/unborder | 为 View 添加/取消边框,用法: border 0x79ec3140 -c green -w 2
|
slowanim/unslowanim | 减慢/还原动画的速度(0~1) |
alamborder/ alamunborder | 给存在 Ambiguous Layouts 问题的 View 加上/取消 Border (可以设置颜色,边框宽度 -c -w) |
caflush | 实时刷新视图渲染,前面介绍过了,相当于 [CATransaction flush] |
常规调试
命令 | 说明 |
---|---|
bmessage | 添加一个符号断点,前面介绍过了 |
wivar | 给实例添加 watchpoint,观察赋值变化,前面也介绍过了 |
参考
- WWDC 2018:效率提升爆表的 Xcode 和 LLDB 调试技巧
- iOS开发调试 - LLDB使用概览
- The LLDB Debugger(GDB to LLDB Command Map)
- https://github.com/facebook/chisel
- Chris Lattner 访谈录
下一期
Runtime Sanitization
- Address Sanitizer(地址消毒剂)
- Thread Sanitizer
Memory Management
- Malloc Scribble
- Malloc Guard Edges和Guard Malloc
- Zombie Objects(僵尸对象)
Instrument
- Leaks
- Time Profiler
- Allocations
- Core Animation
网友评论