iOS知识小集 第8期(2016.09.20)

今年的Apple发布会也开完了,没有什么太出彩的地方。不过广受非议的iPhone 7依然大卖。群里、微信里都是各种讨论外加各种炫,而我只能静静地看着,等着公司的测试机了。

每次都感叹时间过得快,总是有各种事情,这一晃又三个星期了,哎。这期整理了之前的5个问题,无规则无主题,大伙慢慢看:

  1. block未判空导致的EXC_BAD_ACCESS崩溃;
  2. 多Target开发;
  3. dispatch_sync导致死锁;
  4. makeObjectsPerformSelector:;
  5. NSSetUncaughtExceptionHandler

block未判空导致的EXC_BAD_ACCESS崩溃

我们在调用block时,如果这个block为nil,则程序会崩溃,报类似于EXC_BAD_ACCESS(code=1, address=0xc)异常[32位下的结果,如果是64位,则address=0x10]。如下图所示,这个异常表示程序在试图读取内存地址0xc的信息时出错。

在定义一个block时,编译器会在栈上创建一个结构体,类似于图2的结构体。

1
2
3
4
5
6
7
8
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
}

block就是指向这个结构体的指针。其中的invoke就是指向具体实现的函数指针。当block被调用时,程序最终会跳转到这个函数指针指向的代码区。而当block为nil时,程序就会试图去读取0xc地址的信息,而这个地址什么都不会有(duff address),于是抛出一个segmentation fault。在32位系统下,之所以是0xc,是因为invoke前面的三个成员变量的大小正好是12。

所以我们在使用block时,应该首先去判断block是否为空。一种比较优雅的写法是:

1
!block ?: block()

参考

  1. Why do nil / NULL blocks cause bus errors when run?

多Target开发

在Xcode中,一个target表示工程中的一个product,target用于组织product所需要的源文件、资源文件、配置信息等。

在一些情况下,我们可以为一个工程设置多个target,如:同时开发Lite版和正式版;开发版本和发布版本需要不同配置;单工程构建多个相似的App等等。如下图所示。

这么做的好处是在共用一份代码的情况下,可以为不同的target配置不同的资源、信息等,如不同的Info.plist, Build Setting, Build Phase配置等,最后得到不同的product。

参考

  1. Xcode Target
  2. 猿题库iOS客户端的技术细节(一):使用多target来构建大量相似App

dispatch_sync导致死锁

dispatch_sync函数用于将一个block提交到队列中同步执行,直到block执行完后,这个函数才会返回。

这里有一个潜在的问题,如果我们在某个串行队列中调用dispatch_sync,并将其block提交到这个串行队列中执行,则会引发死锁。如下代码所示。

1
2
3
4
5
6
7
8
9
10
// 死锁
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"B");
});
NSLog(@"A");
});

其实还是很好理解,在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。

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"B");
});
NSLog(@"A");
});

又或是dispatch_sync的目标队列不是当前队列,如下代码所示,也可以正常打印出B,A。

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue1 = dispatch_queue_create("com.apple.test1", NULL);
dispatch_queue_t queue2 = dispatch_queue_create("com.apple.test2", NULL);
dispatch_async(queue1, ^{
dispatch_sync(queue2, ^{
NSLog(@"B");
});
NSLog(@"A");
});

我们在使用dispatch_sync提交task时,可以看到大部分情况下task是在dispatch_sync所在的上下文线程中执行,而不管dispatch_sync指定的队列是什么【串行或并行】,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 串行队列
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100303310>{number = 1, name = main}
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100303310>{number = 1, name = main}
});
// 并行队列
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100505ea0>{number = 2, name = (null)}
dispatch_queue_t queue = dispatch_queue_create("com.apple.test", NULL);
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]); // <NSThread: 0x100505ea0>{number = 2, name = (null)}
});
});

官方文档给我们的解释是这么做的目的是为了优化性能:

As an optimization, this function invokes the block on the current thread when possible。

我们需要了解的是队列和线程并不是一回事。我们将任务以block的形式提交到队列,然后由GCD来决定将队列中的block分发到系统管理的线程池中的某个线程中去执行。

参考

  1. dispatch_sync

makeObjectsPerformSelector:

遍历一个数组的方法有几种,for, forin, enumerateObjectsUsingBlock:方法。现在用得比较多的可能是enumerateObjectsUsingBlock:,它能很方便地让我们获取到数组中的元素及对应的索引,然后根据这些信息做一些操作,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Test *t = [[Test alloc] init];
t.index = index;
[array addObject:t];
}
[array enumerateObjectsUsingBlock:^(Test * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj test];
}];

不过,如果在循环中,只是想调用元素的某一个方法,则可以考虑使用makeObjectsPerformSelector:或者makeObjectsPerformSelector:withObject:,这两个方法会按元素的顺序向数组中的每个元素发送Selector指定的消息。如下代码所示:

1
2
3
4
5
6
7
8
9
10
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger index = 0; index < 10; index++) {
Test *t = [[Test alloc] init];
t.index = index;
[array addObject:t];
}
[array makeObjectsPerformSelector:@selector(test)];
[array makeObjectsPerformSelector:@selector(testWithNumber:) withObject:@10];

当然,Selector不能是NULL,否则会抛NSInvalidArgumentException异常。大家如果熟悉runtime的话,应该知道消息机制是如何处理调用不存在方法的。

NSSetUncaughtExceptionHandler

Foundation里面提供了一个NSSetUncaughtExceptionHandler函数,可以设置一个顶层异常处理函数,让我们在程序发生异常并终止前,有最后的机会来捕获并输出异常信息,如下代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UncaughtExceptionHandler(NSException *exception) {
NSArray *symbols = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSLog(@"reason = %@", reason);
NSLog(@"name = %@", name);
NSLog(@"symbols = %@", symbols);
}
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
return YES;
}
@end

这个函数的参数是一个函数指针,指向的函数其签名是:void NSUncaughtExceptionHandler(NSException *exception)。可以看到这个函数有参数是一个NSException对象,通过这个对象我们就可以获取到异常的信息。假定发生数组越界异常时,会有如下输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2016-09-20 11:55:36.719 Test111[5548:199035] reason = *** -[__NSSingleObjectArrayI objectAtIndex:]: index 10 beyond bounds [0 .. 0]
2016-09-20 11:55:36.720 Test111[5548:199035] name = NSRangeException
2016-09-20 11:55:36.720 Test111[5548:199035] symbols = (
0 CoreFoundation 0x0000000106cef34b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010675021e objc_exception_throw + 48
2 CoreFoundation 0x0000000106d47bdf -[__NSSingleObjectArrayI objectAtIndex:] + 111
3 Test111 0x000000010617d87b -[AppDelegate application:didFinishLaunchingWithOptions:] + 235
4 UIKit 0x000000010710968e -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 290
5 UIKit 0x000000010710b013 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4236
6 UIKit 0x00000001071113b9 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1731
7 UIKit 0x000000010710e539 -[UIApplication workspaceDidEndTransaction:] + 188
8 FrontBoardServices 0x000000010a2ee76b __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
9 FrontBoardServices 0x000000010a2ee5e4 -[FBSSerialQueue _performNext] + 189
10 FrontBoardServices 0x000000010a2ee96d -[FBSSerialQueue _performNextFromRunLoopSource] + 45
11 CoreFoundation 0x0000000106c94311 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
12 CoreFoundation 0x0000000106c7959c __CFRunLoopDoSources0 + 556
13 CoreFoundation 0x0000000106c78a86 __CFRunLoopRun + 918
14 CoreFoundation 0x0000000106c78494 CFRunLoopRunSpecific + 420
15 UIKit 0x000000010710cdb6 -[UIApplication _run] + 434
16 UIKit 0x0000000107112f34 UIApplicationMain + 159
17 Test111 0x000000010617db4f main + 111
18 libdyld.dylib 0x000000010928a68d start + 1
19 ??? 0x0000000000000001 0x0 + 1
)

不过这个函数有效范围局限于异常,还有很多错误是无法处理的,如EXC_BAD_ACCESS内存访问错误,这类错误抛出的是Signal,需要专门做Signal处理。

小结

Crash始终是我们开发最大最头疼的问题,总会有各种各样的Crash情况出现。看着Fabric里面长长的Crash列表,总是很伤感的。我们的成长史也是一部和Bug战斗的斗争史,自己写的Bug,熬夜也要把它们搞完。继续战斗吧,Bug君。


欢迎关注我的微信公众号:iOS知识小集,扫扫左边站点概览里的二维码就OK了。对了,还有微博:@南峰子_老驴