为什么 UIViewController 在主线程上被释放?

Why is UIViewController deallocated on the main thread?

我最近偶然发现了 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。我想我理解这个问题及其影响,但为了确定我想在一个小测试项目中重现它。我首先创建了自己的 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 for UIViewController 发生在主线程上.


我想我可以给出一个简单的答案,但也许我今天可以做一点"teach a man to fish"。

好的。我在任何地方都找不到这方面的文档,而且我也不记得它曾被公开说过。事实上,我一直竭尽全力确保视图控制器在主线程上被释放,这是我第一次看到有人指出 UIViewController objects 被自动释放在主线程上。

也许其他人可以找到官方声明,但我找不到。

但是,我确实有一些证据可以证明它确实发生了。实际上,起初,我认为您没有正确处理您的块或引用计数,并且以某种方式在主线程上保留了一个引用。

不过粗略看了下,还是有兴趣亲自试一试。为了满足我的好奇心,我做了一个类似于你的继承自UIViewController的class。它的 dealloc 运行 在主线程上。

所以,我只是把基数class改成了UIResponder,也就是UIViewController的基数class,然后又改成了运行。这次它在后台线程上 dealloc 运行。

嗯。也许闭门造车有事。我们有很多调试技巧。答案总是在你尝试的最后一个中,但我想我会为这类东西尝试我惯用的技巧。

日志通知

我最喜欢的了解事情如何实施的工具之一是记录所有通知。

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

然后我 运行 使用两个 classes,并且没有发现两者之间有任何意外或不同之处。我没想到,但那个小技巧非常简单,它对我发现很多其他东西是如何工作的有很大帮助,所以它通常是第一个。

登录Method/Message发送

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

instrumentObjcMessageSends(YES);

然后我关闭了日志记录,这是 dealloc 方法的第一行。

instrumentObjcMessageSends(NO);

现在,在我所知道的任何 headers 中都找不到此 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 方法有一个特殊的覆盖实现。我想知道为什么?难道是专门t运行sfer对dealloc的调用到主线程?

调试器

是的,这是我首先想到的,但我没有证据表明 UIViewController 中存在覆盖,所以我进行了正常流程。我发现当我跳过步骤时,它通常需要更长的时间。

无论如何,现在我们知道我们在寻找什么,我在 "sleeping" 块的最后一行放置了一个断点,并使 class 继承自 UIViewController

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

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

继续之后,我看到了这个很棒的程序集,它从视觉上证实了正在发生的事情。

pthread_main_np 是一个函数,它告诉您是否 运行ning 在主线程上。单步执行汇编指令确认我们不在主线程上 运行ning。

更进一步,我们到达第 27 行,在那里我们跳过对 dealloc 的调用,取而代之的是 运行 您可以轻松看到的是 运行 a dealloc-helper 在主线程上。

你能指望未来吗?

因为我找不到它的文档,我不知道我是否会一直指望这种情况发生,但它非常方便,而且显然是他们有意放入代码中的东西。

每次 Apple 发布 iOS 和 OSX 的新版本时,我都会 运行 进行一组测试。我假设大多数开发人员做的事情非常相似。我想我会做的是编写一个单元测试,并将其添加到那个集合中。因此,如果他们改回来,我一出来就知道。

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

但是,请注意 subclasses 可能会选择覆盖 release(如果它们是在禁用 ARC 的情况下编译的),并且如果它们不调用基础 class 实现,你不会得到这种行为。

因此,您可能希望为任何 third-party 视图控制器 classes 编写测试你用。

我的详细信息

我只用 XCode 6.4,部署目标 8.4,模拟 iPhone 6 测试了这个。我将把其他版本的测试作为 reader 的练习。

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