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

时间:2015-08-14 09:19:17

标签: ios objective-c cocoa-touch uikit objective-c-blocks

我最近在一些Objective-C代码中偶然发现了The Deallocation Problem。之前在Block_release deallocating UI objects on a background thread的Stack Overflow上讨论过这个主题。我想我理解这个问题及其含义,但我确定我想在一个小小的测试项目中重现它。我首先创建了自己的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秒后崩溃。拟议的修复似乎有效。即如果我将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的修复在语法上甚至更加丑陋)。

1 个答案:

答案 0 :(得分:8)

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

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

行。我无法在任何地方找到这方面的文档,而且我也不记得它曾被公开发表过。事实上,我总是不顾一切地确保在主线程上取消分配视图控制器,这是我第一次见到某人表明UIViewController个对象被自动解除分配主线。

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

但是,我确实有一些证据证明它确实发生了。实际上,起初,我认为你没有正确处理你的块或引用计数,并且不知何故在主线程上保留了引用。

然而,粗略地看一下后,我有兴趣为自己尝试一下。为了满足我的好奇心,我创建了一个类似于你的UIViewController继承的课程。它的dealloc在主线上运行。

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

嗯。也许闭门造车会发生一些事情。我们有很多调试技巧。答案总是在你尝试的最后一个答案中,但我想我会尝试使用我常用的技巧。

日志通知

我最喜欢的工具之一就是记录所有通知。

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

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

记录方法/消息发送

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

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中有覆盖,所以我经历了正常的过程。我发现当我跳过步骤时,通常需要更长的时间。

无论如何,既然我们已经知道我们在寻找什么,那么我会在&#34;睡觉&#34;的最后一行放置一个断点。阻止并使该类继承自UIViewController

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

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

继续之后,我接受了这个令人敬畏的装配,这在视觉上证实了正在发生的事情。

enter image description here

pthread_main_np是一个函数,它告诉您是否在主线程上运行。单步执行程序集指令确认我们没有在主线程上运行。

进一步说,我们到达第27行,在那里我们跳过调用dealloc,然后运行你可以很容易看到的代码来在主线程上运行dealloc-helper。

你能指望这种前进吗?

由于我无法记录,我不知道我会一直指望这种情况发生,但它非常方便,显然是他们故意将这些内容放入代码中。

每次Apple发布新版本的iOS和OSX时,我都会运行一系列测试。我假设大多数开发人员都做了类似的事情我想我要做的就是编写一个单元测试,并将其添加到该集合中。因此,如果他们改变它,我会立刻知道它。

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

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

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

我的详细信息

我只使用XCode 6.4,部署目标8.4,模拟iPhone 6对此进行了测试。我将其他版本的测试留给读者练习。

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