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方法在语法上甚至更加丑陋).
文艺青年最爱的 -虽然我找不到任何官方文件,当前的实现确实保证dealloc了UIViewController在主线程上发生.
我想我可以给出一个简单的答案,但也许我今天可以做一点"教人钓鱼".
好.我无法在任何地方找到这方面的文档,我也不记得曾经公开说过.事实上,我总是不顾一切地确保在主线程上取消分配视图控制器,这是我第一次看到有人指出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进行了测试.我将把测试与其他版本作为读者练习.
顺便说一句,如果您不介意,您发布的示例的详细信息是什么?