【问题标题】:Memory leak with `NSLayoutConstraint``NSLayoutConstraint`的内存泄漏
【发布时间】:2019-07-05 05:45:04
【问题描述】:

我有一个简单的布局,其中包含连接两个视图的约束,我将该约束存储在一个强属性中。

我的期望是,在删除其中一个视图后,约束仍然存在,但它会变为非活动状态。但事实并非如此,我的应用程序因EXC_BAD_ACCESS 异常而崩溃。

幸运的是,我能够重现该问题。我附上视图控制器的源代码。您可以将其粘贴到使用“Single View App”模板创建的新项目中。

要重现问题,请在删除其中一个视图之前和之后打印约束描述。它发生在我身上一次,约束没有被删除并且代码有效(但它只发生过一次)。使用调试器,我能够检查我删除的视图是否也没有被释放,而它已经没有层,所以它的某些部分已经被解构。在它发生之后,我注释掉了printConstraintDescriptionButtonTouchedUpInside 的实现,并在removeViewButtonTouchedUpInside 中设置了断点。当调试器在第二次按下按钮后暂停应用程序时,约束不再存在。

是否不允许对NSLayoutConstraint 实例进行强引用?我没有在文档中找到该信息。

使用 Xcode 版本 10.1 (10B61) 编译,运行 iOS Simulator iPhone SE 12.1。在运行 iOS 12.1.3 (16D5032a) 和 11.2.2 (15C202) 的物理设备上,相同的逻辑但在隔离的 sn-p 中没有失败。使用 Xcode 版本 9.4.1 进行编译不会改变任何内容。

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

【问题讨论】:

    标签: ios objective-c memory-leaks autolayout


    【解决方案1】:

    问题不在于约束不再存在……问题在于:

    NSLog(@"%@", self.constraint);
    

    尝试打印约束的(默认)描述。如果约束未激活,则会抛出 EXC_BAD_ACCESS 异常(您无法使用 @try/catch 块捕获该异常)。

    用这个替换你的方法:

    - (void)printConstraintDescriptionButtonTouchedUpInside {
    
        NSLog(@"self.constraint still exists? %@", self.constraint != nil ? @"Yes" : @"No");
    
        NSLog(@"self.constraint.constant = %0.2f", self.constraint.constant);
    
        if (self.constraint.isActive) {
            NSLog(@"self.constraint (default description) %@", self.constraint);
        } else {
            // this will throw EXC_BAD_ACCESS exception if constraint is not active
            // so, don't do it
            //NSLog(@"self.constraint (default description) %@", self.constraint);
        }
    
    }
    

    您将看到前两个NSLog()s 执行良好,无论约束是否处于活动状态。但是,如果您删除bottomView或以其他方式停用self.constraint),如果您尝试访问self.constraint.description(或self.constraint.debugDescription),则会收到错误消息。

    【讨论】:

    • 感谢您的回答。我真的很感激你的努力。你不同意应用程序不应该崩溃并且只打印与手动禁用约束时相同的描述吗?按照这个概念,我不同意“或以其他方式停用 self.constraint”,因为当我手动停用它而不是删除 bottomViewdescription 方法工作正常,它只是说约束不活跃。跨度>
    • 不幸的是,根据您在讨论中引入的新事实,我无法解决此问题。我有约束缓存机制。我没有删除它们,而是禁用它们,然后当需要它们时,如果secondItem 没有更改,我将它们重新激活并调整constant(如果需要)。只是打印 - 以及比较 secondItem - 会使应用程序崩溃以及在约束本身上调用 description
    • 从表面上看,我同意...检查.description.secondItem 应该抛出无法捕获的异常。也许提交错误报告?无论如何...在您的示例代码中,您正在设置self.bottomView = nil;。基于此,您还想设置 self.constraint = nil; 似乎是合乎逻辑的,这将避免该问题。当然,您的特定用例可能会使这很麻烦,但是……这可能是一种选择。否则,从superview中删除bottomView时不要杀死它?
    猜你喜欢
    • 1970-01-01
    • 2012-07-06
    • 1970-01-01
    • 1970-01-01
    • 2010-12-17
    • 2016-05-09
    • 2017-11-03
    • 2011-02-17
    • 2012-09-14
    相关资源
    最近更新 更多