Block_Release Deallocating UI Objects on a Background Thread

Block_release deallocating UI objects on a background thread

You can use the __block storage type qualifier for such a case. __block variables are not automatically retained by the block. So you need to retain the object by yourself:

__block UIViewController *viewController = [myViewController retain];
dispatch_async(backgroundQueue, ^{
// Do long-running work here.
dispatch_async(dispatch_get_main_queue(), ^{
[viewController updateUIWithResults:results];
[viewController release]; // Ensure it's released on main thread
}
});

EDIT

With ARC, __block variable object is automatically retained by the block, but we can set nil value to the __block variable for releasing the retained object whenever we want.

__block UIViewController *viewController = myViewController;
dispatch_async(backgroundQueue, ^{
// Do long-running work here.
dispatch_async(dispatch_get_main_queue(), ^{
[viewController updateUIWithResults:results];
viewController = nil; // Ensure it's released on main thread
}
});

Why is UIViewController deallocated on the main thread?

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?

dealloc on Background Thread

Yes, it is an error to make a UIViewController releasing in a background thread (or queue). In UIKit, dealloc is not thread safe. This is explicitly described in Apple's TN2109 doc:

When a secondary thread retains the target object, you have to ensure that the thread releases that reference before the main thread releases its last reference to the object. If you don't do this, the last reference to the object is released by the secondary thread, which means that the object's -dealloc method runs on that secondary thread. This is problematic if the object's -dealloc method does things that are not safe to do on a secondary thread, something that's common for UIKit objects like a view controller.

However, it's quite hard to respect this rule and if you put precondition(Thread.isMainThread) in all your view controller dealloc/deinit(), you probably would notice very weird (and hard to fix) cases in which this rule is not respected.

It seems that Apple is aware of this fragility and is moving away from this rule. In fact, when you annotate a class with @MainActor, everything is ensured to be executed in the Main Thread, except deinit(). In Swift 6, the compiler prevents you from calling main actor code from deinit().

Even if in the past we have been asked to ensure deallocation in the main thread, in the future we will be asked to ensure that deallocation can happen in any thread.

Can I call [self retain] within -dealloc? Or, how do I ensure dealloc happens on the main thread?

Why not just override release?

- (void)release
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO];
} else {
[super release];
}
}

Edit: This was pre-ARC. Don't do it with ARC.

Using block callbacks to the main thread from an NSOperation subclass (ARC)

If you need to ensure the callback runs even if the controller has been popped from the stack, then your workaround is correct.

If, however, you really only need the callback to run if the controller is still around, then it would be simpler to use weak references in the callback to ensure that the block itself doesn't retain the controller in the first place. It would look something like this:

- (void)demoMethod {
__weak id weakSelf = self;
MySubclass *subclass = [[MySubclass alloc] initWithCallback:^{
if (!weakSelf) {
return;
}
else {
// Do whatever the callback does here
}
}];

// Do something with `subclass` here
}

A blocks for-loop with UI callbacks

Way too complicated thinking. Current setup:

  1. do one piece of work
  2. hand control over to background thread
  3. sleep on background
  4. schedule next piece of work on main thread

What you want to do:

  1. do one piece of work
  2. schedule next piece of work

Here's the code:

- (void)doWork
{
if (_assetIndex < assets.count) {
[self obtainAssetAtIndex:_assetIndex++];
progressView.progress += progressPerFile;
[self performSelector:@selector(doWork) withObject:nil afterDelay:0];
} else {
[self didImportGroup];
}
}

You don't need a background thread at all (especially when all it's ever doing is sleeping).

UIWebView in multithread ViewController

I had the same solution where a background thread was the last release, causing dealloc of view controller to happen in a background thread ending up with the same crash.

The above [[self retain] autorelease] would still result in the final release happening from the autorelease pool of the background thread. (Unless there's something special about releases from the autorelease pool, I'm surprised this would make a difference).

I found this as my ideal solution, placing this code into my view controller class:

- (oneway void)release
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO];
} else {
[super release];
}
}

This ensures that the release method of my view controller class is always executed on the main thread.

I'm a little surprised that certain objects which can only be correctly dealloc'ed from the main thread don't already have something like this built in. Oh well...

Best/most common way to update UI from background thread

This is the way to go:

let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some background task
dispatch_async(dispatch_get_main_queue()) {
// update some UI
}
}

Retrieve application state from background thread?

Setup notifications for changes to the state on the main thread and assign them atomically to a variable. That variable can now be accessed atomically as well from your background thread.



Related Topics



Leave a reply



Submit