Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is UIViewController deallocated on the main thread?

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).

like image 687
s1m0n Avatar asked Aug 14 '15 09:08

s1m0n


1 Answers

tl;dr - While I can find no official documentation, the current implementation does indeed ensure that dealloc for UIViewController happens on the main thread.


I guess I could just give a simple answer, but maybe I can do a little "teach a man to fish" today.

OK. I can't find documentation for this anywhere, and I don't remember it ever being said publicly either. In fact, I have always gone out of my way to make sure view controllers were deallocated on the main thread, and this is the first time I've ever seen someone indicate that UIViewController objects get automatically deallocated on the main thread.

Maybe someone else can find an official statement, but I couldn't find one.

However, I do have some evidence to prove that it does indeed happen. Actually, at first, I thought you were not properly handling your blocks or reference counts, and somehow a reference was being retained on the main thread.

However, after a cursory look, I was interested enough to try it for myself. To satisfy my curiosity, I made a class similar to yours that inherited from UIViewController. Its dealloc ran on the main thread.

So, I just changed the base class to UIResponder, which is the base class of UIViewController, and ran it again. This time its dealloc ran on the background thread.

Hmmm. Maybe there is something going on behind closed doors. We have lots of debugging tricks. The answer always lies with the last one you try, but I figured I'd try my usual bag of tricks for this kind of stuff.

Log Notifications

One of my favorite tools to find out how things are implemented is to log all notifications.

[[NSNotificationCenter defaultCenter]
    addObserverForName:nil
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *note) { NSLog(@"%@", note); }];

I then ran using both classes, and didn't see anything unexpected or different between the two. I didn't expect to, but that little trick is very simple, and it has helped me tremendously in discovering how a lot of other things worked, so it's usually first.

Log Method/Message Sends

My second trick it to enable method logging. However, I don't want to log all methods, just what happens between the time the last block executes, and the call to dealloc. So, turned on method logging by adding this as the last line of the "sleeping" block.

instrumentObjcMessageSends(YES);

And I turned logging back off, with this as the first line of the dealloc method.

instrumentObjcMessageSends(NO);

Now, this C function can't be readily found in any headers that I know of, so you need to declare it at the top of your file.

extern void instrumentObjcMessageSends(BOOL);

The logs go into a unique file in /tmp, named msgSends-.

The files for the two runs contained the following output.

$ cat msgSends-72013
- __NSMallocBlock__ __NSMallocBlock release
- SOUnsafeObject SOUnsafeObject dealloc

$ cat msgSends-72057
- __NSMallocBlock__ __NSMallocBlock release
- SOUnsafeObject UIViewController release
- SOUnsafeObject SOUnsafeObject dealloc

There is not too much surprising about that. However, the presence of UIViewController release indicates that UIViewController has a special override implementation for the +release method. I wonder why? Could it be to specifically transfer the call to dealloc to the main thread?

Debugger

Yes, this is the first thing I thought of, but I had no evidence that there was an override in UIViewController so I went through my normal process. I have found when I skip steps, it typically takes longer.

Anyway, now that we know what we are looking for, I put a breakpoint on the last line of the "sleeping" block and made the class inherit from UIViewController.

When I hit the breakpoint, I added a manual breakpoint...

(lldb) b [UIViewController release]
Breakpoint 3: where = UIKit`-[UIViewController release], address = 0x000000010e814d1a

After continuing, I was greeted with this awesome assembly, which confirms visually what is happening.

enter image description here

pthread_main_np is a function that tells you if you are running on the main thread. Stepping through the assembly instructions confirmed that we are not running on the main thread.

Stepping further, we get to line 27, where we jump over the call to dealloc, and instead run what you can easily see is code to run a dealloc-helper on the main thread.

Can You Count on This Going Forward?

Since I can't find it documented, I don't know that I would count on this happening all the time, but it is very convenient, and obviously something they intentionally put into the code.

I have a set of tests that I run every time Apple releases a new version of iOS and OSX. I assume most developers do something very similar. I think what I would do is write a unit test, and add it to that set. Thus, if they ever change it back, I'll know as soon as it comes out.

Otherwise, I tend to think this may be one of those things that can safely be assumed.

However, be aware that subclasses may choose to override release (if they are compiled with ARC disabled), and if they do not call the base class implementation, you will not get this behavior.

Thus, you may want to write tests for any third-party view controller classes you use.

My Details

I only tested this with XCode 6.4, Deployment target 8.4, simulating iPhone 6. I'll leave testing with other versions as an exercise for the reader.

BTW, if you don't mind, what are the details for your posted example?

like image 129
Jody Hagins Avatar answered Oct 13 '22 21:10

Jody Hagins