一个模拟推送Segue的自定义Segue将VC变成Zombie

时间:2012-10-20 11:44:28

标签: iphone objective-c runtime-error uistoryboardsegue data-management

[使事情简短明了]

我写了一个自定义的segue。

-(void)perform {
UIView *preV = ((UIViewController *)self.sourceViewController).view;
UIView *newV = ((UIViewController *)self.destinationViewController).view;

[preV.window insertSubview:newV aboveSubview:preV];
newV.center = CGPointMake(preV.center.x + preV.frame.size.width, newV.center.y);
[UIView animateWithDuration:0.4
 animations:^{
     newV.center = CGPointMake(preV.center.x, newV.center.y);
     preV.center = CGPointMake(0- preV.center.x, newV.center.y);}
    completion:^(BOOL finished){ [preV removeFromSuperview]; }];
}

当触发segue时,没有异常。但是,它会取消分配 destinationViewController

单击destinationViewController中触发另一个segue的按钮时,应用会崩溃。

我尝试删除[preV removeFromSuperview],但无济于事。

[详细]

我最近开始使用Object-C,并编写了一个模拟推送segue的自定义segue。

第一次触发时,一切正常。

但在那之后,无论segue被触发了什么, 应用程序崩溃,我会收到EXC_BAD_ACCESS错误。


我的第一个猜测是,这与内存管理有关。那里的东西必须要解除分配,但我不知道它是什么。

我的第二个猜测是,这与UIViewUIWindow提供的基础设施有关。但再一次,由于我缺乏知识和经验,我无法弄清楚真正的问题是什么。

我知道我实际上可以采用一种简单的方法并使用rootviewcontroller创建一个push segue并简单地隐藏导航栏,但我真的想知道我看似构造良好的自定义segue究竟出了什么问题并了解代码结构下发生的事情。


[更新]

感谢Phillip Mills& Joachim Isaksson在进行了一些实验并利用断点和僵尸工具后提出了建议,

这是我意识到的:

  1. 在按钮触发自定义segue后,应用程序只会在按钮触发下一个segue时崩溃。使用viewDidAppear触发下一个segue不会导致任何崩溃。

  2. 坠机背后的主要原因:

  3. 将Objective-C邮件发送到已解除分配的对象(僵尸)

    [#,event type,refCt,Library,Caller]

    0   Malloc  1   UIKit   -[UIClassSwapper initWithCoder:]
    1   Retain  2   UIKit   -[UIRuntimeConnection initWithCoder:]
    2   Retain  3   UIKit   -[UIRuntimeConnection initWithCoder:]
    3   Retain  4   UIKit   -[UIRuntimeConnection initWithCoder:]
    4   Retain  5   UIKit   -[UIRuntimeConnection initWithCoder:]
    5   Retain  6   UIKit   -[UIRuntimeConnection initWithCoder:]
    6   Retain  7   UIKit   -[UIRuntimeConnection initWithCoder:]
    7   Retain  8   UIKit   -[UIRuntimeConnection initWithCoder:]
    8   Retain  9   UIKit   UINibDecoderDecodeObjectForValue
    9   Retain  10  UIKit   UINibDecoderDecodeObjectForValue
    10  Retain  11  UIKit   -[UIStoryboardScene setSceneViewController:]
    11  Retain  12  UIKit   -[UINib instantiateWithOwner:options:]
    12  Release 11  UIKit   -[UINibDecoder finishDecoding]
    13  Release 10  UIKit   -[UINibDecoder finishDecoding]
    14  Release 9   UIKit   -[UIRuntimeConnection dealloc]
    15  Release 8   UIKit   -[UIRuntimeConnection dealloc]
    16  Release 7   UIKit   -[UIRuntimeConnection dealloc]
    17  Release 6   UIKit   -[UIRuntimeConnection dealloc]
    18  Release 5   UIKit   -[UINibDecoder finishDecoding]
    19  Release 4   UIKit   -[UIRuntimeConnection dealloc]
    20  Release 3   UIKit   -[UIRuntimeConnection dealloc]
    21  Release 2   UIKit   -[UIRuntimeConnection dealloc]
    22  Retain  3   UIKit   -[UIStoryboardSegue initWithIdentifier:source:destination:]
    23  Retain  4   ProjectX    -[pushlike perform]
    24  Retain  5   UIKit   -[UINib instantiateWithOwner:options:]
    25  Retain  6   UIKit   +[UIProxyObject addMappingFromIdentifier:toObject:forCoder:]
    26  Retain  7   UIKit   -[UIProxyObject initWithCoder:]
    27  Retain  8   UIKit   -[UIRuntimeConnection initWithCoder:]
    28  Retain  9   UIKit   UINibDecoderDecodeObjectForValue
    29  Retain  10  UIKit   UINibDecoderDecodeObjectForValue
    30  Release 9   UIKit   -[UINib instantiateWithOwner:options:]
    31  Release 8   UIKit   +[UIProxyObject removeMappingsForCoder:]
    32  Release 7   UIKit   -[UINibDecoder finishDecoding]
    33  Release 6   UIKit   -[UIRuntimeConnection dealloc]
    34  Release 5   UIKit   -[UINibDecoder finishDecoding]
    35  Release 4   UIKit   -[UINibDecoder finishDecoding]
    36  Release 3   ProjectX    -[pushlike perform]
    37  Retain  4   libsystem_sim_blocks.dylib  _Block_object_assign
    38  Retain  5   UIKit   -[UIApplication _addAfterCACommitBlockForViewController:]
    39  Release 4   UIKit   -[UIStoryboardSegue dealloc]
    40  Release 3   UIKit   _UIApplicationHandleEvent
    41  Release 2   UIKit   -[UIStoryboardScene dealloc]
    42  Retain  3   UIKit   _applyBlockToCFArrayCopiedToStack
    43  Release 2   UIKit   _applyBlockToCFArrayCopiedToStack
    44  Release 1   UIKit   __destroy_helper_block_739
    45  Release 0   UIKit   _applyBlockToCFArrayCopiedToStack
    46  Zombie  -1  UIKit   -[UIApplication sendAction:to:from:forEvent:]
    

    暗示(,如果我没有错误

    自定义segue以某种方式触发了解除分配对象的内容,在点击触发另一个segue的按钮(在destinationViewController中)之后将向其发送Objective-C消息。


    更多细节

    没有调用prepareForSegue,因为我不需要在视图之间传递数据。

    我的segues都以同样的方式触发:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        CGRect buttonFrame = CGRectMake( 10, 40, 200, 50 );
        UIButton *button = [[UIButton alloc] initWithFrame: buttonFrame];
        [button setTitle: @"Go" forState: UIControlStateNormal];
        [button addTarget:self action:@selector(nextView) forControlEvents:UIControlEventTouchUpInside];
        [button setTitleColor: [UIColor blackColor] forState: UIControlStateNormal];
        [self.view addSubview:button]; 
    }
    
    - (void)nextView{
        [self performSegueWithIdentifier:@"push" sender:self];
    }
    

    我启用了ARC,所以我自己并没有真正进行任何释放..


    [更新2]

    已变成僵尸的对象是自定义segue的destinationViewController

    不在自定义segue中调用removeFromSuperview不会阻止对象变成僵尸。

    只要我使用常规模型segue或push segue(使用rootViewController)而不是我制作的自定义模型,就不会有任何僵尸,一切都会正常工作。

3 个答案:

答案 0 :(得分:5)

你的崩溃只是因为你的新控制器在segue执行后没有保留。

你做的是这个:

  • src和dest控制器实例化
  • 您执行动画
  • 完成后删除src视图
  • 你的src控制器被释放,但是窗口的rootViewController 仍然指向它,你的目标视图控制器被添加到窗口的层次结构中。

这将按预期工作:

-(void)perform {
  UIView *preV = ((UIViewController *)self.sourceViewController).view;
  UIView *newV = ((UIViewController *)self.destinationViewController).view;

  UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
  newV.center = CGPointMake(preV.center.x + preV.frame.size.width, newV.center.y);
  [window insertSubview:newV aboveSubview:preV];

  [UIView animateWithDuration:0.4
                   animations:^{
                       newV.center = CGPointMake(preV.center.x, newV.center.y);
                       preV.center = CGPointMake(0- preV.center.x, newV.center.y);}
                   completion:^(BOOL finished){
                       [preV removeFromSuperview];
                       window.rootViewController = self.destinationViewController;
                   }];
}

答案 1 :(得分:2)

在iOS中,viewController与它的.view之间的关系很特殊。

所有这些运行时调用(initWithCoder:UIStoryboardSegue initWithIdentifier:source:destination:等等)都指向尝试在幕后访问与viewController有关的事情,并且删除了你做过的一件事它的主要观点来自于其超级视图中的预期。

通过在sourceViewController的.view上执行removeFromSuperview,你正在招致破坏。

如果你想要一个不是push-segue的视图,你可以让segue成为一个模态segue。这将使您不必乱用导航栏,但可以允许CocoaTouch运行时为您完成segue(以及之后的清理)的工作。

如果您真的希望控件保留在同一个viewController中,那么您可以修改代码以执行[preV setHidden:YES][preV setAlpha:0]。它仍然会在那里,所以它不会变成一个僵尸,你可以通过扭转你喜欢的上述两个动作中的任何一个来回到它。

您甚至可以尝试删除(或注释掉调用)[preV removeFromSuperview],并在您从newV中执行的操作返回时将其滑回。

修改

要考虑的另一件事是在变量preV上使用the __block Storage Type,因为你在本地声明它,并且因为你要离开范围而原始的viewController可能会导致它消失。在完成块中,您最终可能会引用一个已经将其ref-count降为0的变量,并在您到达时将其删除。 __block旨在防止这种情况发生。

答案 2 :(得分:0)

您需要保留对destinationViewController的引用,以便它不会被取消分配。显然,您无法在自定义segue中执行此操作,因为在转换完成后将释放此对象。标准push segue通过将视图控制器添加到viewControllers的{​​{1}}属性来实现此目的。

在您的情况下,您可以致电

UITabBarController

在视图控制器之间建立正确的连接。不要忘记在反向调用中调用[sourceViewController addChildViewController:destinationViewController];