背景
最近遇到线上一个偶现的崩溃,简化一下问题的模型就是:
@protocol SceneDelegate <NSObject>
- (nullable NSData *)onSceneRequest;
@end
@interface MyScene : NSObject<SceneDelegate>
@end
@implementation MyScene
- (nullable NSData *)onSceneRequest {
return [NSData new];
}
@end
@interface ViewController ()
@property(nonatomic, strong) id<SceneDelegate> scene;
@end
@implementation ViewController
- (void)setScene:(id<SceneDelegate>scene {
self.scene = scene;
}
- (void)onRequest {
if (self.scene) {
NSData *data = [self.scene onSceneRequest];
NSLog(@"%@", data);
}
}
@end
崩溃的点在[self.scene onSceneRequest];
崩溃的类型是BAD_ACCESS(SIGBUS)
;
定位的过程
一开始并没有什么头绪,开始在网上扒SIGBUS
崩溃的相关资料,找到了Apple的文章 Investigating Memory Access Crashes
其中有一段:
Consult the crashed thread’s backtrace for clues on where the memory access issue is occurring. Some types of memory access issues, such as dereferencing a NULL pointer, are easy to identify when looking at the backtrace and comparing it to the source code. Other memory access issues are identified by the stack frame at the top of the crashed thread’s backtrace:
- If
objc_msgSend
,objc_retain
, orobjc_release
is at the top of the backtrace, the crash is due to a zombie object. See Investigating Crashes for Zombie Objects.
对比一下崩溃的堆栈,这不正好就是objc_msgSend
了么?
Apple如此笃定的认为,这就是一个僵死对象引起的?
然后,看代码并没有看出来哪里有什么僵死对象啊。搜遍代码就只有一个setScene修改对象的状态,即使是设置了nil,也不应该是僵死对象啊。
把焦点就围绕在setScene
和onSceneRequest
,代码深挖后,发现存在两个不同的线程分别调用这两个函数。似乎问题点就在这里了,Objective-C和Java不同,对象等号=
赋值并不是一个原子操作。
如何验证这个猜测是否正确的呢?只需要弄多几个线程,同时执行者两个方法,密集的模拟一下了。
说干就干:
- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 2; i++) {
[NSThread detachNewThreadWithBlock:^{
while(true) {
[self setScene:[MyScene new]];
[NSThread sleepForTimeInterval:0.3];
}
}];
}
for (int i = 0; i < 10; i++) {
[NSThread detachNewThreadWithBlock:^{
while(true) {
[self onRequest];
[NSThread sleepForTimeInterval:0.5+(CGFloat)i/10.0];
}
}];
}
}
创建两个线程,每隔0.3s就调用setScene
,创建10个不同的线程,以不同的时间间隔不断的调用onRequest
。
好样的,跑起来,不到1分钟,就出现一个崩溃了。
好,就是这个原因了,修改方法就是atomic
,对,就那么简单:)
@property(atomic, strong) id<SceneDelegate> scene;
背后的原因
为何对属性赋值的操作不是原子性的呢?
Objective-C的运行时的各个方法都是没有上锁的,大致理了一下多线程下是如何导致崩溃的:
所以,属性的访问修饰里才会有nonatomic和atomic的选择。
很多时候,我们都只是默默的写nonatomic,而没有思考什么时候才需要atomic。
注意:atomic并不能解决多线程的竞争,它只能解决这种指针错误的崩溃。
关于nonatomic和atomic的区别,可以参考这个文章。
也可以阅读Apple的文章
SO上也有一些精彩的讨论
注意:上述的文章都有讲述到属性的原子性只是保证访问属性对象的时候是线程安全的,并没有说访问属性对象内部数据是线程安全的。
如果存在多线程访问和修改属性内部的情况,需要做额外的线程安全措施。