如果我不保留IBOutlet会发生什么?

39

如果我这样做:

@interface RegisterController : UIViewController <UITextFieldDelegate>
{
    IBOutlet UITextField *usernameField;
}

不要这样做:

@interface RegisterController : UIViewController <UITextFieldDelegate>
{
    UITextField *usernameField;
}
@property (nonatomic, retain) IBOutlet UITextField *usernameField;

会发生什么不好的事情吗?我知道在第二种情况下,该字段被保留,但是由于nib拥有该字段,这是否会有所不同?如果没有保留,该字段会消失吗?在什么情况下会消失?第一种情况下的代码可以工作,想知道在内存管理方面是否存在问题。

6个回答

71
推荐您为所有IBOutlets声明属性,以确保清晰和一致性。 具体细节可参考Memory Management Programming Guide。基本意思是,在未归档NIB对象时,nib加载代码将使用setValue:forKey:设置所有IBOutlets。当您在属性上声明内存管理行为时,就不会有任何疑惑。如果该视图被卸载,但您使用了被声明为retain的属性,则仍然可以引用到您的textfield。
也许一个更具体的例子会更有用,以说明为什么应该使用保留属性:
我假设您的上下文环境是这样的——上面提到的UITextField是另一个由UIViewController控制的视图的子视图。我假设在某个时刻,该视图已经离开了屏幕(也许它是在UINavigationController的上下文中使用),并且在某个时候,您的应用程序会收到内存警告。
因此,假设您的UIViewController子类需要访问其视图以在屏幕上显示它。此时,nib文件将被加载,并且每个IBOutlet属性都将使用setValue:forKey:由nib加载代码设置。这里需要注意的重要部分包括将设置为UIViewController的view属性的顶级视图(这将保留此顶级视图)以及您的UITextField,它也将被保留。如果它只是被设置,nib加载代码会对其进行保留,否则该属性会将其保留。UITextField还将是顶级UIView的子视图,因此它将受到额外的保留,成为顶级view的子视图数组中的一部分,所以此时textfield已经被保留了两次。

如果你想要以编程方式切换文本字段,那么现在你可以这样做。使用属性可以更清晰地管理内存;你只需用一个新的自动释放的文本字段设置该属性即可。如果没有使用该属性,你必须记住释放它,并可能保留新的文本字段。此时,由于内存管理语义不包含在setter中,所以新的文本字段属于谁有些模糊。

现在假设另一个视图控制器被推到了UINavigation Controller的堆栈上,因此该视图不再处于前台。在发生内存警告的情况下,这个离屏视图控制器的视图将被卸载。此时,顶级UIView的视图属性将被置空,它将被释放并取消分配。

由于UITextField被设置为被保留的属性,因此UITextField不会被释放,就像它属于顶级视图的子视图数组一样被释放。

如果没有通过属性设置UITextField的实例变量,则它也会存在,因为当设置实例变量时,nib加载代码已经保留了它。

这个例子强调的一个有趣点是,由于UITextField通过属性另外被保留,你可能不希望在内存警告时保留它。因此,你应该在- [UIViewController viewDidUnload]方法中将该属性置为nil。这将消除UITextField上的最终释放并按预期释放它。如果使用属性,则必须明确释放它。虽然这两个操作在功能上是等效的,但意图不同。

如果你选择从视图中删除文本字段而不是交换出文本字段,你可能已经从视图层次结构中删除了它并将其属性设置为nil,或者释放了该文本字段。虽然在这种情况下编写正确的程序是可能的,但很容易犯错误,在viewDidUnload方法中过度释放文本字段。过度释放对象是一个导致崩溃的错误;再次将已经为空的属性设置为nil则不会。

我的描述可能过于冗长,但我不想在情境中漏掉任何细节。遵循指南将有助于您在遇到更复杂的情况时避免问题。

此外值得注意的是,在Mac OS X桌面版本上,内存管理行为有所不同。在桌面版本上,如果没有setter方法,设置IBOutlet不会保留实例变量;但如果setter方法可用,仍然会使用它。


2
Joey,写得太棒了。完美! - Jordan
3
啊,恐怕我在描述中犯了一个错误。iPhone OS和Mac OS X之间的区别在于,在iPhone OS上,如果没有setter可用,设置outlet将默认保留对象,因此您需要释放它。在Mac OS X上,如果没有setter可用,它只是简单地分配。对于我的错误,我已经在上面更正了我的帖子。 - Joey Hagedorn
1
这篇文章有点长,但我点了+1并收藏了它,将继续使用retain(我原本打算开始使用assign)。 - Dan Rosenstark
1
这里有另一个非常相关的答案,它明确指出了关于内存管理的一些要点:https://dev59.com/7nM_5IYBdhLWcg3wzmgw#1236985 - Joey Hagedorn

11

在内存管理方面,将something声明为IBOutlet不会产生任何影响(实际上,IBOutlet就是一个什么都没有的#defined)。 声明时包含IBOutlet的唯一原因是如果您打算在Interface Builder中连接它(这就是IBOutlet声明的用途,向IB提供提示)。

现在,仅在您打算编程分配它们时才为实例变量创建@property。 如果没有(也就是说,您只是在IB中设置您的UI),则无论是否创建属性都没有关系。 在我看来没有理由这样做。

回到您的问题。 如果您只是在IB中设置此ivar(usernameField),那么不必使用property,它不会影响任何内容。 如果您为usernameField创建了属性(因为您正在以编程方式创建它),请务必为其创建属性,并且如果需要,请绝对使属性保留。


6
事实上有两个模型:

旧模型

这个模型是Objective-C 2.0之前从Mac OS X继承下来的。它仍然可以使用,但是您不应该声明属性来修改ivars。

@interface StrokeWidthController : UIViewController {
    IBOutlet UISlider* slider;
    IBOutlet UILabel* label;
    IBOutlet StrokeDemoView* strokeDemoView;
    CGFloat strokeWidth;
}
@property (assign, nonatomic) CGFloat strokeWidth;
- (IBAction)takeIntValueFrom:(id)sender;
@end

在这种模型中,您不需要保留IBOutlet ivars,但是您需要释放它们。换句话说:
- (void)dealloc {
    [slider release];
    [label release];
    [strokeDemoView release];
    [super dealloc];
}

新模型

你需要为IBOutlet变量声明属性:

@interface StrokeWidthController : UIViewController {
    IBOutlet UISlider* slider;
    IBOutlet UILabel* label;
    IBOutlet StrokeDemoView* strokeDemoView;
    CGFloat strokeWidth;
}
@property (retain, nonatomic) UISlider* slider;
@property (retain, nonatomic) UILabel* label;
@property (retain, nonatomic) StrokeDemoView* strokeDemoView;
@property (assign, nonatomic) CGFloat strokeWidth;
- (IBAction)takeIntValueFrom:(id)sender;
@end

此外,您必须在dealloc中释放变量:
- (void)dealloc {
    self.slider = nil;
    self.label = nil;
    self.strokeDemoView = nil;
    [super dealloc];
}

此外,在非易碎平台上,您可以删除 ivars:
@interface StrokeWidthController : UIViewController {
    CGFloat strokeWidth;
}
@property (retain, nonatomic) IBOutlet UISlider* slider;
@property (retain, nonatomic) IBOutlet UILabel* label;
@property (retain, nonatomic) IBOutlet StrokeDemoView* strokeDemoView;
@property (assign, nonatomic) CGFloat strokeWidth;
- (IBAction)takeIntValueFrom:(id)sender;
@end

奇怪的事情

在这两种情况下,通过调用setValue:forKey:来设置插座。运行时内部(特别是_decodeObjectBinary)会检查是否存在setter方法。如果不存在(只存在ivar),它会向ivar发送一个额外的retain。因此,如果没有setter方法,您不应该保留IBOutlet。


2
传统的智慧是,在dealloc中不应该使用@property setters,因为如果你提供了自己的setter,它们可能会产生意想不到的后果。相反,你应该使用[someOutletVar release]; someOutletVar = nil;来避免在dealloc期间意外地分配内存。 - Paul Scott

2
在使用属性提供的访问器之前,这两个接口定义的工作方式没有任何区别。
在这两种情况下,您仍然需要在dealloc或viewDidUnload方法中释放和设置IBOutlet。
IBOutlet指向在XIB文件中实例化的对象。该对象由XIB文件的File's Owner对象拥有(通常是声明IBOutlet的视图控制器)。
因为对象是通过加载XIB创建的,所以它的保留计数为1,由上述File's Owner拥有。这意味着File's Owner在被解除分配时负责释放它。
添加具有retain属性的属性声明只是指定setter方法应保留传递进来要设置的对象-这是正确的方法。如果您没有在属性声明中指定保留,则IBOutlet可能指向可能不再存在的对象,因为它已由其所有者释放或在程序生命周期的某个时刻自动释放。保留它会防止该对象在您完成后被释放。

1
Nib文件中的对象创建时保留计数为1,然后自动释放。当UIKit重建对象层次结构时,使用setValue:forKey:重新建立对象之间的连接,该方法使用可用的setter方法或默认情况下保留对象(如果没有setter方法)。这意味着您拥有输出口的任何对象仍然有效。但是,如果有任何未存储在输出口中的顶级对象,则必须保留loadNibNamed:owner:options:方法返回的数组或数组内部的对象,以防止这些对象过早释放。

0
嗯,在第二种情况下,您正在为该特定的IBOutlet添加getter / setter方法。每当您添加getter / setter方法时,由于内存管理问题,您(几乎总是)希望将其设置为保留。我认为提问时更好的方式应该是这样的:
@interface RegisterController : UIViewController <UITextFieldDelegate>
{
IBOutlet UITextField *usernameField;
}
@property (nonatomic) IBOutlet UITextField *usernameField;

或者

@interface RegisterController : UIViewController <UITextFieldDelegate>
{
IBOutlet UITextField *usernameField;
}
@property (nonatomic, retain) IBOutlet UITextField *usernameField;

在这种情况下,是的,您需要添加一个保留,因为它会影响内存管理。即使它可能没有任何影响,如果您以编程方式添加和删除IBOutlet,您可能会遇到问题。
作为一般规则:每当您有一个IBOutlet时,始终添加一个带有保留的@property。

让我们暂时忘记getter/setter。在原始问题中,如果IBOutlets没有被保留,会发生什么不好的事情?代码在没有属性(retain)设置的情况下可以正常工作。我可以轻松访问usernameField,例如:usernameField.text。 - Jordan
如果您不添加保留,那么如果您从代码更改IBOutlet(当您的程序变得庞大时可能会发生这种情况),则可能会遇到问题。虽然现在可能没有问题,但最终可能会成为一个问题。请注意,如果您只是使用IB,它将完美地工作。 - Saurabh Sharan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接