为什么UIViewController在主线程上解除分配?

我最近偶然发现了一些Objective-C代码中的Deallocation Problem 。 这个主题之前讨论过,在Block_release的 Stack Overflow 释放后台线程上的UI对象 。 我想我明白这个问题及其含义,但是确定我想在一个小testing项目中重现它。 我首先创build了我自己的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秒后,由于断言失败。 build议的修复似乎工作。 即应用程序不会崩溃,如果我更改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的angular色)。 实现与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!的消息I'm deallocated! 打印到控制台3秒后,所以我非常确定视图控制器正在取消分配。

为什么视图控制器在主线程上解除分配,而不安全的对象在后台线程上解除分配? 代码几乎相同。 UIKit在幕后做了一些奇特的事情,以确保UIViewController总是在主线程上解除分配? 我开始怀疑这一点,因为下面的片段也没有打破我的主张:

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

如果是这样,这种行为logging在某处? 这种行为可以依靠吗? 还是我完全错了,有什么明显的我在这里失踪?

注意:我完全知道我可以在这里使用__weak引用的事实,但是让我们假设视图控制器应该仍然活着,以便在主线程上执行我们的完成代码。 另外,在我绕过这个问题之前,我试图理解这个问题的核心。 我将代码转换为Swift,并得到了与Objective-C相同的结果( SOUnsafeObject的修复程序在语法上甚至更丑)。

tl; dr – 尽pipe我找不到官方文档,但是当前的实现确实确保UIViewController dealloc发生在主线程上。


我想我可以给一个简单的答案,但也许我今天可以做一点“教一个人钓鱼”。

好。 我找不到任何地方的文件,我也不记得曾经公开过。 事实上,我一直在为了确保视图控制器在主线程上的释放而走了出路,这是我第一次见到有人指出UIViewController对象在主线程上被自动解除分配。

也许其他人可以find一个正式的声明,但我找不到一个。

不过,我确实有一些证据certificate确实发生了。 实际上,起初我以为你没有正确地处理你的数据块或引用计数,不知怎的,主线程上保留了一个引用。

但是,经过粗略的观察,我有兴趣尝试一下。 为了满足我的好奇心,我做了一个类似于你从UIViewControllerinheritance的类。 它的dealloc在主线程上运行。

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

嗯。 也许有一些事情正在闭门进行。 我们有很多debugging技巧。 答案总是在最后一个尝试的位置,但我想我会尝试我惯用的这种东西。

日志通知

我最喜欢的工具之一是要logging所有的通知。

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

然后,我跑了两个class,没有看到任何意外或两者之间的不同。 我没有想到,但是这个小窍门非常简单,它帮助我发现了很多其他的东西,所以它通常是第一个。

日志方法/消息发送

我的第二个技巧是启用方法日志logging。 但是,我不想logging所有方法,只是在最后一个块执行的时间和调用dealloc之间发生了什么。 所以,通过添加这个作为“睡眠”块的最后一行来打开方法日志logging。

 instrumentObjcMessageSends(YES); 

而且我把日志恢复了,这是dealloc方法的第一行。

 instrumentObjcMessageSends(NO); 

现在,这个C函数不能在任何我知道的头文件中find,所以你需要在文件的顶部声明它。

 extern void instrumentObjcMessageSends(BOOL); 

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

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

 $ 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调用到主线程吗?

debugging器

是的,这是我想到的第一件事,但我没有证据显示UIViewController中有一个覆盖,所以我经历了我的正常过程。 我发现,当我跳过步骤,通常需要更长的时间。

无论如何,现在我们知道我们正在寻找什么,我在“睡眠”块的最后一行放置了一个断点,并使该类inheritance自UIViewController

当我打断点时,我添加了一个手动断点…

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

继续之后,我受到了这个令人敬畏的大会的欢迎,它确认了正在发生的事情。

在这里输入图像说明

pthread_main_np是一个函数,告诉你是否在主线程上运行。 单步执行汇编指令证实,我们没有在主线程上运行。

更进一步,我们到了第27行,在那里我们跳过调用dealloc ,而是运行你可以很容易地看到的是在主线程上运行dealloc-helper的代码。

你能指望这个前进吗?

由于我找不到logging,我不知道我会一直指望这种情况,但是这非常方便,显然是故意放在代码中的东西。

每当Apple发布新版本的iOS和OSX时,我都会运行一系列testing。 我假设大多数开发者做的事情非常相似。 我想我会做的是写一个unit testing,并将其添加到该集。 因此,如果他们把它改回来,我会尽快知道它的出现。

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

但是,请注意,子类可能会select重写release (如果它们是在禁用ARC的情况下编译的),并且如果它们不调用基类实现,则不会得到此行为。

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

我的细节

我只用XCode 6.4,部署目标8.4,模拟iPhone 6testing了这个。我将用其他版本作为练习给读者留下testing。

顺便说一句,如果你不介意,你的发布的例子有什么细节?