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

s1m*_*m0n 4 cocoa-touch objective-c uikit ios objective-c-blocks

我最近偶然发现了一些Objective-C代码中的Deallocation问题.之前讨论过Block_release中的 Stack Overflow在后台线程上释放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}]
Run Code Online (Sandbox Code Playgroud)

现在,正如预期的那样,如果由于断言失败,我在3秒[[[SOUnsafeObject alloc] init] reloadDataInBackground];application:didFinishLaunching..将应用程序内部崩溃.拟议的修复似乎有效.即如果我将实现更改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);
});
Run Code Online (Sandbox Code Playgroud)

好的,所以看起来我对这个问题的理解以及如何在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
Run Code Online (Sandbox Code Playgroud)

现在,让我们[[SODemoViewController alloc] init] reloadDataInBackground];进去吧application:didFinishLaunching...嗯,断言没有失败..消息I'm deallocated!在3秒后打印到控制台,所以我很确定视图控制器正在被取消分配.

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

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
    SODemoViewController()
});
Run Code Online (Sandbox Code Playgroud)

如果是这样,这种行为是否记录在某处?可以依赖这种行为吗?或者我完全错了,有什么显而易见的我在这里缺席了吗?

注意:我完全知道我可以__weak在这里使用引用这一事实,但我们假设视图控制器应该仍处于活动状态,以便在主线程上执行完成代码.此外,在我绕过它之前,我试图了解问题的核心.我将代码转换为Swift并获得了与Objective-C相同的结果(修复SOUnsafeObject方法在语法上甚至更加丑陋).

Jod*_*ins 8

文艺青年最爱的 -虽然我找不到任何官方文件,当前的实现确实保证deallocUIViewController在主线程上发生.


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

好.我无法在任何地方找到这方面的文档,我也不记得曾经公开说过.事实上,我总是不顾一切地确保在主线程上取消分配视图控制器,这是我第一次看到有人指出UIViewController对象在主线程上自动解除分配.

也许别人可以找到官方声明,但我找不到.

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

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

所以,我只是改变了基类UIResponder,这是基类的UIViewController,然后再运行它.这次它dealloc运行在后台线程上.

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

日志通知

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

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

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

记录方法/消息发送

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

instrumentObjcMessageSends(YES);
Run Code Online (Sandbox Code Playgroud)

然后我把关闭记录,这是该dealloc方法的第一行.

instrumentObjcMessageSends(NO);
Run Code Online (Sandbox Code Playgroud)

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

extern void instrumentObjcMessageSends(BOOL);
Run Code Online (Sandbox Code Playgroud)

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

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

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

$ cat msgSends-72057
- __NSMallocBlock__ __NSMallocBlock release
- SOUnsafeObject UIViewController release
- SOUnsafeObject SOUnsafeObject dealloc
Run Code Online (Sandbox Code Playgroud)

对此没有太多的惊讶.但是,UIViewController release指示的存在UIViewController具有该+release方法的特殊覆盖实现.我想知道为什么?是否可以专门将调用转移dealloc到主线程?

调试器

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

无论如何,既然我们知道我们在寻找什么,我就在"睡眠"块的最后一行放置一个断点,并使该类继承UIViewController.

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

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

继续之后,我受到了这个令人敬畏的装配的欢迎,它可以直观地确认正在发生的事情.

在此输入图像描述

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

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

你能指望这种前进吗?

由于我无法找到它,我不知道我会一直指望这种情况发生,但它非常方便,显然它们是故意放入代码中的.

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

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

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

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

我的细节

我只用XCode 6.4,部署目标8.4,模拟iPhone 6进行了测试.我将把测试与其他版本作为读者练习.

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