NSAssert与dispatch_once
相信大家公司的代码中多多少少存在一些断言(例如NSAssert)。一种常见的断言场景是:SDK的开发者为了避免SDK的初始化方法与功能接口,会在功能接口中判断是否已经初始化,否则就触发断言。当然还有各种各样其他场景。
探索 NSAssert
本文探索下Objective C的断言方法NSAssert的一种现象。这个现象比较细节,不太好描述,咱们就直接聊吧。
假设有下面的断言:
NSAssert(NO, @"should not call this");
当断言代码有源码时,触发时如下图:
完整的callstack如下:
由于有源码,Xcode很智能的把编辑器定位到了NSAssert的那一行。同时我们也知道了另一个信息,NSAssert其实就是产生了一个Exception,Exception会触发 objc_exception_throw
这个c函数。
与GCD作用
但如果公司内推行过把Pod转为静态库(为了加快编译速度,一般团队人数多的产品都会这么做),NSAssert那一行没有源码,那很可能Callstack会如下图:
当然并不是只有没源码时会像上图这样。如果断言在GCD的一些block中,而且上下文也没有源码,也会像上图这样。例如下面的代码,就会导致Xcode不能断点到代码行。
为什么会这样呢?看下详细的Callstack:
仔细看了看,这里并没有 objc_exception_throw
。那我们加上符号断点看下。
没问题,这个方法是调用了的。我们看下objc_exception_throw
的实现。
https://opensource.apple.com/tarballs/objc4/
下载最新的代码。找到这个方法,如下。
看完似乎没啥想法。
我们再看看GCD的这两个方法,
再从这里找到 libdispatch 的代码: https://opensource.apple.com/tarballs/libdispatch/
这下就明白了,_dispatch_client_callout 把GCD block中的OC Exception捕获了,然后直接 objc_terminate。也就是这里,导致Callstack断开了。
这个问题暂告于段落。
dispatch_once
为了启动优化,我写了一个启动器的代码,为了避免内部代码执行多次,增加了一个 dispatch_once。启动器中执行各类启动逻辑。然而,有一段时间,总有人说我的代码Crash。
大概情况如下:
左侧myRunner表示启动器。从上图看,确实Crash到了我的代码中。
然而实际情况呢?
因为dispatch_once中的代码throw了OC异常。一般大公司初期这种情况经常遇到,后期一般都会针对断言专门开发一些代码用来定位Owner,结果由于 dispatch_once 导致都找到了我。
最简单的解决方法是,加个异常断点。(也就是符号断点 objc_exception_throw)
别小看这个操作哈,我见过很多开发同学不知道这个操作(这可能是小公司iOS同学进入大公司的第一个必备技能)。
再想下原因,看下Callstack
还是存在 _dispatch_client_callout 。但稍微不同之处是,dispatch_once的这个方法是inline的,写到了头文件里
Xcode会尝试在Callstack中找到最后一个有匹配代码的行,并定位到这行,显示给开发者。
怎么解决
原因弄清楚了。那怎么绕过这个问题呢?目前看来不是用GCD即可。
例如:C++的std::call_once。
再例如利用static变量的自带锁(这个可以写篇文章探索一下)
更多方法参考: https://stackoverflow.com/questions/8412630/how-to-execute-a-piece-of-code-only-once
大家喜欢的话,就关注下订阅号,以示鼓励: