AutoLayout:removeFromSuperview/removeConstraints抛出异常并严重崩溃

Gre*_*mbs 53 ios automatic-ref-counting autolayout

我们有选择地使用自动布局约束,主要是为了定位与可编辑字段元素相关的标签(通常是UITextView,UITextField).但是,自从为这些字段实现自动布局以来,每当我们卸载视图,解除分配等时,我们都会看到一个令人讨厌的异常并崩溃.异常发生的原因是它试图在卸载视图之前从视图中删除约束.

我们的视图/控制器层次结构如下:

UITableViewController (plain style, but with cell appearance to mimic grouped style)
--> UITableViewCell
----> UIViewController (container for editable form)
------> UICollectionViewController (editable form)
--------> UICollectionViewCell
-----------> UIViewController (editable field)
--------------> UILabel (field label)                   **HAS CONSTRAINTS**
--------------> UITextView / UITextField (field value)  **HAS CONSTRAINTS**
Run Code Online (Sandbox Code Playgroud)

很多时候,当上层表格单元被解除分配/替换/重新加载时,我们会看到一个巨大的异常,然后崩溃,因为它试图释放/卸载其中的视图层次结构.

我试图通过捕获异常(没有帮助)并通过在释放/卸载(in viewWillDisappear:)之前强制删除受影响的视图和所有子视图上的所有约束来缓解崩溃,它似乎没有帮助.我甚至试图逐个删除这些约束,看看是否有一个特别是导致麻烦但是当我们打电话removeConstraint:removeConstraints:在容器上准备消失时它们都会爆炸.

我很困惑!这是我们例外的一小部分 - 大约有3000行被切断了,所以如果你需要更多,请问.

Exception while deallocating view: { Rows:
    0x18911270.posErrorMarker == 4 + 1*0x18911270.negError + 1*0x189112f0.marker + -1*0x189113f0.negError + 1*0x189113f0.posErrorMarker + 1*0x18911a60.marker + -0.5*0x1892dae0.negError + 0.5*0x1892dae0.posErrorMarker + 1*0x18951520.negError + -1*0x18951520.posErrorMarker + -0.5*0x18958090.negError + 0.5*0x18958090.posErrorMarker
    0x189112b0.negError == 12 + 1*0x189112b0.posErrorMarker + -1*0x189112f0.marker + 1*0x189113f0.negError + -1*0x189113f0.posErrorMarker + -1*0x18911a60.marker + 1*0x18925530.marker + 0.5*0x1892dae0.negError + -0.5*0x1892dae0.posErrorMarker + 1*0x1893e080.marker + 0.5*0x18958090.negError + -0.5*0x18958090.posErrorMarker + 1*0x18963640.marker
    0x18911370.negError == 9 + -1*0x189112f0.marker + 1*0x18911370.posErrorMarker + 1*0x18925530.marker + 1*0x1892dae0.negError + -1*0x1892dae0.posErrorMarker + 1*0x1893e080.marker + 1*0x18963640.marker
    0x189113b0.slackMarker == 2 + -1*0x189107d0.marker + 1*0x18910b90.negError + -1*0x18910b90.posErrorMarker + 

      ........ EXPLETIVES DELETED .........

   UITableView:0xca2b000.contentHeight == 36 + 1*0xc221c00.marker
   UITableView:0xca2b000.contentWidth == 704 + 1*0xc239470.marker
   UITableView:0xca2b000.minX == 0 + 1*0xc2a23f0.marker + -0.5*0xc2a2590.marker
   UITableView:0xca2b000.minY == 0 + 1*0xc2a25d0.marker + -0.5*0xc2a2630.marker
   UITableViewCellContentView:0x18ab13d0.Height == 174 + 1*0x18abd4f0.marker
   UITableViewCellContentView:0x18ab13d0.Width == 704 + 1*0x18abd470.marker

      ........ EXPLETIVES DELETED .........

    <NSAutoresizingMaskLayoutConstraint:0x18988bc0 h=-&- v=-&- UIView:0x18911e50.midY == UIView:0x1892d0c0.midY>        Marker:0x18988bc0.marker
    <NSAutoresizingMaskLayoutConstraint:0x18994b40 h=-&- v=-&- UIView:0xc4a6fb0.midX == UIView:0xc4b4990.midX>      Marker:0x18994b40.marker
    <NSAutoresizingMaskLayoutConstraint:0x18998480 h=-&- v=-&- UIView:0x18915180.width == UIView:0xc4c5970.width>       Marker:0x18998480.marker
    <NSAutoresizingMaskLayoutConstraint:0x18aae320 h=--& v=--& TapSectionalTableViewCell:0x18a3d270.midX == + 352>      Marker:0x18aae320.marker
    <NSAutoresizingMaskLayoutConstraint:0x18aae410 h=--& v=--& H:[TapSectionalTableViewCell:0x18a3d270(704)]>       Marker:0x18aae410.marker
    <NSAutoresizingMaskLayoutConstraint:0x18aae450 h=--& v=--& TapSectionalTableViewCell:0x18a3d270.midY == + 144>      Marker:0x18aae450.marker

      ........ EXPLETIVES DELETED .........

    <NSAutoresizingMaskLayoutConstraint:0xc2de2f0 h=--& v=--& TapGenericCollectionCell:0xc2ac500.midX == + 499>     Marker:0xc2de2f0.marker
    <NSAutoresizingMaskLayoutConstraint:0xc2de3b0 h=--& v=--& V:[TapGenericCollectionCell:0xc2ac500(34)]>       Marker:0xc2de3b0.marker
    <NSAutoresizingMaskLayoutConstraint:0xc2de430 h=-&- v=-&- UIView:0x18953f80.height == UIView:0xc2acb20.height>      Marker:0xc2de430.marker
    <NSAutoresizingMaskLayoutConstraint:0xc2de520 h=-&- v=-&- UIView:0x18923af0.height == UIView:0xc2ae570.height>      Marker:0xc2de520.marker
    <NSAutoresizingMaskLayoutConstraint:0xc2de560 h=--& v=--& H:[TapGenericCollectionCell:0xc2ac500(280)]>      Marker:0xc2de560.marker

      ........ EXPLETIVES DELETED .........

    <NSContentSizeLayoutConstraint:0xc2f5730 H:[_UIBaselineLayoutStrut:0x18994a30(0)] Hug:250 CompressionResistance:750>        Marker:0xc2f5730.posErrorMarker
    <NSContentSizeLayoutConstraint:0xc2f5730 H:[_UIBaselineLayoutStrut:0x18994a30(0)] Hug:250 CompressionResistance:750>        Marker:0xc2f5730.posErrorMarker
    <NSContentSizeLayoutConstraint:0xc2f5770 V:[_UIBaselineLayoutStrut:0x18994a30(18)] Hug:250 CompressionResistance:750>       Marker:0xc2f5770.posErrorMarker

internal error.  Cannot find an outgoing row head for incoming head UIView:0x189712b0.Width, which should never happen.'
Run Code Online (Sandbox Code Playgroud)
/**** BEGIN Individual Field Controller - This code is from the base individual field controller used in our editable form collection *****/

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.clipsToBounds = YES;
    self.view.opaque = YES;

    CGRect viewFrame = self.view.frame;
    viewFrame.size = [self defaultFieldSize];
    self.view.frame = viewFrame;

    if (self.backgroundColor) {
        self.view.backgroundColor = self.backgroundColor;
    }
    else {
        self.view.backgroundColor = [UIColor whiteColor];
    }
    [self createLabelAndField];

    [self setLabelAndFieldContraints];

    [self.view addConstraints:self.labelValueConstraints];
    [self.view setNeedsUpdateConstraints];
}

- (void)createLabelAndField {
    [self removeLabelAndField];

    UILabel *label = [[UILabel alloc] init];
    label.font = self.labelFont;
    label.textColor = self.labelColor;
    label.lineBreakMode = NSLineBreakByWordWrapping;
    label.textAlignment = NSTextAlignmentLeft;
    label.adjustsFontSizeToFitWidth = NO;
    label.numberOfLines = 0;

    if (self.backgroundColor) {
        label.backgroundColor = self.backgroundColor;
    }
    else {
        label.backgroundColor = [UIColor whiteColor];
    }

    [self.view addSubview:label];

    self.label = label;


    /// EXAMPLE valueView initialization from a subclass that handles long text

    TapEditableTextView *textView = [[TapEditableTextView alloc] init];
    if (self.hasLabelOverValue) {
        textView.shouldMimicTextField = NO;
    }
    else {
        textView.shouldMimicTextField = YES;
    }
    textView.delegate = self;
    textView.keyboardType = UIKeyboardTypeDefault;
    textView.font = self.valueFont;
    textView.textColor = self.valueColor;
    textView.textAlignment = NSTextAlignmentLeft;
    textView.normalBackgroundColor = self.backgroundColor;
    textView.editable = NO;
    textView.textLines = self.textLines;

    self.valueTextView = textView;
    self.valueView = textView;
    [self.view addSubview:textView];
}

- (void)removeLabelAndField {
    [self clearConstraints];

    if (self.label) {
        [self.label removeFromSuperview];
        self.label = nil;
    }
    if (self.valueView) {
        [self.valueView removeFromSuperview];
        self.valueView = nil;
    }
}

- (void)clearConstraints {
    if (self.isViewLoaded && self.labelValueConstraints) {
        [self.view removeConstraints:self.labelValueConstraints];
    }
    self.labelValueConstraints = nil;
    self.labelToValueHorizConstraint = nil;
    self.valueWidthConstraint = nil;
}

// This is called in our field's viewDidLoad, after we've created our label and valueView (UITextField, UITextView, etc)
- (void)setLabelAndFieldContraints {
    [self clearConstraints];

    self.labelValueConstraints = [NSMutableArray array];

    self.label.translatesAutoresizingMaskIntoConstraints = NO;
    self.valueView.translatesAutoresizingMaskIntoConstraints = NO;

    NSLayoutConstraint *constraint = nil;

    constraint = [NSLayoutConstraint
                  constraintWithItem:self.label attribute:NSLayoutAttributeLeft
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeLeft
                  multiplier:1.0f constant:self.labelValueGap];
    constraint.priority = UILayoutPriorityRequired;
    [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.label attribute:NSLayoutAttributeTop
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeTop
                  multiplier:1.0f constant:0];
    constraint.priority = 550;
    [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.label attribute:NSLayoutAttributeBottom
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeBottom
                  multiplier:1.0f constant:0];
    constraint.priority = 400;
    [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.valueView attribute:NSLayoutAttributeTop
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeTop
                  multiplier:1.0f constant:0];
    constraint.priority = UILayoutPriorityRequired;
    [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.valueView attribute:NSLayoutAttributeBottom
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeBottom
                  multiplier:1.0f constant:0];
    constraint.priority = 499;
    [self.labelValueConstraints addObject:constraint];


     constraint = [NSLayoutConstraint
                  constraintWithItem:self.valueView attribute:NSLayoutAttributeRight
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeRight
                  multiplier:1.0f constant: -(kDisclosureWidth + self.labelValueGap) ];
     constraint.priority = 901;
     [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.valueView attribute:NSLayoutAttributeLeading
                  relatedBy:NSLayoutRelationGreaterThanOrEqual
                  toItem:self.label attribute:NSLayoutAttributeTrailing
                  multiplier:1.0f constant:self.labelValueGap];
    constraint.priority = UILayoutPriorityDefaultHigh + 1;
    [self.labelValueConstraints addObject:constraint];
    self.labelToValueHorizConstraint = constraint;


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.label attribute:NSLayoutAttributeBaseline
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.valueView attribute:NSLayoutAttributeBaseline
                  multiplier:1.0f constant:0.f];
    constraint.priority = 600;
    [self.labelValueConstraints addObject:constraint];


    constraint = [NSLayoutConstraint
                  constraintWithItem:self.valueView attribute:NSLayoutAttributeWidth
                  relatedBy:NSLayoutRelationEqual
                  toItem:self.view attribute:NSLayoutAttributeWidth
                  multiplier:(1.f - self.labelWidthPercentage) constant:0];
    constraint.priority = 305;
    [self.labelValueConstraints addObject:constraint];
    self.valueWidthConstraint = constraint;


    [self setCompressionAndHuggingForLabelView:self.label];
    [self setCompressionAndHuggingForValueView:self.valueView];
}

- (void)setCompressionAndHuggingForLabelView:(UILabel *)labelView {
    if (!labelView) {
        return;
    }
    [labelView setContentCompressionResistancePriority:510 forAxis:UILayoutConstraintAxisHorizontal];
    [labelView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical];
    [labelView setContentHuggingPriority:450 forAxis:UILayoutConstraintAxisHorizontal];
    [labelView setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical];
}

- (void)setCompressionAndHuggingForValueView:(UIView *)valueView {
    if (!valueView) {
        return;
    }
    [valueView setContentCompressionResistancePriority:509 forAxis:UILayoutConstraintAxisHorizontal];
    [valueView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical];
    [valueView setContentHuggingPriority:300 forAxis:UILayoutConstraintAxisHorizontal];
    [valueView setContentHuggingPriority:650 forAxis:UILayoutConstraintAxisVertical];
}

/****** END Individual Field Controller ******/
Run Code Online (Sandbox Code Playgroud)

smi*_*org 88

我与一位苹果工程师就这次事故进行了(广泛的)对话.

以下是两个最可能的原因:

  1. 您有一个无效的约束,例如,view1.left = view2.left + 20哪里view2出乎意料为零,或者乘数为0.请务必加倍(和三倍)检查约束以确保它们是正确的.以下是有问题约束的2个示例:

    // The first constraint would be a problem if view2 were nil
    [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeBottom multiplier:1 constant:20];
    // The second constraint is a problem because the 0 multiplier causes view2 to be "lost"
    [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeBottom multiplier:0 constant:5];
    
    Run Code Online (Sandbox Code Playgroud)
  2. 您在内部Foundation自动布局引擎中遇到了与累积浮点精度损失相关的错误.当您崩溃时,您可以知道这种情况的方法是在控制台中搜索(大)异常日志以获得非常小(几乎为零)的浮点数,例如:

<505:-7.45058e-08>*PWPlotLegendEntryView:0x600000582be0.Height{id: 34609} +

(e-在控制台输出中搜索以查找这样的小数字.)此数字(-7.45058e-08在本例中)表示内部引擎解决约束时此特定时间点的系数.在这种情况下,数字应该恰好为0,但由于自动布局引擎使用浮点数进行计算的方式,它已成为一个非常小的负数,而不是将所有内容都搞砸了.如果你能在输出中找到这样的数字,你知道你已经遇到了这个错误.

你怎么解决这个问题?

更改添加(激活)约束的顺序最终可能会更改内部引擎中的计算顺序,从而导致此问题消失,因为数学运算完成时没有任何问题的精度损失.

当您更改内容压缩阻力或内容拥抱视图的优先级时,此问题似乎更频繁出现,因此请尝试注释掉任何执行此操作的代码,以查看是否导致此错误发生,或重新排序以使其更早发生或者稍后在约束设置代码中.

关于我的具体案例的更多细节:

我在iOS上遇到了这次崩溃.重现它的步骤非常有趣:

  1. 在屏幕上(在导航控制器中)推送包含表视图的视图控制器.
  2. 表视图必须包含足够的单元格,因此它们并非全部适合可见区域,然后必须滚动到最后一个单元格然后稍微备份(可能是这导致单元格被重用,这触发了这个问题).
  3. 然后,当包含表视图的视图控制器从导航堆栈弹出时,在弹出动画完成后,应用程序将立即从视图层次结构中删除视图控制器视图的位置崩溃.

经过大量的反复试验后,我能够将问题分解为一个特定的事情:UIImageView在每个表格视图单元格中设置内容压缩阻力和拥抱优先级.在这种情况下,使用单元格内的自动布局定位图像视图,并且为了实现正确的布局,图像视图需要正是其固有内容大小(其图像的大小).

这是有问题的代码:

// Inside of the UITableViewCell's updateConstraints method...

[self.imageView setContentCompressionResistancePriority:?UILayoutPriorityRequired forAxis:?UILayoutConstraintAxisHorizontal];        
[self.imageView setContentCompressionResistancePriority:?UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];       
[self.imageView setContentHuggingPriority:?UILayoutPriorityRequired forAxis:?UILayoutConstraintAxisHorizontal];      
[self.imageView setContentHuggingPriority:?UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
Run Code Online (Sandbox Code Playgroud)

删除上面的代码并将其替换为2个约束(在所需的优先级),将图像视图的宽度和高度固定为图像的大小,可以获得相同的结果,但避免了崩溃.这是替换代码(使用PureLayout):

[self.imageView autoSetDimensionsToSize:self.imageView.image.size];
Run Code Online (Sandbox Code Playgroud)

我还发现,只是将有问题的4行移动到我的约束设置代码中的不同位置解决了这个问题,大概是因为这改变了计算的顺序,足以防止有问题的精度损失.

  • 这个答案救了我们的培根!由于浮点错误,我们的观点仅在iPad 2上崩溃*.设置乘数从1.6到1.59999固定它. (3认同)
  • @smileyborg - 这是一些很棒的调查工作......很棒的答案! (3认同)
  • 我遇到了同样的问题,当我再次弹出时,使用Autolayout布局的子视图推送的模态视图已经崩溃了.我在设置宽度为0.8的UIView中添加了一个约束作为视图控制器视图的乘数,其中添加了约束.这是在代码中完成的.将值设置为0.8以外的任何其他值意味着应用程序不会崩溃.一个很奇怪的错误. (3认同)

Leo*_*zov 14

解除分配问题 - 一种可能性

使用自动布局的代码可能在主线程上运行,但是在后台运行并使用您的视图(可能是间接的)的块之一,可能会对视图或其所有者之一(如视图控制器)保持强引用(即Objective-C块的默认行为).当这样的块在后台队列上运行并解除分配时,它捕获的强引用将在同一队列中释放,您可能会面临众所周知的释放问题.

  1. 在视图控制器中,确保self在所有不需要强引用的块中使用弱引用(并且可以在后台运行).您可以这样声明:__weak typeof(self) weakSelf = self;在块之前 - 并weakSelf在块内部使用.

  2. 对于包含对视图的引用的任何局部变量也是如此 - 确保将它们的值捕获为弱引用.

另一种可能性

在我的工作中,当隐藏视图参与布局时,我在iOS 6上遇到了类似的问题.从层次结构(-[UIView removeFromSuperview])中删除视图而不是将hidden属性设置为YES为我修复了问题.