1. 常用调试方式
Print VS 单步调试
说到调试,刚入门编程时,用得最多的无疑是 print,毕竟连教材都是这样写的,直接打印,简单明了。但是当打印内容太多时,就容易看得头晕脑胀了,这里以 Swift 为例,稍微改进下 print 方法:
/**
print log
#file String 包含这个符号的文件的路径
#line Int 符号出现处的行号
#column Int 符号出现处的列
#function String 包含这个符号的方法名字
*/
func printLog<T>(_ message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}
打印的时候输入文件的路径,行列号和方法明等,这样更方便通过 print 的日志来精准定位问题。但是通过 print 来调试时,有时候就不容易发现一些逻辑上的问题,或者需要使用大量的 print 输出日志,这个时候就可以考虑使用单步调试:
单步调试的时候可以逐步查看代码的执行过程,是解决 bug 的神器,如果你是一位新手,这肯定是你首要学会的技能,在单步调试的时候,一般都会结合 LLDB 命令来使用,关于 LLDB 的内容下面会详细说。
但在实际开发中,有些场景是无法通过单步调试来复现的,比如说多线程的场景下,在使用单步时,很多时候是无法复现真实场景的,这个时候就需要使用万能的 print 了。总之,两种方式是必不可少的,在实际开发中,很多时候我们不会直接使用 print,一般都会使用日志框架,来对日志进行和记录和收集。
Crash Report
在我们平常的开发中,你提交测试包给 QA 测试时,他那边出现在了 crash,但却不是调试模式下,这里我们就可以通过通过测试设备,在 Xcode 的 Devices 中把 crash 日志导出来:
关于如何去阅读 Crash report 和定位该 Crash 原因,可以看我之前写的文章:浅谈 Crash Report,这里就不再重复谈。
dSYM 文件
首先来科普下什么是 dSYM 文件:
Xcode编译项目后,我们会看到一个同名的 dSYM 文件,dSYM 是保存函数地址映射信息的文件,调试的 symbols 都会包含在这个文件中,并且每次编译项目的时候都会生成一个新的 dSYM 文件。
应用上架后,可以通过类似友盟统计等工具收集线上的 Crash,这里直接以友盟的为例,先看下 Crash 信息:
这里可以看出这个错误的原因是数组越界了,那么问题来了,我们并不知道是哪里越界,上面只给出了一个内容地址:
5 YHRSS 0x1000420b0 YHRSS + 270512
6 YHRSS 0x100041378 YHRSS + 267128
这时就可以通过 dSYM 文件来定位出问题的地方了。首先通过 archives 来找到 dSYM 文件:
步骤1:
步骤2:
步骤3:
我们 cd 到该文件目录下,然后执行:
atos -arch arm64 -o YHRSS 0x1000420b0
注意这里的 -arch 是和上面 crash 报告中的对应,否则是看不到相应的信息的:
$ atos -arch arm64 -o YHRSS 0x1000420b0
specialized YHArticlesViewController.tableView(_:heightForRowAt:) (in YHRSS) (YHArticlesViewController.swift:215)
这样我们能就精准地获取 crash 出现的具体位置,然后就该是发挥自我价值的时候了。
那些项目中遇到的常见问题定位
循环引用快速定位和解决
如果你怀疑存在循环引用,你可以 Instrument 工具来定位,但这也太麻烦了,你可以直接在 deinit{} 方法( OC 中对应的就是 dealloc 方法)里面打一个断点,如果页面退出时没有执行到该处,就说明该页面存在循环引用,页面内存没有办法释放。
如果存在循环引用,那么首先要检查的是 block 里面的 self 是不是需要 weak,自定义的 delegate 是不是写成了 strong,绝大部分都是这两个原因导致的,逐个去检查就好。
2. LLDB 常用命令的使用
什么是 LLDB
LLDB 是 Xcode 内置的调试工具,它与 LLVM 编译器一起,给开发者提供更丰富的流程控制和数据检测的调试功能,它的主要功能是为 Xcode 提供底层调试环境。
常用命令
-
help 最牛逼的命令
help 可以输出 LLDB 的命令,使用 help <command> 可以输出相应命令的 help。
-
po、p 打印值
po 和 p 的区别在于使用po只会输出对应的值,p 则会返回值的类型以及命令结果的引用名。
-
exp 输出或修改值(主要作用是修改值)
-
bt 当前线程的调用堆栈,可能通过后面添加数字来限制输出线程数,如 bt 5,只输出前5个。
-
thread return 跳出当前方法的执行(thread return 0 设置返回值),但在 swift 中,是无法使用的,已知的问题了,只能等待修复吧,这里给个 OC 的例子:
3. Instrument 的使用
写在最前面,在做性能测试的时候,不能用模拟器,用真机,用真机,用真机,重要的事情说三次。
Time Profiler
time profile 是时间分析工具,主要用来检测应用 CPU 的使用情况,可以看到应用程序中各个方法消耗 CPU 时间。关于概念,这里就不详细介绍了,直接进行实际操作:
-
通过 xcode 中的 product --> profile 来启动 Instrument,并选择 Time Profiler 工具:
-
运行 Time Profiler,配置显示方式,分线程显示和隐藏系统的无关内容:
-
在手机上执行想要测试的操作,执行完后停止 Time Profiler 进行分析:
-
找到主要耗时的地方,并定位到具体的代码行(点击方法的小箭头就可以进入相应的代码处):
这里可以看出,主要有两一个耗时的操作,但明显后面那我格式转换我们没有办法去处理,我们只能从第一个入手。它每次创建都比较耗时,那么我们就不要多次去创建,因为它每次使用的格式的都是一样的,这样我们实质上只需要创建一次就可以了。那我们有什么方法去只创建一次呢,首先能想到的肯定是单例,但是用单例太麻烦了,通过 static 定义成一个常量就可以了,就这样,这处的性能问题就解决了,其它地方也可以通过同样的方法,逐步分析和解决就可以了。
Leaks
使用的步骤几乎和上面的一样,这里就不重复上图了,但在出现内存泄露的地方,我们需要手动去选取对应的位置,这样才方便分析问题:
因为 Leaks 的使用和 Time Profiler 是一样的,这里就不去重复描述使用过程,在这里简单地介绍下会出现内存泄露的常见情况:
- 循环引用
- ARC 中使用 C 方式开辟的内存没有手动释放
- URLSession 对象的多次创建,使用 AFNetworking 时也是同理
这里单独针对 URLSession 对象的多次创建会导致内存泄露问题单独说明下,其实我们只要看过 URLSession 的文档我们就知道了:
Important
The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you don’t invalidate the session, your app leaks memory until it exits.
session 对象会一直持有强引用,导致无法释放,多次创建就会有内存泄露问题,关于 session 的使用,我们应该使用一个单例来管理。而且唯一的 session 还可以加快网络请求,如连接复用等,这里就不详细说,回头有时间再单独写一篇相关的文章,毕竟这不是一两句话就能说清楚的事情。