现在的位置: 首页 > 综合 > 正文

关于调试 Part-1

2013年03月06日 ⁄ 综合 ⁄ 共 7089字 ⁄ 字号 评论关闭

  我发现在ios讨论群里欢迎喜欢交流和热心的iphone开发朋友加入qq群参与讨论:186739796,验证码:csdn。)很多人会贴一些系统在控制台输出的崩溃代码,来咨询是什么问题导致的。其实问题真的是没有范式的,但是解决问题的方法却大多相通。我以前也遇到过很多的crash,也是从慢慢的解决问题当中走过来的。我大致的收集了一些关于调试的技巧的文章,希望大家能体会出解决问题背后的一些思考方法,这才是快速解决问题的最好的利器!

part-1

转自:http://article.ityran.com/archives/1006

有这样一种情形:当我们正在快乐的致力于我们的app时,并且什么看都是无比顺利,但是突然,坑爹啊,它崩溃了。(悲伤地音乐响起)

我们需要做的第一件事就是:不要惊慌。

修复崩溃不是很困难的。假如你崩溃了,并且胡乱的改些东西,而且还在不停的念着咒语希望bug神奇的自动消失,你大多数情况下都会使情况更麻烦。相反的,你需要知道一些系统的方法,并且学习怎么找到崩溃和他的原因。

第一件需要知道的就是在你的代码中准确的找到crash发生的地方:在那个文件,那一行。Xcode debugger将会帮助你,但是你需要懂得怎么样最好的使用它,这也是这篇教程展示给你的。

这篇教程对于所有的开发者都是有利的。即使你是一个很有经验的ios开发者,你也可能会从中学习到一些你不知道的小窍门。

准备开始

下载这个例子程序。你将会看到这是一个有bug的程序。当你打开这个项目的时候,xcode会显示至少8个编译警告,这个通常都是危险的信号。顺便说一下,我们使用xcode4.3来做这篇教程,4.2的版本也应该没有什么问题。

注意:为了跟随这篇教程,这个编译生成的app需要运行在ios5的模拟器上面。假如你运行这个app到你的设备上,你也会崩溃,但是他们可能不会发生和教程一样的情况。

在模拟器上面运行你的app,你将会看到发生了什么。

The app crashes immediately.

嘿,他崩溃了。

两种最基本的crash类型常发生:SIGABRT(也叫EXC_CRASH)和EXC_BAD_ACCESS(也可能会是SIGBUS或者SIGSEGV)

就crash而言,SIGABRT是一个比较好解决的,因为他是一个可掌控的crash。App会在一个目的地终止,因为系统意识到app做了一些他不能支持的事情。

EXC_BAD_ACCESS是一个比较难处理的crash了,当一个app进入一种毁坏的状态,通常是由于内存管理问题而引起的时,就会出现出现这样的crash。

幸运的是,第一种崩溃(也是大多数崩溃)是SIGABRT,SIGABRT通常会在xcode的Debug Output窗口(在窗口的右下角)输出一些错误的信息。假如你没有看到Debug Output窗口,在你的xcode窗口的右上角一组图标中点击中间那个,假如还是没有看到Debug Output窗口,你需要点击这个小窗口的右上角的中间那个图标,他靠近搜索框。在这个情况下,会展示一些下面东西:

  1. Problems[14465:f803] -[UINavigationController setList:]: unrecognized selector sent to
  2. instance 0x6a33840
  3. Problems[14465:f803] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
  4. reason: '-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'
  5. *** First throw call stack:
  6. (0x13ba052 0x154bd0a 0x13bbced 0x1320f00 0x1320ce2 0x29ef 0xf9d6 0x108a6 0x1f743
  7. 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7
  8. 0x11a9b 0x2792 0x2705)
  9. terminate called throwing an exception

了解这些错误消息是非常重要的,因为他们包含了错误在那里的重要线索,一下就是需要关注的部分:

  1. [UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840

“unrecognized selector sent to instance XXX” 这条错误消息意味着你的app正在试着执行一个不存在的方法。这种情况的发生,主要是都是一个方法被错误的对象调用了(也就是这个对象没有这个方法,但是你调用了他,就错了)。例如在这里这个问题上,对象就是UINavigationController (在内存地址0x6a33840上),方法就是setList:。

知道crash的原因是很好的,但是你的第一行动目的就是指出这个错误的发生在代码的那个地方。你需要找到源文件的名字和这个错误方法在那一行。你通过使用call stack(就像堆栈跟踪(stacktrace)或者回溯(backtrace))就可以知道这些东西。

当你的程序crash了时,在xcode窗口的左边小窗口会启动Debug Navigator(调试导航)。他会展示在这个app中那个线程是活动的,并且高亮显示crash了的线程。通常他会是线程1,这个app的主线程,这个线程也是你会做最多工作的线程。假如你的代码里面使用了队列(queues)或者后台线程(background threads),这个app也可能会在其他的线程里面崩溃。

The call stack. It doesn't show everything yet.

 

当前xocde就高亮显示了main.m里面的main()函数。但是那些东西并没有告诉你很多,所以你需要继续的向深层次的挖掘。

为了看到堆栈的更多信息,拖拽Debug Navigator底部的滑块到最右边。它将会展示出崩溃时全部的堆栈信息:

The expanded call stack.

这个列表里面的每一项都是一个来这个app或者ios的framework里的方法或者函数。堆栈展示了当前活跃在这个app里面的方法或者方法。调试器(debugger)已经暂停了这个程序,并且所有的这些方法和函数在这个时候也被冻结了。

在底部的函数start(),第一个被调用。在他的执行里面的有些地方,main()函数在他之前。(Somewhere in its execution it called the function above it, main().)。他是应用程序的开始入口点,并且它经常在底部附近。Main()也叫UIApplicationMain()(这个针对的是ios哈,并不是其他所有程序都是这样的)。在这个编辑窗口里面用绿色箭头指示的那一行(就是在这个教程最开始前面程序崩溃时停止在那个图片上,高亮显示的部分)。

进一步来看看这个堆栈,UIApplication()在UIApplication对象里调用_run方法,_run方法里面又调用CFRunLoopRunInMode()方法,CFRunLoopRunInMode()方法里面又调用CFRunLoopSpecific()方法,就这样一直向下调用,一直到__pthread_kill。

How a call stack works.

所有在这个堆栈里面的函数和方法都是灰色的,除了main()函数。那是因为他们都来自内置的ios frameworks(ios内置框架)。所以没有针对他们可见的源码。

在这个堆栈里面唯一的东西就是你有main.m的源码,因此xcode的代码编辑器就显示了它,即使他不是这个崩溃的真正原因。但是这个经常混淆初学者,但是马上我将展示怎么样来弄懂它。

开个玩笑,点击这个堆栈里面的任意一项,你将会看到许多的汇编代码,这些你可能完全不理解:

If there is no source code, Xcode shows assembly.

加入我们得到那样的源码,我想很多人都会说:坑爹啊。

异常断点

你怎么样找到是代码里面的哪一行使app崩溃的?无论什么时候,你得到的一个想这样的堆栈路径,一个异常通过这个app抛出。(你多半会说因为堆栈里面有一个函数叫objc_exception_rethrow。)

当程序由于做了一些他不能完成的事情时,一个异常就会发生。你所看到的就是这个异常的结果:app做了一些错的事情,异常被抛出,xcode展示异常的结果。理想情况下,你想要的准确的看到异常在那里抛出的。

幸运的是,通过使用Exception Breakpoint(异常断点),你可以告诉xcode在一个特定的时候暂停这个程序。断点是一个在特定时刻暂停你的程序的调试工具。你将会第二篇教程里面看到更多关于他们的信息,但是现在你将会使用一个特殊的断点,它将会在抛出异常前暂停你的程序。

为了设置异常断点,我们不得不切换到Breakpoint Navigator(断点导航器):

The Breakpoint Navigator

在底部有一个小的加号(“+”)按钮。点击它,并且选择Add Exception Breakpoint:

Adding the Exception Breakpoint

一个新的断点将会被增加到这个列表里:

After the Exception Breakpoint has been added

点击Done按钮使弹出的窗口消失。注意在xcode工具栏上面Breakpoints button(断点按钮)是有效的。加入你不想要带着任何断点运行你的app,你可以简单的开关这个按钮到off。但是现在,让它打开,并且再一次运行这个app。

After the crash, the problematic source line is now highlighted.

太好了!代码编辑器现在停止并且指到了代码中的其中一行,不再在令人烦躁的汇编代码了,并且注意在在左边的的Debug Navigatot(调试导航器)里面显示的堆栈信息也不一样了。

显然的,问题就出在AppDelegate里面的application:didFinishLaunchingWithOptions:方法里:

  1. viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];

仔细再次看看这个错误消息:

  1. [UINavigationController setList:]: unrecognized selector sent to instance 0x6d4ed20

在这个代码里面,“viewController.list = something”这种方式隐式的调用了setList:方法,也就是set方法,因为“list”是MainViewController类的一个属性。然而,通过这个错误消息,我们知道viewController这个变量没有指向MainViewController对象,而是指向了UINavigationController,所以显然的,UINavigationController没有“list”属性!所以这些变量在这里混淆了。

打开Storyboard文件,看看window的rootViewController属性实际上是指向那个的:

The storyboard has a navigation controller.

哈哈!Storyboard的最初的view controller是一个Navigation controller。这就是为什么window.rootViewController是一个UINavigationController对象,而不是你自认为的MainViewController。为了修改这里,使用下面的代码来替代application:didFinishLaunchingWithOptions:里面的:

  1. - (BOOL)application :( UIApplication *)application didFinishLaunchingWithOptions :( NSDictionary *)launchOptions
  2. {
  3. UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
  4. MainViewController *viewController = (MainViewController *)navController.topViewController;
  5. viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];
  6. return YES;
  7. }

通过代码可以看出,首先你通过self.window.rootViewController得到UINavigationController,一旦你得到了上面的。你就可以通过请求navigation controller来得到topViewController,进而得到MainViewController。现在viewController变量就是指向了正确的对象了。

注意:一旦你得到“unrecognized selector sent to instance XXX”错误,你就需要检查这个对象是不是正确类型,并且检查它真的是有那个名字的方法么。你会经常发现你调用一个你认为是这个对象的方法,因为指针变量可能没有包含这个正确值,所以导致很多的错误。

The different parts of the "Unrecognized selector" error message.

另外一个经常出现错误的原因就是将方法名称拼写错误。一会儿你将会看到一个这样的例子。(译者:我个人认为有xcode的代码提示功能,这种错误应该还是比较少吧,多数应该出现在通过selector,或者传递函数指针的时候,应该会多点这个错误)。

你的第一个内存错误

你可能已经修复了你的第一个问题。再一次运行这个程序。坑爹啊,在同样的一行,又崩溃了,但是现在是EXC_BAD_ACCESS错误。那意味着这个app有内存管理的问题。

Our first EXC_BAD_ACCESS error.

一个和内存相关的崩溃一般很难定位到源代码,因为这个恶魔可能很早就在程序中做了坏事了。假如一段有问题的代码混乱了内存结构,这样产生的蝴蝶效应可能会在之后很久才表现出来,并且总在不同的地方。

实际上,在你所有的测试中,这个bug可能永远不会出现,但是却在你的客户的设备上展露出它丑陋的脑袋。这种是很多人都不想的。

这种特别的崩溃但是却很容易修复。假如你看到你的代码编辑器,xcode其实一直就在警告你这一行代码。看到左边靠近行号的那个黄色三角形没有?那个指出一个编译警告。假如你点击那个黄色的三角形,xcode将会弹出一个“Fix-it”的建议,就像下面的一样:

Xcode warns about a missing sentinel.

这个代码使用了一系列的对象来初始化一个数组(NSArray),并且像那样的一系列的对象应该使用nil来终止,这个警告的标记就是想要表达一个这样的意思。但是代码却没有那样做,所以NSArray就很困惑,很迷茫。它试着读取一个不存在的对象,最后这个app艰难的崩溃了。

这种错误,你真的不应该犯,特别是xcode已经警告了你。修复这个错误,通过像下面一样增加一个nil(或者你可以简单的选择刚刚弹出来的菜单里面“Fix-it”):

  1. viewController.list = [NSArray arrayWithObjects:@"One", @"Two", nil];

“This class is not key value coding-compliant”

重新运行这个程序,看看为你准备的其他有趣的bug。信不信由你?它又在main.m里面崩溃了。虽然Exception Breakpoint任然起作用了,但是我们没有看见任何高亮的程序代码,这次的崩溃真的没有发生在任何程序代码里。这个调用堆栈证实了这点:这里面的方法没有一个属于的程序的,除了main():

The call stack for the "key value coding" crash.

假如你从上到下浏览一下这些方法的名字,有些问题发生在NSObject和Key-Value Coding。在那之下调用了[UIRuntimeOutletConnection connect]。我不知道那个是干什么的,但是看起来好像它做了连接outlet的一些事情。在那之下的一些方法是从nib中加载view。因此以上那些也给你一些线索。

但是,在xcode的调试窗口,并没有易懂的错误消息。那是因为没有异常被抛出。在xcode告诉你异常的原因之前,Exception Breakpoint已经暂停了这个程序。有些时候你会从Exception Breakpoint得到一些局部的错误消息,但是有些时候就得不到。

为了得到全部的错误消息,点击调试器工具栏上的“Continue Program Execution”按钮:

The Continue Program Execution button.

你可能需要点击好几次才可以,然后你将会得到错误消息:

  1. Problems[14961:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException',
  2. reason: '[ setValue:forUndefinedKey:]: this class is not
  3. key value coding-compliant for the key button.'
  4. *** First throw call stack:
  5. (0x13ba052 0x154bd0a 0x13b9f11 0x9b1032 0x922f7b 0x922eeb 0x93dd60 0x23091a 0x13bbe1a
  6. 0x1325821 0x22f46e 0xd6e2c 0xd73a9 0xd75cb 0xd6c1c 0xfd56d 0xe7d47 0xfe441 0xfe45d
  7. 0xfe4f9 0x3ed65 0x3edac 0xfbe6 0x108a6 0x1f743 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5
  8. 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7 0x11a9b 0x2872

抱歉!评论已关闭.