MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

了解Objective-C中弱引用(weak)与强引用(strong)

2023-08-318.0k 阅读

一、内存管理基础

在深入探讨 Objective-C 中的弱引用(weak)与强引用(strong)之前,我们先来回顾一下内存管理的基础知识。

在编程中,内存管理是至关重要的一环。当我们创建一个对象时,系统会为其分配内存空间,而当这个对象不再被使用时,我们需要正确地释放这些内存,以便其他程序可以使用这部分内存资源。如果没有正确地管理内存,就可能会导致内存泄漏(memory leak),即已分配的内存无法被释放,随着程序运行时间的增加,占用的内存越来越多,最终导致系统资源耗尽,程序崩溃。

在 Objective-C 中,内存管理主要依赖于引用计数(reference counting)机制。每个对象都有一个引用计数,当对象被创建时,引用计数初始化为 1。每当有一个新的强引用指向该对象时,引用计数就会加 1;而当一个强引用不再指向该对象(比如强引用变量被释放或者重新赋值)时,引用计数就会减 1。当引用计数变为 0 时,系统会自动释放该对象所占用的内存空间。

二、强引用(strong)

2.1 强引用的定义与作用

强引用是 Objective-C 中默认的引用类型。当我们使用 strong 关键字声明一个变量来指向一个对象时,就创建了一个强引用。强引用的作用是保持对象的存活,只要有至少一个强引用指向对象,对象就不会被释放。

例如,我们创建一个简单的 NSString 对象,并使用强引用指向它:

NSString *strongString = @"Hello, World!";

在这个例子中,strongString 是一个强引用,它指向了字符串对象 "Hello, World!"。只要 strongString 存在,这个字符串对象就会一直存活在内存中。

2.2 强引用的生命周期管理

当强引用变量超出其作用域或者被赋值为 nil 时,它对对象的强引用就会被释放,对象的引用计数会相应减 1。

来看下面这个例子:

{
    NSString *strongString = @"Hello, World!";
    // 此时 strongString 对字符串对象有强引用,对象引用计数至少为 1
}
// strongString 超出作用域,被释放,其对字符串对象的强引用消失,字符串对象引用计数减 1

在这个代码块中,当 strongString 超出其作用域时,它对字符串对象的强引用被释放。如果此时没有其他强引用指向该字符串对象,那么字符串对象的引用计数就会变为 0,系统会自动释放该对象所占用的内存。

2.3 强引用循环(Retain Cycle)问题

强引用虽然在保持对象存活方面起到了重要作用,但如果使用不当,会导致强引用循环(也称为 retain cycle)的问题。

强引用循环是指两个或多个对象之间通过强引用相互引用,形成一个闭环,使得这些对象的引用计数永远不会变为 0,从而导致内存泄漏。

例如,假设有两个类 ClassAClassB,它们之间存在如下的相互强引用关系:

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

然后在使用时:

ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.classB = b;
b.classA = a;

在这个例子中,ab 有一个强引用(通过 a.classB),ba 也有一个强引用(通过 b.classA)。这样就形成了一个强引用循环。即使 ab 超出了它们的作用域,由于它们相互持有强引用,它们的引用计数都不会变为 0,也就不会被释放,从而导致内存泄漏。

三、弱引用(weak)

3.1 弱引用的定义与作用

弱引用(weak)是一种不增加对象引用计数的引用类型。弱引用的主要作用是解决强引用循环问题,同时在某些情况下,当我们希望对象在被释放时,指向它的引用能够自动变为 nil,以避免野指针(dangling pointer)的出现,弱引用就非常有用。

例如,我们对前面的 ClassAClassB 例子进行修改,将其中一个引用改为弱引用:

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

然后同样使用:

ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.classB = b;
b.classA = a;

此时,虽然 ab 有强引用,但 ba 是弱引用。当 a 超出作用域被释放时,ba 的弱引用不会阻止 a 被释放,a 被释放后,b.classA 会自动变为 nil。同样,当 b 超出作用域被释放时,a.classB 会因为强引用消失而导致 b 被释放。这样就避免了强引用循环导致的内存泄漏问题。

3.2 弱引用的特性

  1. 不增加引用计数:与强引用不同,弱引用不会增加对象的引用计数。这意味着即使有多个弱引用指向一个对象,只要没有强引用指向该对象,对象就会被释放。
  2. 自动置 nil:当被弱引用指向的对象被释放时,所有指向该对象的弱引用都会自动被设置为 nil。这可以有效避免野指针问题。例如:
NSString *strongString = @"Hello, World!";
__weak NSString *weakString = strongString;
strongString = nil;
// 此时 strongString 不再指向字符串对象,字符串对象引用计数减为 0 并被释放
// weakString 会自动变为 nil
  1. 弱引用对象可能为 nil:由于弱引用不保证对象的存活,在使用弱引用指向的对象之前,需要先检查该对象是否为 nil,以防止程序崩溃。例如:
__weak NSString *weakString;
// 这里 weakString 初始化为 nil
if (weakString) {
    NSLog(@"%@", weakString);
}

3.3 弱引用的应用场景

  1. 视图控制器之间的父子关系:在 iOS 开发中,视图控制器之间常常存在父子关系。例如,一个导航控制器(UINavigationController)管理着多个子视图控制器。通常,父视图控制器会对其子视图控制器持有强引用,而子视图控制器对父视图控制器持有弱引用。这样可以避免强引用循环,同时确保子视图控制器在被释放时,父视图控制器的引用不会变成野指针。
@interface ParentViewController : UIViewController
@property (nonatomic, strong) ChildViewController *childVC;
@end

@interface ChildViewController : UIViewController
@property (nonatomic, weak) ParentViewController *parentVC;
@end
  1. 解决委托(delegate)中的强引用循环:委托模式在 Objective-C 中广泛应用。在委托关系中,通常是委托者(delegator)对代理(delegate)持有弱引用。例如,一个自定义的视图可能会有一个代理来处理特定的事件,视图对代理持有弱引用,以避免强引用循环。
@protocol CustomViewDelegate <NSObject>
- (void)customViewDidTap:(CustomView *)view;
@end

@interface CustomView : UIView
@property (nonatomic, weak) id<CustomViewDelegate> delegate;
@end

四、强引用与弱引用的对比

4.1 内存管理方面

强引用通过增加对象的引用计数来保持对象的存活,只要有强引用存在,对象就不会被释放。而弱引用不增加对象的引用计数,当对象没有强引用时,即使有弱引用指向它,对象也会被释放。

例如,我们创建一个简单的对象,并分别使用强引用和弱引用指向它:

NSObject *strongObject = [[NSObject alloc] init];
__weak NSObject *weakObject = strongObject;
strongObject = nil;
// 此时 strongObject 不再指向对象,对象引用计数减为 0 并被释放
// weakObject 会自动变为 nil

在这个例子中,当 strongObject 被赋值为 nil 后,对象的引用计数变为 0 并被释放,而 weakObject 自动变为 nil。这体现了强引用和弱引用在内存管理上的不同机制。

4.2 防止野指针方面

强引用在对象被释放后,如果没有将强引用变量赋值为 nil,就可能会产生野指针。例如:

NSObject *strongObject = [[NSObject alloc] init];
NSObject *anotherStrongObject = strongObject;
strongObject = nil;
// 此时 anotherStrongObject 成为野指针,如果使用它访问对象,可能会导致程序崩溃

而弱引用在对象被释放时,会自动将弱引用变量设置为 nil,从而避免了野指针问题。例如:

NSObject *strongObject = [[NSObject alloc] init];
__weak NSObject *weakObject = strongObject;
strongObject = nil;
// 此时 weakObject 自动变为 nil,避免了野指针问题

4.3 应用场景方面

强引用适用于需要确保对象存活的场景,比如在一个对象需要长期存在并被频繁访问的情况下,使用强引用是合适的。例如,一个应用的核心数据模型对象,可能需要被多个模块访问和操作,使用强引用可以保证其在整个应用生命周期内存活。

而弱引用主要用于解决强引用循环问题,以及在对象之间存在临时性、非必需的引用关系时使用。如前面提到的视图控制器之间的父子关系、委托关系等场景。

五、使用弱引用和强引用的注意事项

5.1 弱引用使用注意事项

  1. 使用前检查 nil:由于弱引用不保证对象的存活,在使用弱引用指向的对象之前,一定要先检查该对象是否为 nil。否则,可能会导致程序崩溃。例如:
__weak NSString *weakString;
// 假设这里 weakString 指向了一个对象,但是对象可能已经被释放
if (weakString) {
    NSLog(@"%@", weakString);
}
  1. 避免链式弱引用:虽然在某些情况下可以使用链式弱引用,但这样做可能会增加代码的复杂性和可读性。尽量保持弱引用关系的简单和清晰。例如,避免这样的代码:
__weak NSObject *weakObject1;
__weak NSObject *weakObject2 = weakObject1;
// 这样的链式弱引用可能会让人困惑,并且在对象释放顺序上容易出错

5.2 强引用使用注意事项

  1. 避免强引用循环:在设计对象之间的引用关系时,要特别注意避免强引用循环。仔细分析对象之间的依赖关系,合理使用强引用和弱引用,确保对象能够在不再需要时被正确释放。如前面提到的 ClassAClassB 的例子,通过将其中一个引用改为弱引用可以避免强引用循环。
  2. 合理管理强引用生命周期:要清楚强引用变量的生命周期,确保在适当的时候释放强引用,以避免不必要的内存占用。例如,在一个方法中创建了一个强引用对象,当方法执行完毕后,如果不再需要该对象,应该及时将强引用变量赋值为 nil,以便对象可以被释放。

六、实际项目中的应用案例

6.1 视图控制器之间的引用关系优化

在一个复杂的 iOS 应用中,视图控制器之间的跳转和交互非常频繁。例如,有一个主视图控制器(MainViewController),它可以跳转到一个详情视图控制器(DetailViewController),并且 DetailViewController 可以通过一个回调方法通知 MainViewController 某些事件。

在这种情况下,如果 MainViewControllerDetailViewController 持有强引用,而 DetailViewController 又对 MainViewController 持有强引用,就会形成强引用循环。为了避免这种情况,我们可以将 DetailViewControllerMainViewController 的引用改为弱引用。

@interface MainViewController : UIViewController
@property (nonatomic, strong) DetailViewController *detailVC;
@end

@interface DetailViewController : UIViewController
@property (nonatomic, weak) MainViewController *mainVC;
- (void)doSomethingAndNotifyMainVC;
@end

@implementation DetailViewController
- (void)doSomethingAndNotifyMainVC {
    if (self.mainVC) {
        // 通知 MainViewController
    }
}
@end

这样,当 DetailViewController 被释放时,不会因为对 MainViewController 的强引用而导致 MainViewController 无法释放,从而避免了内存泄漏。

6.2 自定义控件与代理的关系处理

假设我们开发了一个自定义的表格视图控件(CustomTableView),它需要一个代理来处理单元格点击等事件。如果 CustomTableView 对代理持有强引用,而代理又可能对 CustomTableView 有其他操作,就可能形成强引用循环。

通过将 CustomTableView 对代理的引用设置为弱引用,可以解决这个问题。

@protocol CustomTableViewDelegate <NSObject>
- (void)customTableView:(CustomTableView *)tableView didSelectCellAtIndex:(NSIndexPath *)indexPath;
@end

@interface CustomTableView : UITableView
@property (nonatomic, weak) id<CustomTableViewDelegate> delegate;
@end

这样,即使代理对象和 CustomTableView 对象之间有复杂的交互,也不会因为强引用循环而导致内存泄漏。

七、总结强引用和弱引用的要点

  1. 强引用
    • 保持对象存活,增加对象引用计数。
    • 默认的引用类型,使用不当可能导致强引用循环和内存泄漏。
    • 在对象需要长期存活并被频繁访问时使用。
  2. 弱引用
    • 不增加对象引用计数,用于解决强引用循环问题。
    • 当对象被释放时,弱引用自动变为 nil,避免野指针。
    • 在对象之间存在临时性、非必需的引用关系时使用,如视图控制器父子关系、委托关系等。

在实际的 Objective-C 编程中,正确理解和使用强引用与弱引用是非常重要的,它直接关系到程序的内存管理和稳定性。通过合理运用这两种引用类型,可以编写出高效、稳定且内存安全的代码。无论是开发小型应用还是大型项目,对内存管理和引用类型的深入理解都是开发者必备的技能。