今年的Apple发布会也开完了,没有什么太出彩的地方。不过广受非议的iPhone 7依然大卖。群里、微信里都是各种讨论外加各种炫,而我只能静静地看着,等着公司的测试机了。
每次都感叹时间过得快,总是有各种事情,这一晃又三个星期了,哎。这期整理了之前的5个问题,无规则无主题,大伙慢慢看:
- block未判空导致的EXC_BAD_ACCESS崩溃;
- 多Target开发;
- dispatch_sync导致死锁;
- makeObjectsPerformSelector:;
- NSSetUncaughtExceptionHandler
block未判空导致的EXC_BAD_ACCESS崩溃
我们在调用block时,如果这个block为nil,则程序会崩溃,报类似于EXC_BAD_ACCESS(code=1, address=0xc)异常[32位下的结果,如果是64位,则address=0x10]。如下图所示,这个异常表示程序在试图读取内存地址0xc的信息时出错。
在定义一个block时,编译器会在栈上创建一个结构体,类似于图2的结构体。
|
|
block就是指向这个结构体的指针。其中的invoke就是指向具体实现的函数指针。当block被调用时,程序最终会跳转到这个函数指针指向的代码区。而当block为nil时,程序就会试图去读取0xc地址的信息,而这个地址什么都不会有(duff address),于是抛出一个segmentation fault。在32位系统下,之所以是0xc,是因为invoke前面的三个成员变量的大小正好是12。
所以我们在使用block时,应该首先去判断block是否为空。一种比较优雅的写法是:
|
|
参考
多Target开发
在Xcode中,一个target表示工程中的一个product,target用于组织product所需要的源文件、资源文件、配置信息等。
在一些情况下,我们可以为一个工程设置多个target,如:同时开发Lite版和正式版;开发版本和发布版本需要不同配置;单工程构建多个相似的App等等。如下图所示。
这么做的好处是在共用一份代码的情况下,可以为不同的target配置不同的资源、信息等,如不同的Info.plist, Build Setting, Build Phase配置等,最后得到不同的product。
参考
dispatch_sync导致死锁
dispatch_sync函数用于将一个block提交到队列中同步执行,直到block执行完后,这个函数才会返回。
这里有一个潜在的问题,如果我们在某个串行队列中调用dispatch_sync,并将其block提交到这个串行队列中执行,则会引发死锁。如下代码所示。
|
|
其实还是很好理解,在com.apple.test这个串行队列中,我们执行一个task A,在这个task A中,我们又向队列提交了一个同步的task B。由于是串行队列,task A在task B之前,所以task B的执行依赖于task A的完成,而task B又包含在task A中,task A的完成依赖于task B的完成。这样就成了一个死锁。
所以,千万不要在主队列中这样调用dispatch_sync,否则会导致主线程卡死。
当然,如果在并行队列中这样使用是没有问题的,如下代码所示,可以正常打印出B,A。
|
|
又或是dispatch_sync的目标队列不是当前队列,如下代码所示,也可以正常打印出B,A。
|
|
我们在使用dispatch_sync提交task时,可以看到大部分情况下task是在dispatch_sync所在的上下文线程中执行,而不管dispatch_sync指定的队列是什么【串行或并行】,如下代码所示:
|
|
官方文档给我们的解释是这么做的目的是为了优化性能:
As an optimization, this function invokes the block on the current thread when possible。
我们需要了解的是队列和线程并不是一回事。我们将任务以block的形式提交到队列,然后由GCD来决定将队列中的block分发到系统管理的线程池中的某个线程中去执行。
参考
makeObjectsPerformSelector:
遍历一个数组的方法有几种,for, forin, enumerateObjectsUsingBlock:方法。现在用得比较多的可能是enumerateObjectsUsingBlock:,它能很方便地让我们获取到数组中的元素及对应的索引,然后根据这些信息做一些操作,如下代码所示:
|
|
不过,如果在循环中,只是想调用元素的某一个方法,则可以考虑使用makeObjectsPerformSelector:或者makeObjectsPerformSelector:withObject:,这两个方法会按元素的顺序向数组中的每个元素发送Selector指定的消息。如下代码所示:
|
|
当然,Selector不能是NULL,否则会抛NSInvalidArgumentException异常。大家如果熟悉runtime的话,应该知道消息机制是如何处理调用不存在方法的。
NSSetUncaughtExceptionHandler
Foundation里面提供了一个NSSetUncaughtExceptionHandler函数,可以设置一个顶层异常处理函数,让我们在程序发生异常并终止前,有最后的机会来捕获并输出异常信息,如下代码所示。
|
|
这个函数的参数是一个函数指针,指向的函数其签名是:void NSUncaughtExceptionHandler(NSException *exception)。可以看到这个函数有参数是一个NSException对象,通过这个对象我们就可以获取到异常的信息。假定发生数组越界异常时,会有如下输出。
|
|
不过这个函数有效范围局限于异常,还有很多错误是无法处理的,如EXC_BAD_ACCESS内存访问错误,这类错误抛出的是Signal,需要专门做Signal处理。
小结
Crash始终是我们开发最大最头疼的问题,总会有各种各样的Crash情况出现。看着Fabric里面长长的Crash列表,总是很伤感的。我们的成长史也是一部和Bug战斗的斗争史,自己写的Bug,熬夜也要把它们搞完。继续战斗吧,Bug君。
欢迎关注我的微信公众号:iOS知识小集,扫扫左边站点概览里的二维码就OK了。对了,还有微博:@南峰子_老驴。