如何优雅地调试

几乎所有高级语言的编译过程都是预处理>编译>汇编>链接,而gcc是应用最广的编译器了。但是apple嫌弃他太臃肿,于是就有了Clang(编译器前端),LLVM(编译器架构,本质是一个库),LLDB(调试器)

效率低下的调试方法

打断点配合NSLog,这真是最朴素的调试方式。
NSLog效率低下的原因及尝试lldb断点打印Log这篇博客里有个测试,NSLog和printf的效率差了100多倍。调试的NSLog还好,记得自己删掉就行,但是代码里充斥着许多的NSLog日志,肯定是消耗性能的。
一般是这样处理:

#ifdef DEBUG
#define DebugLog(...) NSLog(@"%s 第%d行 \n %@\n\n", __func__, __LINE__, [NSString stringWithFormat:__VA_ARGS__])
#define DebugFunctionLog() NSLog(@"\n==============================================================================\nclassName : %s\nclassFunction : %s\nclassFunctionLine : %d\n==============================================================================", object_getClassName(self),__PRETTY_FUNCTION__,__LINE__)
#else
#define DebugLog(...) /* */
#define DebugFunctionLog(...) /* */
#endif

不过调试的时候,遇到个问题,想到了NSLog,然后还要重新跑,如此循环,很是浪费时间。
还好有LLDB,他的常用调试命令一定要牢牢记住,并且用在调试中,会事半功倍。

LLDB常用命令

与调试器共舞 - LLDB 的华尔兹
这篇博客介绍的很详细,自己对照着玩一下就能记住了,我大致把常用的归类了下

命令 作用 备注
help 列举出所有命令 help <command>了解更多细节
print(p) 打印值 会有美元符开头的字符,类似$0,可以对他操作
expression(e) 修改值 --表示参数结束,e --没有任何参数,相当于print
e -O --(po) 打印对象详情 /x十六进制,/t二进制...(p也适用)
c,n,s,finish 流程控制 对应xcode框上的四个按钮
frame info 当前的行数和源码文件等信息
thread return ... 改变函数返回值 这个超级有用,不用在模拟数据测试了
  • p,po打印变量可以以各种格式打印出,完整清单
  • LLDB无法确定涉及的类型,我以前还以为不能打印出来,其实是需要强制转换告诉LLDB类型
    LYModel *person = [[LYModel alloc] init];
    person.name = @"luyang";
    person.age = 10;
    NSArray *personArray = @[person];

(lldb) p personArray[0].name
error: property 'name' not found on object of type 'id'
(lldb) p ((LYModel *)person).name
(__NSCFConstantString *) $0 = 0x0000000107a17100 @"luyang"

断点


一共有6种类型的断点,最常见的有3种:

  • 行为断点,就是点在行数上的蓝色点,右滑会做个小动画消失的断点。可以设置condition和action。
  • Symbolic Breakpoint,可以设定一些方法名(symbol),执行到的时候停止程序。
  • Exception Breakpoint,主要是出现异常的时候停止程序,开在那边好处多多,当程序崩的时候,很多情况下可以直接定位到错误。

以前一直以为断点只是让程序断住,直到发现了Edit Breakpoint。主要有2个功能,condition,action。(Exception Breakpoint是出异常一定会停止,所以没有condition)

condition是设置触发断点的条件,action是触发断点后可以执行的操作(可以添加多个action)。

    for (int i = 0; i < 10; i++) {
        
    }


  • Ignore是指condition判断忽略的次数,比如设置为2,那么i = 0,i = 1不会进入condition判断,所以也肯定不会停止程序。
  • Automatically continue after evaluating actions就如他的意思,执行完action后不会停止程序。

Breakpoint面板上的操作可以全部由LLDB调试器的命令来实现,不过还是觉得这个在面板里操作更方便些。

Chisel

LLDB调试器还能玩些更疯狂的事,更新UI,pushVC等等。不过用原生的命令会比较难记,正好facebook开源的LLDB插件Chisel完美地解决了这个问题,可以让你的调试更Easy。

安装Chisel

brew update
brew install chisel

安装完成按照安装日志上的提示,在~/.lldbinit文件中添加一行,没有则新建

常用命令

  • pviews & pvc 层级打印views,viewcontroller
  • fc & fvc 通过正则搜索view,viewcontroller,可以正则搜索
  • visualize 预览打开UIImage,UIView,CALayer,可以先搜索,在用地址打开
(lldb) visualize 0x7fcf6fd17f90

  • show & hide 显示和隐藏指定的View,效果立刻出现
(lldb) hide 0x7fcf6fd17f90

  • border/unborder 给指定view加上边框
(lldb) border 0x7fcf6fd17f90

  • caflush 当坐标或者颜色改变了,不需要继续,直接caflush就可以刷新界面
(lldb) e (void)[0x7fcf6fd17f90 setBackgroundColor:[UIColor greenColor]]
(lldb) caflush

  • bmessage 打断点用的,比原生和面板好的地方是,即使子类没有实现的方法,一样会在父类里打,并且做判断[self isKindOfClass:[MyViewController class]],不会打到别的继承类上面去。这样就不用麻烦地重写一个空函数,例如viewWillAppear:
(lldb) bmessage -[MyViewController viewWillAppear:]

自定义命令
还可以执行自定义命令,非常简单。
就是写个类,实现name,description, run3个方法。

  • name 返回值就是自定义命令
  • description 返回值就是描述,help 会返回描述
  • run 运行函数,执行的结果就会显示在window上
# example.py
import lldb
import fblldbbase as fb

def lldbcommands():
    return [ testCommand() ]

class testCommand(fb.FBCommand):
    def name(self):
        return 'levi'

    def description(self):
        return 'this is levi command'

    def run(self, arguments, options):
        print arguments



这是很简单的例子,arguments会接收参数,从而实现很多的功能,还可以lldb.debugger.HandleCommand(...)执行lldb别的命令来返回值,随心所欲。

把example.py的位置放进~/.lldbinit里


重启xcode之后就可以执行自定义命令啦。

作者:levi
comments powered by Disqus