background

We recently encountered an occasional crash online. To simplify the problem, the model looks like this:

@protocol SceneDelegate<NSObject>
- (nullable NSData *)onSceneRequest;
@end
@interface MyScene : NSObject<SceneDelegate>
@end
@implementation MyScene
- (nullable NSData *)onSceneRequest {
    return [NSData new];
}
@end
@interface ViewController(a)
@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
Copy the code

The crash point is in [self.scene onSceneRequest]; The crash type is BAD_ACCESS (SIGBUS);

Process of positioning

I had no idea at first, but I started looking online at the SIGBUS crash and found an Apple article Investigating Memory Access Crashes

In one passage:

Consult the crashed thread’s backtrace for clues on where the memory access issue is zip. 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, or objc_release is at the top of the backtrace, the crash is due to a zombie object. See Investigating Crashes for Zombie Objects.

Compare that to a crashed stack. Isn’t that objc_msgSend?

Is Apple so sure that this is caused by a dead object?

However, the code doesn’t show any zombie objects anywhere. So if you go through the code there’s just a setScene that changes the state of the object, even if it’s set to nil, it shouldn’t be a zombie object.

Focusing on setScene and onSceneRequest, we dig deep and discover that there are two different threads calling these two functions. This seems to be where the problem lies. Unlike Java, ObjC does not have to be atomic to assign an object equal sign.

How do you test this guess? Just get a few more threads, execute both methods at the same time, and simulate intensively:

- (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]; }}]; }}Copy the code

Create two threads, call setScene every 0.3s, create 10 different threads, call onRequest at different intervals. Good run, less than a minute, there was a crash. Ok, that’s the reason, the modification method is atomic, yes, simple 🙂

@property(atomic, strong) id>SceneDelegate> scene;
Copy the code

The reason behind it

Why isn’t the assignment of an attribute atomic?

ObjC’s runtime methods are unlocked, and a broken interaction might look like this:

That’s why nonatomic and atomic options are available in the attribute access modifier. Most of the time, we just write nonatomic without thinking about when we need them.

Note: Atomic does not solve multithreaded races, it can only solve pointer error crashes.

For the difference between nonatomic and atomic, see this article.

You can also read the Apple article

There are also some excellent discussions on SO

Note: All of the above articles have addressed the issue that the atomicity of properties only ensures thread-safe access to property objects, not thread-safe access to property objects’ internal data.

If you have multiple threads accessing and modifying properties inside, do additional thread-safety measures.