说到调试,分为开发过程中的调试和开发结束自测或者优化阶段的调试:
开发期:LLDB调试、Xcode断点调试等
优化期: instruments 内存优化、运行时间、动画渲染优化等
1、LLDB调试
LLDB是Xcode自带的一个开源调试器。存在于主窗口底部的控制台中,我们开发时间断点调试配合LLDB命令调试为多,文中只介绍常用命令,其余可以直接到文档搜索,或者根据命令直接 终端可以输入help查询具体参数使用。
详细文档
暂停方式:
1.断点
2.控制台上方有一个暂停按钮,点击即可暂停程序
LLDB常用命令
1、expression
expression 可简写为e,作用为执行一个表达式,可以用来查询当前堆栈变量的值。查询的时候和p是一样的。
当然e的更主要的用法是通过执行表达式,动态修改当前线程堆栈变量的值,从而达到调试的目的(其实查询也很主要,只是会用另一种方式查询)。
e viewController.view.backgroundColor = [UIColor blackColor]
//动态将我们后面要跳入的控制器背景色改变
if(result) {
//do sth
} else {
//do sth
}
我们也可以在某个if..else..的语句前打上断点,直接修改条 result 的值,使程序覆盖了不同分支:
e self.result = true
而不用代码修改变量值、多次编译执行来进行调试,节省了修改与编译时间。
2 、p、po & print & call
print: 打印某个东西,可以是变量和表达式
(lldb) print self
(ViewController *) $0 = 0x0000618000003880
p: 可以看做是print的简写 和 expression 一样
(lldb) p self
(ViewController *) $1 = 0x0000618000003880
po : 打印一个对象
(lldb) po self
call: 调用某个方法。
call [self.view setBackgroundColor:UIColor.redColor]
3、thread backtrace & bt 、 frame
有时候我们想要了解线程堆栈信息,可以使用thread backtrace 作用是将线程的堆栈打印出来。
thread backtrace 简写 bt
当发生crash的时候,我们可以使用thread backtrace查看堆栈调用 bt 是缩写别名。
打印出来的结果其实和我们Xcode侧边Thread调试的堆栈调用信息是一样的。
bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x0000000103d239b8 OcDemo`-[ViewController viewDidLoad](self=0x0000618000003880, _cmd="viewDidLoad") at ViewController.m:44
frame #1: 0x00000001065f8d51 UIKit`-[UIViewController loadViewIfRequired] + 1235
frame #2: 0x00000001065f919e UIKit`-[UIViewController view] + 27
frame #3: 0x00000001064ccd17 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 122
frame #4: 0x00000001064cd41f UIKit`-[UIWindow _setHidden:forced:] + 294
frame #5: 0x00000001064e02bf UIKit`-[UIWindow makeKeyAndVisible] + 42
在上面打出的堆栈调用信息中可以看到很多frame(帧) 开头的段落。
frame 可以使用的命令
frame variable ,可以打印出当前frame的所有变量 如果需要打印frame中得指定变量,也可以在后面跟参数
(ViewController *) self = 0x0000618000003880
(SEL) _cmd = "viewDidLoad"
frame select ,根据frame队列号选中堆栈调用列表中得frame
(lldb) frame select 0
frame #0: 0x0000000103d239b8 OcDemo`-[ViewController viewDidLoad](self=0x0000618000003880, _cmd="viewDidLoad") at ViewController.m:44
41 }
42
43 - (void)viewDidLoad {
-> 44 �[4m[�[0msuper viewDidLoad];
45
46 }
frame info ,查看当前frame的信息
frame info
frame #0: 0x0000000103d239b8 OcDemo`-[ViewController viewDidLoad](self=0x0000618000003880, _cmd="viewDidLoad") at ViewController.m:44
不过以上操作都可以在Xcode的侧边Thread堆栈调用列表操作,所以使用也较少。
4、c & n & s & finish
c/ continue/ thread continue: 这三个命令都表示程序继续运行
n/ next/ thread step-over: 这三个命令表示单步运行
s/ step/ thread step-in: 这三个命令效果表示进入某个方法
finish/ step-out: 这两个命令效果表示直接走完当前方法,返回到上层frame
5、breakpoint
breakpoint set 设置断点
参数 :
-n 根据当前类中方法名设置断点
(lldb) breakpoint set -n viewDidLoad
Breakpoint 6: 2 locations.
-f 根据我们指定文件设置断点
(lldb) breakpoint set -f VcTwo.swift -n viewDidLoad
Breakpoint 8: 2 locations.
-l 根据文件某一行设置断点 和 -f配合使用
(lldb) breakpoint set -f VcTwo.swift -l 35
Breakpoint 12: where = SWIFTDEMO`SWIFTDEMO.VcTwo.viewDidLoad () -> () + 679 at VcTwo.swift:35, address = 0x0000000109f08637
-c 设置条件断点
breakpoint set -n goS -c flag == YES
-o 设置单次断点
breakpoint set -n goS -O
breakpoint list 断点列表
(lldb) breakpoint list
Current breakpoints:
1: names = {'objc_exception_throw', '__cxa_throw'}, locations = 2, resolved = 2, hit count = 0
1.1: where = libobjc.A.dylib`objc_exception_throw, address = 0x000000010594af11, resolved, hit count = 0
1.2: where = libc++abi.dylib`__cxa_throw, address = 0x0000000109314b86, resolved, hit count = 0
2: file = '/Users/young/Desktop/demo/work/OcDemo/OcDemo/ViewController.m', line = 44, exact_match = 0, locations = 1, resolved = 1, hit count = 1
2.1: where = OcDemo`-[ViewController viewDidLoad] + 216 at ViewController.m:44, address = 0x0000000103d239b8, resolved, hit count = 1
breakpoint disable/enable 暂停/恢复断点
(lldb) breakpoint disable 2
1 breakpoints disabled.
breakpoint delete 删除断点
(lldb) breakpoint delete 1
1 breakpoints deleted; 0 breakpoint locations disabled.
删除所有断点
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (6 breakpoints)
如果文件不存在或者方法不存在
Breakpoint 11: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
6、其他
开启调试模式、打印出所有运行时发送的消息: 可以在代码里执行下面的方法:
(void)instrumentObjcMessageSends(YES);
或者断点暂停程序运行,并在 gdb 中输入下面的命令:
call (void)instrumentObjcMessageSends(YES)
之后,运行时发送的所有消息都会打印到/tmp/msgSend-xxxx
文件里了。
终端中输入命令前往:
open /private/tmp
可能看到有多条,找到最新生成的,双击打开
在模拟器上执行执行以下语句(这一套调试方案仅适用于模拟器,真机不可用,关于该调试方案的拓展链接: *Can the messages sent to an object in Objective-C be monitored or printed out?* )
2、Chisel调试
Chisel扩展了一些列的lldb的命令来帮助iOS开发者调试iOS应用程序。
1.安装Chisel
1.确保终端安装了Homebrew
2.终端执行命令:brew install chisel 输入命令后我遇到第一个问题。
brew install chisel 后可能出现的问题
碰见这个问题终端执行命令:sudo chown -R ${USER} /Library/Caches/Homebrew/,执行此命令后问题解决。
3.如果没有第二步的问题,执行命令:brew install chisel后出现如下界面
brew install chisel 执行成功
4.注意看Caveats下面的那两行,意思是把第二行的文字command script import /usr/local/opt/chisel/libexec/fblldb.py添加到.lldbinit文件中,这时执行命令echo command script import /usr/local/opt/chisel/libexec/fblldb.py >> ~/.lldbinit(粗体文字替换为你终端Caveats下面的第二行文字)可免去你去找.lldbinit文件,或者.lldbinit文件不出现的烦恼啊。到此步不出意外已经安装成功。
5.安装成功后重新启动Xcode即可。
6.xcode检查是否安装成功,打断点,控制台输入help. 终端下检查是否安装成功输入命令:lldb,然后输入help
Current user-defined commands:
alamborder -- Put a border around views with an ambiguous layout
alamunborder -- Removes the border around views with an ambiguous layout
出现这个就表示安装成功,可以使用了。
2.Chisel的使用
1、pviews
这个命令可以递归打印所有的view,并能标示层级,相当于 UIView 的私有辅助方法 [view recursiveDescription] 。 善用使用这个功能会让你在调试定位问题时省去很多麻烦。可以直接根据view名称或者内存地址去查找对应的层级view
pviews 0x7f88ae7a08c0
>
2、pvc
这个命令也是递归打印层级,但是不是view,而是viewController。利用它我们可以对viewController的结构一目了然。 其实苹果在IOS8也默默的添加了 UIViewController 的一个私有辅助方法 [UIViewController _printHierarchy]
同样的效果。而且还可以看到 viewController 是否已经 viewDidLoad 。
3、fv & fvc
fv
和 fvc
这两个命令是用来通过类名搜索当前内存中存在的view和viewController实例的命令,支持正则搜索。
fv UI
0x7f88ae55f830 UILayoutContainerView
0x7f88ae562120 UINavigationTransitionView
0x7f88ae5d44c0 UIViewControllerWrapperView
0x7f88ae43fec0 UILayoutContainerView
4、visualize
这是个很有意思的功能,它可以让你使用Mac的预览打开一个 UIImage, CGImageRef, UIView, 或 CALayer。 这个功能或许可以帮我们用来截图、用来定位一个view的具体内容。 但是在我试用了一下,发现暂时还是只能在模拟器时使用,真机还不行。比如说知道一块内存地址,这种情况下可以用这个命令直接去看截图效果,更容易知道对应的位置。
visualize 0x7f88ae728e20
或
visualize self.xxxlabel
5、show & hide
这两个命令用来显示和隐藏一个指定的 UIView . 你甚至不需要Continue Progress. 就可以看到效果。
6、mask/umask border/unborder
这两组命令用来标识一个view或layer的位置时用, mask用来在view上覆盖一个半透明的矩形, border可以给view添加边框。但是在我实际使用的过程中mask总是会报错,估计是有bug, 那么mask/unmask 一般不要用好了,用border命令是一样的效果,反正二者的用途都是找到一个对应的view。
7、caflush
这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush]
方法,要注意如果在动画过程中执行这个命令,就直接渲染出动画结束的效果。
当你想在调试界面颜色、坐标之类的时候,可以直接在控制台修改属性,然后caflush
就可以看到效果啦,是不是要比改代码,然后重新build省事多了呢。
3、instruments 的使用
打开方式:Xcode - Open Developer Tool - Instruments
Timer Profiler : 分析代码的执行时间
TimeProfiler见名知意:时间分析工具,它会按照设定的时间间隔(默认1毫秒)来跟踪每一线程的堆栈信息(stacktrace),并通过比较时间间隔之间的堆栈状态,来推算出某个方法执行了多久,给出一个近似值。具体步骤如下:
1、双击TimeProfiler进入到调试界面
2、选择机器和要调试的App(最好选择真机,性能参数更真实,而且Xcode9模拟器经常跑不了)
3、点击左上角红色按钮开启调试之后便可以看到如下图的时间消耗
但是整个执行过程包括了很多我们不需要看到的系统进程等,所以我们通过底部的 Call Tree 来做一些筛选过滤:
Separate by Thread:按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程。
Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide Missing Symbols:隐藏缺失符号。如果dSYM文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,简化列表。
Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
Top Functions:找到最耗时的函数或方法。
将以上勾选之后便可以看到对应的具体代码,具体方法的执行时长,并进行优化。
问题
符号化问题:
当跟踪数据里面显示的是地址而不是可读性较强的符号
因此需要将地址转换为符号。地址和 符号的映射保存在dSYM文件中。instruments工具一般会自动找到dSYM文件,默认在当前电脑build的项目是可以正常使用的。
如果其他方式的包进行调试需要配好对应的 dSYM文件,可以手动设置(暂停调试):files -> symbols > 选择需要符号化的可执行文件或者framework > “select dSYM or containing folder” > 找到dSYM文件(dSYM文件默认会保存在build文件夹下)
Leaks : 内存泄漏检测工具
除开代码运行时间分析,我们还常用的就是内存检测,灵活的运用Leaks可以帮助我们预防程序中的内存泄漏防止程序内存耗用过大被挂起。
具体操作:
1、双击Leaks进入到调试界面
2、选择机器和要调试的App(最好选择真机,性能参数更真实,而且Xcode9模拟器经常跑不了)
3、点击左上角红色按钮开启调试之后使用App,观察Leaks Checks 栏,当出现红叉,就表示有内存泄漏点:
4、在上图可以看到我们选中了Leak Checks栏,然后在中间的工具栏切换到Call Three,同上时间分析,我们需要设置一些过滤条件,过滤掉一些妨碍我们观察的内容:
如上图是已过滤后,剩下都是用户写的代码,选中其中一个双击或者 右击 reval to xcode,可以直接跳转到对应代码区域,看代码高亮两行就是内存泄漏点,如下:
3、Core Animation:动画检测
在出现图像性能问题,滑动,动画不够流畅之后,我们首先要做的就是定位出问题的所在。而这个过程并不是只靠经验和穷举法探索,我们应该用有脉络,有顺序的科学的手段进行探索。
首先,我们要有一个定位问题的模式。我们可以按照这样的顺序来逐步定位,发现问题。
- 定位帧率,为了给用户流畅的感受,我们需要保持帧率在60帧左右。当遇到问题后,我们首先检查一下帧率是否保持在60帧。
- 定位瓶颈,究竟是CPU还是GPU。我们希望占用率越少越好,一是为了流畅性,二也节省了电力。
- 检查有没有做无必要的CPU渲染,例如有些地方我们重写了drawRect,而其实是我们不需要也不应该的。我们希望GPU负责更多的工作。
- 检查有没有过多的offscreen渲染,这会耗费GPU的资源,像前面已经分析的到的。offscreen 渲染会导致GPU需要不断地onScreen和offscreen进行上下文切换。我们希望有更少的offscreen渲染。
- 检查我们有无过多的Blending,GPU渲染一个不透明的图层更省资源。
- 检查图片的格式是否为常用格式,大小是否正常。如果一个图片格式不被GPU所支持,则只能通过CPU来渲染。一般我们在iOS开发中都应该用PNG格式,之前阅读过的一些资料也有指出苹果特意为PNG格式做了渲染和压缩算法上的优化。
- 检查是否有耗费资源多的View或效果。我们需要合理有节制的使用。像之前提到的UIBlurEffect就是一个例子。
- 最后,我们需要检查在我们View层级中是否有不正确的地方。例如有时我们不断的添加或移除View,有时就会在不经意间导致bug的发生。像我之前就遇到过不断添加View的一个低级错误。我们希望在View层级中只包含了我们想要的东西。
OK,当我们有了一套模式之后,就可以使用苹果为我们提供的优秀测试工具来进行测试了。
对于图形性能问题的地位。一般我们有下列测试工具:
Instruments里的:Core Animation instrument
然后我们来根据上面定位问题的模式来选择相应测试工具:
1、定位帧率
2、定位瓶颈
3、检查有无必要的CPU渲染
以上三点我们可以使用CoreAnimation instrument来测试。
CoreAnimation instrument包含了两个模块:
Core Aimation FPS : 检测帧率
Time Profiler: 检测主线程时间利用( CPU)
关于GPU的瓶颈问题,我们可以通过OpenGL ES Driver instrument来获得更详细的信息。
4、检查有无过多offscreen渲染
5、检查有无过多Blending
6、检查有无不正确图片格式,图片是否被放缩,像素是否对齐。
7、检查有无使用复杂的图形效果。
以上这四点我们同样使用CoreAnimation instrument来测试。
我们可以看到上图右下角的Debug options有多个选项。我们通过勾选这些选项来触发Color Debug。下面逐个对这些选项进行分析。
1、Color Blended layers(图层混合)
这个选项是检测哪里发生了图层混合,先介绍一下什么是图层混合?很多情况下,界面都是会出现多个UI控件叠加的情况,如果有透明或者半透明的控件,那么GPU会去计算这些这些layer最终的显示的颜色,也就是我们肉眼所看到的效果。例如一个上层Veiw颜色是绿色RGB(0,255,0),下层又放了一个View颜色是红色RGB(0,0,255),透明度是50%,那么最终显示到我们眼前的颜色是蓝色RGB(0,127.5,127.5)。这个计算过程会消耗一定的GPU资源损耗性能。如果我们把上层的绿色View改为不透明, 那么GPU就不用耗费资源计算,直接显示绿色。
如图,勾选 Color Blended Layers 选项后,blended layer 就会被显示为红色,而不透明的layer则是绿色。我们希望越少红色区域越好。
处理方法:把一些不需要透明的变成不透明,减少红色部分。
对于UIImage来说,本身图片的透明也会影响计算。
2、Color Hits Green and Misses Red(光栅化)
这个选项主要是检测我们有无滥用或正确使用layer的 shouldRasterize 属性。
shouldRasterize = YES 开启光栅化,什么是光栅化?光栅化是将一个layer预先渲染成位图(bitmap),再加入到缓存中,成功被缓存的layer会标注为绿色,没有成功缓存的会标注为红色,正确使用光栅化可以得到一定程度的性能提升。
适用情况:一般在图像内容不变的情况下才使用光栅化,例如设置阴影耗费资源比较多的静态内容,如果使用光栅化对性能的提升有一定帮助。
非适用情况:如果内容会经常变动,这个时候不要开启,否则会造成性能的浪费。例如我们在使用tableViewCell中,一般不要用光栅化,因为tableViewCell的绘制非常频繁,内容在不断的变化,如果使用了光栅化,会造成大量的离屏渲染降低性能。
在测试的过程中,第一次加载时,开启光栅化的layer会显示为红色,这是很正常的,因为还没有缓存成功。但是如果在接下来的测试,例如我们来回滚动TableView时,我们仍然发现有许多红色区域,那就需要谨慎对待了。因为像我们前面讨论过的,这会引起offscreen rendering。
检查一下是否有滥用该属性,因为系统规定的缓存大小是屏幕大小的2.5倍,如果使用过度,超出了缓存大小,会引起offscreen rendering(离屏渲染)。检测layer是否内容不断更新,内容的更新会导致缓存失效和大量的offscreen rendering。
缓存的时间为100ms,因此如果在100ms内没有使用缓存的对象,则会从缓存中清除。
3、Color copied images(图片颜色格式)
被拷贝给CPU进行转化的图片显示为绿色。那么这句话怎么理解呢?如果GPU不支持当前图片的颜色格式,那么就会将图片交给CPU预先进行格式转化,并且这张图片标记为蓝色。那么GPU支持什么格式呢?苹果的GPU只解析32bit的颜色格式。
知识扩展:32bit指的是图片颜色深度,用“位”来表示,用来表示显示颜色数量,例如一个图片支持256种颜色,那么就需要256个不同的值来表示不同的颜色,也就是从0到255,二进制表示就是从00000000到11111111,一共需要8位二进制数,所以颜色深度是8。通常32bit色彩中使用三个8bit分别表示R红G绿B蓝,还有一个8bit常用来表示透明度(Alpha)。
4、Color Non-Standard Surface Formats (不标准的表面颜色格式)
5、Color Immediately(颜色刷新频率)
6、Color Misaligned Images(图片大小)
这个选项可以帮助我们查看图片大小是否正确显示
7、Color Offscreen-Rendered Yellow(离屏渲染)
离屏渲染Off-Screen Rendering 指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。还有另外一种屏幕渲染方式-当前屏幕渲染On-Screen Rendering ,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。 离屏渲染会先在屏幕外创建新缓冲区,离屏渲染结束后,再从离屏切到当前屏幕, 把离屏的渲染结果显示到当前屏幕上,这个上下文切换的过程是非常消耗性能的,实际开发中尽可能避免离屏渲染。
触发离屏渲染Offscreen rendering的行为:
(1)drawRect:方法
(2)layer.shadow
(3)layer.allowsGroupOpacity or layer.allowsEdgeAntialiasing
(4)layer.shouldRasterize
(5)layer.mask
(6)layer.masksToBounds && layer.cornerRadius
这里有需要注意的是第三条layer.shouldRasterize ,其实就是我们本文讲的第三个选项光栅化,光栅化会触发离屏渲染,因此光栅化慎用。
第六条设置圆角会触发离屏渲染,如果在某个页面大量使用了圆角,会非常消耗性能造成FPS急剧下降,设置圆角触发离屏渲染要同时满足下面两个条件:
layer.masksToBounds = YES;
layer.cornerRadius = 5;
勾选这个选项将需要offscreen渲染的layer标记为黄色。
处理方法:
1、以上图为例子,NavigationBar和ToolBar被标记为黄色。因为它们需要模糊背后的内容,这需要offscreen渲染。但是这是我们需要的。而图片也是被标记为黄色,那是因为阴影的缘故。我前面已经提到了这一点,如果此时我们用shadowPath来替代的话,就能够避免offscreen渲染带来的巨大开销。
2、圆角什么的变成通过GraphicsContex绘制的方式处理
8、Color Compositing Fast-Path Blue (快速路径)
这个选项勾选后, 标记由硬件绘制的路径为蓝色,蓝色越多越好
9、Flash updated regions(重绘区域)
这个选项会对重绘的内容高亮成黄色,重绘就是指使用Core Graphics绘制,绘制会损耗一定的性能,因此重绘区域应该越小越好。
总结一下
我们需要重点注意的是
Color Blended Layers (图层混合)
Color Hits Green and Misses Red(光栅化)
Color Offscreen-Rendered Yellow(离屏渲染)
因为这三个部分对性能的影响最大。
4、 Xcode 内存图
其实在日常开发中,经常遇到一些写法不严谨、思维绕圈导致的内存循环引用问题,多个资源未释放就会导致内存占用过多,甚至崩溃问题。
开启位置:
当我们察觉内存状态有问题的时候,我们可以通过这种方式来检查内存中对象的引用是否存在未释放的问题。
入上图左侧对象树形图,我们可以看到VideoPlayer在内存中存在3个对象,并且通过右侧可以看到对象间的引用关系。这个时候我们通过逻辑判断,判断3个对象在当前场景下是否合理,如果不合理,在右侧内存图检查是否存在循环引用。
内存图主要用于了解一些引用类型,查看引用结构。也可以很好的帮我们分析内存循环引用的问题。