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的结构体。

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是否为空。一种比较优雅的写法是:


!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提交到这个串行队列中执行,则会引发死锁。如下代码所示。

/ 死锁
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。

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。

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指定的队列是什么【串行或并行】,如下代码所示:

// 串行队列
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:,它能很方便地让我们获取到数组中的元素及对应的索引,然后根据这些信息做一些操作,如下代码所示:

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指定的消息。如下代码所示:

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函数,可以设置一个顶层异常处理函数,让我们在程序发生异常并终止前,有最后的机会来捕获并输出异常信息,如下代码所示。

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对象,通过这个对象我们就可以获取到异常的信息。假定发生数组越界异常时,会有如下输出。

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了。对了,还有微博:@南峰子_老驴

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • 程序中同步和异步是什么意思?有什么区别? 解释一:异步调用是通过使用单独的线程执行的。原始线程启动异步调用,异步调...
    风继续吹0阅读 1,027评论 1 2
  • GCD (Grand Central Dispatch) :iOS4 开始引入,使用更加方便,程序员只需要将任务添...
    池鹏程阅读 1,326评论 0 2
  • iOS中GCD的使用小结 作者dullgrass 2015.11.20 09:41*字数 4996阅读 20199...
    DanDanC阅读 820评论 0 0
  • 一、GCD的API 1. Dispatch queue 在执行处理时存在两种Dispatch queue: 等待现...
    doudo阅读 498评论 0 0
  • 又是一年将尽时。年底总要搞点大事情, 不 然 都 没 法 安 心 过 年。 盘点16年,大事不断,小事新鲜, 1月...
    酒庄惠小九阅读 394评论 0 0