Memory leak with `NSLayoutConstraint`

I have a simple layout with a constraint connecting two views and I store that constraint in a strong property.


My expectation is that after removing one of those views, the constraint will still exist, but it will turn inactive. But it is not the case and my app crashes with

EXC_BAD_ACCESS
exception.


Luckily I am able to reproduce the issue. I attach the source code of the view controller. You can just paste it into a new project created with the "Single View App" template.


To reproduce the issue print the constraint description before and after removing one of the views. It happened to me once, that the constraint was not removed and the code worked (but it happened only once). With the debugger, I was able to check that the view I removed was not deallocated as well, while It already did not have a layer, so some parts of it were already deconstructed. After it happened I commented out the implementation of

printConstraintDescriptionButtonTouchedUpInside
and just set the breakpoint in the
removeViewButtonTouchedUpInside
. When the debugger paused the app after pressing the button for the second time the constraint did not exist anymore.


Is it disallowed to hold a strong reference to the

NSLayoutConstraint
instances? I have not found that information in the docs.


Compiled with Xcode Version 10.1 (10B61), running iOS Simulator iPhone SE 12.1. The same logic but not in the isolated snippet fails on the physical devices running iOS 12.1.3 (16D5032a) and 11.2.2 (15C202). Compiling with Xcode Version 9.4.1 does not change anything.


ViewController.m

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) UIView* topView;
@property (strong, nonatomic) UIView* bottomView;

@property (strong, nonatomic) NSLayoutConstraint* constraint;

@property (strong, nonatomic) UIButton* removeViewButton;
@property (strong, nonatomic) UIButton* printConstraintDescriptionButton;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.topView = [UIView new];
    self.topView.backgroundColor = [UIColor grayColor];
    self.topView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.topView];

    self.bottomView = [UIView new];
    self.bottomView.backgroundColor = [UIColor grayColor];
    self.bottomView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.bottomView];

    self.removeViewButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.removeViewButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.removeViewButton setTitle:@"Remove View"
                           forState:UIControlStateNormal];
    [self.removeViewButton addTarget:self
                              action:@selector(removeViewButtonTouchedUpInside)
                    forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.removeViewButton];

    self.printConstraintDescriptionButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.printConstraintDescriptionButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.printConstraintDescriptionButton setTitle:@"Print Constraint Description"
                                           forState:UIControlStateNormal];
    [self.printConstraintDescriptionButton addTarget:self
                                              action:@selector(printConstraintDescriptionButtonTouchedUpInside)
                                    forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.printConstraintDescriptionButton];

    NSDictionary* views = @{
                            @"topView": self.topView,
                            @"bottomView": self.bottomView,
                            @"removeViewButton": self.removeViewButton,
                            @"printConstraintDescriptionButton": self.printConstraintDescriptionButton
    };

    NSArray<NSString*>* constraints = @[
                             @"H:|-[topView]-|",
                             @"H:|-[bottomView]-|",
                             @"H:|-[printConstraintDescriptionButton]-|",
                             @"H:|-[removeViewButton]-|",
                             @"V:|-[topView(==44)]",
                             @"V:[bottomView(==44)]",
                             @"V:[printConstraintDescriptionButton]-[removeViewButton]-|"
    ];

    [constraints enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:obj
                                                                          options:0
                                                                          metrics:nil
                                                                            views:views]];
    }];

    self.constraint = [NSLayoutConstraint constraintWithItem:self.topView
                                                   attribute:NSLayoutAttributeBottom
                                                   relatedBy:NSLayoutRelationEqual
                                                      toItem:self.bottomView
                                                   attribute:NSLayoutAttributeTop
                                                  multiplier:1.0
                                                    constant:-8.0];
    self.constraint.active = YES;
}

- (void)printConstraintDescriptionButtonTouchedUpInside {
    NSLog(@"%@", self.constraint);
}

- (void)removeViewButtonTouchedUpInside {
    [self.bottomView removeFromSuperview];
    self.bottomView = nil;
}

@end
Post not yet marked as solved Up vote post of luleq Down vote post of luleq
2.4k views

Replies

Your constraint is still alive. The crash occurs when you try to print the constraint, because the bottomView is gone but the constraint still references it, and crashes trying to put that deallocated view's description into a format string. This constraint isn't very useful in this state—you wouldn't be able to re-use it unless you can replace secondItem with some new view and unless I'm reading it wrong, the API for NSLayoutConstraint has a readonly property for those. Probably best to avoid using it once the view is deallocated.


Could UIKit be cahnged so the constraint takes a strong reference on the view? A weak reference? It's possible, but there are performance and memory tradeoffs to each of those, and I'm not sure what you intend to do with that constraint other than print it.

I understand why it crashes, but I do not agree that it should. It is a runtime exception which cannot be handled by the developer.


I found this issue in my constraints caching mechanism. Instead of removing I only deactivate constraints. When I need a constraint with the same attribute for the second time I check if the old one can be reused. One of the conditions which have to be met is that the `secondItem` of the `NSLayoutConstraint` on the old object did not change. That is exactly when my app crashes.


The workaround I had to implement is based on tracking all views I ever created and before I remove them from the view hierarchy I destroy the constraints. It's not an elegant solution, and it also makes impossible to reuse constraint if the view was taken out from the view hierarchy and reattached later on. My app has a dynamic layout which depends on the actions the user performs. When dealing with hundreds of views we found a significant performance improvement in layout after implementing this cache.


I would rather expect that the constraint holds a weak reference and return `nil` if the view does not exist anymore, or at least throw an exception which can be caught. It is never mentioned in the UIKit documentation that `secondItem` property should not be used after deallocating the view.