为什么将UIViewController释放在主线程上? [英] Why is UIViewController deallocated on the main thread?

查看:100
本文介绍了为什么将UIViewController释放在主线程上?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我最近偶然发现某些Objective-C代码中的解除分配问题.之前在>上的Block_release释放UI对象的堆栈溢出中讨论了此主题.后台线程.我想我了解这个问题及其含义,但是可以肯定的是,我想在一个小的测试项目中重现它.我首先创建了自己的SOUnsafeObject(=应该始终在主线程上释放的对象).

@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}]

现在,按预期方式,如果我将[[[SOUnsafeObject alloc] init] reloadDataInBackground];放入application:didFinishLaunching..中,则该应用将在3秒后由于断言失败而崩溃.建议的修复程序似乎有效. IE.如果将reloadDataInBackground的实现更改为:

,该应用程序将不再崩溃

__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);
});

好的,所以看来我对问题及其在ARC下的解决方法的理解是正确的.但是请确保100%确信.让我们对UIViewController进行相同的尝试(因为UIViewController可能会在现实生活中扮演SOUnsafeObject的角色).实现几乎与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

现在,让我们将[[SODemoViewController alloc] init] reloadDataInBackground];放入application:didFinishLaunching..内.嗯,断言不会失败..消息I'm deallocated!在3秒钟后打印到控制台,因此我很确定视图控制器已被释放.

为什么不安全的对象被释放在后台线程上时,视图控制器为什么被释放在主线程上?代码几乎相同. UIKit 是否在幕后做一些花哨的事情,以确保始终在主线程上重新分配UIViewController?我开始怀疑这一点,因为以下代码段也不会破坏我的断言:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
    SODemoViewController()
});

如果是,此行为是否记录在某处?可以依靠这种行为吗?还是我只是完全错了,这里有什么明显的我想念的东西吗?

注意事项:我完全意识到我可以在此处使用__weak引用的事实,但是让我们假设视图控制器应该仍然活着,可以在主线程上执行我们的完成代码.另外,在尝试解决此问题之前,我正在尝试了解问题的核心.我将代码转换为Swift,并获得了与Objective-C相同的结果(SOUnsafeObject的修复在语法上更加丑陋).

解决方案

tl; dr -虽然我找不到官方文档,但是当前的实现确实确保了dealloc用于UIViewController发生在主线程上.


我想我只能给出一个简单的答案,但是也许我今天可以做些教人钓鱼"的事情.

好.我在任何地方都找不到此文档,而且我也不记得曾经公开说过它.实际上,我总是竭尽全力确保将视图控制器分配在主线程上,这是我第一次看到有人指出UIViewController对象会自动分配在主线程上. /p>

也许其他人可以找到一份正式声明,但我找不到.

但是,我确实有证据证明确实如此.实际上,起初,我认为您没有正确处理块或引用计数,并且某种程度上在主线程上保留了引用.

但是,经过粗略的看后,我很有兴趣亲自尝试一下.为了满足我的好奇心,我制作了一个类似于您的从UIViewController继承的类.它的dealloc运行在主线程上.

因此,我只是将基类更改为UIViewController的基类UIResponder,然后再次运行它.这次,它的dealloc在后台线程上运行.

嗯.也许闭门造车.我们有很多调试技巧.答案总是取决于您尝试的最后一个方法,但是我认为我会尝试使用通常的技巧来解决这类问题.

日志通知

我最喜欢的用来发现事情实现方式的工具之一是记录所有通知.

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

然后我同时使用了两个类,没有发现两者之间有什么意外或不同之处.我没想到,但是这个小技巧非常简单,它极大地帮助了我发现许多其他事情的工作原理,因此通常是第一时间.

记录方法/消息发送

我的第二个技巧是启用方法日志记录.但是,我不想记录所有方法,仅记录最后一个块执行到调用dealloc之间发生的情况.因此,通过将其添加为"sleeping"块的最后一行来打开方法日志记录.

instrumentObjcMessageSends(YES);

然后我关闭了登录,这是dealloc方法的第一行.

instrumentObjcMessageSends(NO);

现在,在我所知道的任何标头中都找不到这个C函数,因此您需要在文件顶部声明它.

extern void instrumentObjcMessageSends(BOOL);

日志进入/tmp中名为msgSends-的唯一文件.

两次运行的文件包含以下输出.

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

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

对此并不太令人惊讶.但是,UIViewController release的存在指示UIViewController+release方法具有特殊的替代实现.我想知道为什么?可以专门将对dealloc的调用转移到主线程吗?

调试器

是的,这是我首先想到的,但是我没有证据表明UIViewController中存在替代项,因此我按正常流程进行了操作.我发现跳过步骤时通常会花费更长的时间.

无论如何,现在我们知道了要查找的内容,我在"sleeping"块的最后一行放置了一个断点,并使该类继承自UIViewController.

当我碰到断点时,我添加了一个手动断点...

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

继续后,我迎来了这个很棒的程序集,它可以从视觉上确认正在发生的事情.

pthread_main_np是一个告诉您是否在主线程上运行的函数.逐步执行汇编指令,确认我们没有在主线程上运行.

进一步走到第27行,跳过对dealloc的调用,而是运行您可以轻松看到的代码,以便在主线程上运行dealloc-helper.

您能指望这一进展吗?

由于我找不到记录,所以我不知道我会一直指望这种情况,但这非常方便,而且显然是他们有意将其放入代码中.

每次Apple发行新版本的iOS和OSX时,我都会运行一组测试.我认为大多数开发人员都做类似的事情.我认为我要做的是编写一个单元测试,并将其添加到该集合中.因此,如果他们把它改回来,我会尽快知道.

否则,我倾向于认为这可能是可以安全假设的事情之一.

但是,请注意,子类可能会选择覆盖release(如果在禁用ARC的情况下进行编译),并且如果它们不调用基类实现,则不会出现此行为.

因此,您可能要为使用的任何第三方视图控制器类编写测试.

我的详细信息

我只在模拟iPhone 6的情况下使用XCode 6.4,部署目标8.4进行了测试.我将保留其他版本的测试作为练习,供读者阅读.

顺便说一句,如果您不介意,您发布的示例的详细信息是什么?

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

解决方案

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.

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?

这篇关于为什么将UIViewController释放在主线程上?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆