I recently stumbled upon The Deallocation Problem in some Objective-C code. This topic was discussed before on Stack Overflow in Block_release deallocating UI objects on a background thread. I think I understand the problem and its implications, but to be sure I wanted to reproduce it in a little test project. I first created my own SOUnsafeObject
(= an object which should always be deallocated on the main thread).
@interface SOUnsafeObject : NSObject
@property (strong) NSString *title;
- (void)reloadDataInBackground;
@end
@implementation SOUnsafeObject
- (void)reloadDataInBackground {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"Retrieved data";
});
sleep(3);
});
}
- (void)dealloc {
NSAssert([NSThread isMainThread], @"Object should always be deallocated on the main thread");
}
@end}]
Now, as expected, if I put [[[SOUnsafeObject alloc] init] reloadDataInBackground];
inside application:didFinishLaunching..
the app crashes after 3 seconds due to the failed assertion. The proposed fix seems to work. I.e. the app doesn't crash anymore if I change the implementation of reloadDataInBackground
to:
__block SOUnsafeObject *safeSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
safeSelf.title = @"Retrieved data";
safeSelf = nil;
});
sleep(3);
});
Okay, so it seems like my understanding about the problem and how it can be solved under ARC is correct. But just to be 100% sure.. Let's try the same with an UIViewController
(since an UIViewController
will probably fill in the role of SOUnsafeObject
in real life). The implementation is almost identical to that of the SOUnsafeObject
:
@interface SODemoViewController : UIViewController
- (void)reloadDataInBackground;
@end
@implementation SODemoViewController
- (void)reloadDataInBackground {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"Retrieved data";
});
sleep(3);
});
}
- (void)dealloc {
NSAssert([NSThread isMainThread], @"UI objects should always be deallocated on the main thread");
NSLog(@"I'm deallocated!");
}
@end
Now, let's put [[SODemoViewController alloc] init] reloadDataInBackground];
inside application:didFinishLaunching..
. Hmm, the assertion doesn't fail.. The message I'm deallocated!
is printed to the console after 3 seconds so I'm pretty sure the view controller is getting deallocated.
Why is the view controller deallocated on the main thread while the unsafe object is deallocated on a background thread? The code is nearly identical. Does UIKit do some fancy stuff behind the scenes to make sure an UIViewController
is always deallocated on the main thread? I'm starting to suspect this since the following snippet also doesn't break my assertion:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
SODemoViewController()
});
If so, is this behavior documented somewhere? Can this behavior be relied upon? Or am I just totally wrong and is there something obvious I'm missing here?
Notes: I'm fully aware of the fact that I can use a __weak
reference here, but let's assume the view controller should still be alive to execute our completion code on the main thread. Also, I'm trying to understand the core of the problem here before I circumvent it. I converted the code to Swift and got the same results as in Objective-C (the fix for SOUnsafeObject
is there syntactically even uglier).