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

深入探索Objective-C中的弱引用与强引用

2024-07-314.6k 阅读

一、内存管理基础

在Objective-C编程中,内存管理是一个核心问题。内存管理不当会导致内存泄漏,程序崩溃等一系列严重问题。Objective-C的内存管理模式主要有两种:手动引用计数(MRC)和自动引用计数(ARC)。在ARC模式下,编译器会自动插入内存管理代码,大大减轻了开发者手动管理内存的负担。而无论是MRC还是ARC,引用计数都是内存管理的关键机制。

引用计数的基本原理是,每个对象都有一个引用计数。当对象被创建时,其引用计数被设置为1。每当有一个新的强引用指向该对象时,其引用计数加1;而当一个强引用不再指向该对象时,其引用计数减1。当对象的引用计数变为0时,该对象所占用的内存就会被释放。

二、强引用(Strong Reference)

2.1 强引用的定义

强引用是Objective-C中默认的引用类型。当一个对象被强引用指向时,只要这个强引用存在,对象就不会被释放。例如,我们定义一个简单的类Person

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

在上述代码中,name属性使用了strong关键字修饰,这就是一个强引用。现在我们在另一个类中使用这个Person类:

#import "Person.h"

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    person.name = @"John";
}

@end

viewDidLoad方法中,person是一个强引用,指向新创建的Person对象。只要person变量存在,Person对象就不会被释放。当viewDidLoad方法执行完毕,person变量超出作用域,它对Person对象的强引用被移除,Person对象的引用计数减1。如果此时没有其他强引用指向该Person对象,那么该对象的引用计数变为0,其占用的内存就会被释放。

2.2 强引用循环(Strong Reference Cycle)

强引用循环是使用强引用时可能出现的一个严重问题。当两个或多个对象相互持有强引用时,就会形成强引用循环,导致对象无法被释放,从而造成内存泄漏。

例如,假设有两个类ClassAClassB,它们相互引用:

#import <Foundation/Foundation.h>

@interface ClassB;

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

@implementation ClassA
@end

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

@implementation ClassB
@end

然后在使用时:

#import "ClassA.h"
#import "ClassB.h"

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [[ClassA alloc] init];
    ClassB *b = [[ClassB alloc] init];
    a.classB = b;
    b.classA = a;
}

@end

在上述代码中,ab有一个强引用,ba也有一个强引用。当viewDidLoad方法执行完毕,ab超出作用域,但是由于它们相互持有强引用,它们的引用计数都不会变为0,导致这两个对象永远不会被释放,从而造成内存泄漏。

三、弱引用(Weak Reference)

3.1 弱引用的定义

弱引用是一种不会增加对象引用计数的引用类型。当对象的最后一个强引用被移除,对象被释放时,指向该对象的所有弱引用都会被自动设置为nil,避免了野指针的产生。

在Objective-C中,我们可以使用weak关键字来声明一个弱引用。例如,我们修改前面的ClassAClassB的例子,避免强引用循环:

#import <Foundation/Foundation.h>

@interface ClassB;

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

@implementation ClassA
@end

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

@implementation ClassB
@end

在这个例子中,ClassAClassB的引用变为了弱引用。这样,当ClassB对象的最后一个强引用被移除,ClassB对象被释放时,ClassAclassB属性会自动被设置为nil

3.2 弱引用的实现原理

在底层,弱引用的实现依赖于运行时系统。运行时会维护一个弱引用表,当对象被创建时,运行时会为其分配一个对应的弱引用表条目。当一个弱引用指向该对象时,运行时会将弱引用指针添加到这个表条目中。当对象的引用计数变为0并被释放时,运行时会遍历该对象的弱引用表条目,将所有的弱引用指针设置为nil

3.3 使用弱引用的场景

  • 视图控制器之间的父子关系:在iOS开发中,视图控制器之间经常存在父子关系。例如,一个导航控制器包含多个子视图控制器。通常,父视图控制器对其子视图控制器持有强引用,而子视图控制器对父视图控制器持有弱引用。这样可以避免强引用循环,确保当子视图控制器不再被需要时能够正常释放。
@interface ParentViewController : UIViewController
@property (nonatomic, strong) ChildViewController *childViewController;
@end

@implementation ParentViewController

- (void)loadChildViewController {
    ChildViewController *child = [[ChildViewController alloc] init];
    self.childViewController = child;
    [self addChildViewController:child];
    [self.view addSubview:child.view];
    [child didMoveToParentViewController:self];
}

@end

@interface ChildViewController : UIViewController
@property (nonatomic, weak) ParentViewController *parentViewController;
@end

@implementation ChildViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.parentViewController = (ParentViewController *)self.parentViewController;
}

@end
  • 解决代理模式中的强引用循环:代理模式是一种常用的设计模式,在Objective-C中广泛应用。在代理模式中,通常是一个对象(委托方)将某些任务委托给另一个对象(代理方)。为了避免强引用循环,委托方对代理方通常使用弱引用。
@protocol MyDelegate <NSObject>
- (void)doSomething;
@end

@interface MyClass : NSObject
@property (nonatomic, weak) id<MyDelegate> delegate;
@end

@implementation MyClass

- (void)someMethod {
    if ([self.delegate respondsToSelector:@selector(doSomething)]) {
        [self.delegate doSomething];
    }
}

@end

@interface MyDelegateClass : NSObject <MyDelegate>
@property (nonatomic, strong) MyClass *myClass;
@end

@implementation MyDelegateClass

- (void)doSomething {
    NSLog(@"Doing something...");
}

@end

在上述代码中,MyClassMyDelegateClass使用弱引用作为代理,避免了强引用循环。

四、弱引用和强引用的性能比较

从性能角度来看,强引用相对简单直接,因为它只涉及对象引用计数的增减操作。而弱引用由于需要运行时系统维护弱引用表,在对象创建、销毁以及弱引用赋值等操作时,会有额外的开销。

在对象创建时,除了常规的内存分配和初始化,运行时需要为对象分配弱引用表条目。在对象销毁时,运行时需要遍历弱引用表,将所有弱引用指针设置为nil。每次进行弱引用赋值时,运行时也需要操作弱引用表。

然而,这种额外开销在大多数情况下并不显著,尤其是在现代硬件和优化的运行时系统下。而且,与内存泄漏和野指针带来的严重问题相比,使用弱引用来避免这些问题是非常值得的。

五、在ARC和MRC下的使用差异

5.1 ARC下的使用

在ARC模式下,编译器会自动管理对象的内存,开发者只需要正确地使用strongweak关键字来声明引用类型。编译器会根据代码逻辑在合适的位置插入引用计数的增减代码。例如:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject
@end

@implementation MyObject
@end

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyObject *strongObject = [[MyObject alloc] init];
    __weak MyObject *weakObject = strongObject;
    strongObject = nil;
    NSLog(@"Weak object: %@", weakObject);
}

@end

在上述代码中,当strongObject被设置为nil时,MyObject对象的引用计数变为0并被释放,weakObject会自动被设置为nil

5.2 MRC下的使用

在MRC模式下,开发者需要手动管理对象的引用计数,使用retainreleaseautorelease等方法。虽然在MRC下也可以模拟弱引用的行为,但相对复杂。

例如,要实现类似弱引用的功能,我们可以自己维护一个引用列表,当对象被释放时,手动将列表中的所有引用设置为nil。但这种方式不仅繁琐,而且容易出错。因此,在MRC时代,避免强引用循环是一个更具挑战性的任务。

#import <Foundation/Foundation.h>

@interface MyObject : NSObject
{
    NSMutableArray *weakReferences;
}
- (void)addWeakReference:(id *)reference;
- (void)removeWeakReference:(id *)reference;
@end

@implementation MyObject

- (instancetype)init {
    self = [super init];
    if (self) {
        weakReferences = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)addWeakReference:(id *)reference {
    [weakReferences addObject:reference];
}

- (void)removeWeakReference:(id *)reference {
    [weakReferences removeObject:reference];
}

- (void)dealloc {
    for (id *weakRef in weakReferences) {
        *weakRef = nil;
    }
    [weakReferences release];
    [super dealloc];
}

@end

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyObject *strongObject = [[MyObject alloc] init];
    id __weak weakObject;
    [strongObject addWeakReference:&weakObject];
    [strongObject release];
    NSLog(@"Weak object: %@", weakObject);
}

@end

上述代码展示了在MRC下模拟弱引用的一种方式,但与ARC下使用weak关键字相比,明显复杂许多。

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

  1. 避免野指针:虽然弱引用在对象释放时会自动设置为nil,但在使用强引用时,如果不小心在对象释放后仍然使用指向该对象的指针,就会产生野指针,导致程序崩溃。因此,在释放对象后,务必将强引用指针设置为nil

  2. 合理选择引用类型:在设计对象之间的关系时,要仔细考虑应该使用强引用还是弱引用。如果对象A需要确保对象B在其生命周期内一直存在,那么使用强引用;如果对象A只是偶尔需要访问对象B,并且不希望影响对象B的生命周期,那么使用弱引用。

  3. 线程安全:无论是强引用还是弱引用,在多线程环境下使用时都需要注意线程安全。例如,在一个线程中释放对象,而另一个线程可能正在访问指向该对象的引用,这可能导致未定义行为。可以使用锁或其他同步机制来确保引用的安全访问。

  4. ARC和MRC的兼容性:如果项目中同时存在ARC和MRC代码,要特别注意引用类型的使用。在MRC代码中,不能直接使用weak关键字,需要采用前面提到的手动模拟弱引用的方式。而在ARC代码中,要避免手动调用retainreleaseautorelease等方法。

七、总结引用特性在实际项目中的应用

在实际的Objective-C项目中,正确使用强引用和弱引用是确保内存安全和程序稳定性的关键。在iOS开发中,视图层次结构、数据模型与视图之间的绑定、网络请求的回调等场景都涉及到引用类型的选择。

例如,在一个复杂的iOS应用中,可能存在多个视图控制器之间的嵌套和切换。如果视图控制器之间的引用关系处理不当,很容易出现强引用循环,导致内存泄漏。通过合理地使用弱引用,如子视图控制器对父视图控制器的弱引用,可以有效地避免这种情况。

在数据模型与视图的绑定中,视图通常需要显示数据模型中的数据。为了避免视图持有数据模型的强引用而导致数据模型无法释放,视图对数据模型可以使用弱引用。

网络请求的回调也是一个常见的场景。通常,视图控制器发起网络请求,并在请求完成时通过回调更新界面。如果回调闭包对视图控制器持有强引用,而视图控制器又对网络请求对象持有强引用,就可能形成强引用循环。通过将视图控制器的引用设置为弱引用,可以避免这种情况。

总之,深入理解Objective-C中的弱引用和强引用,并且在实际项目中正确应用,能够显著提高代码的质量和稳定性,减少内存相关的问题。开发者在编写代码时,应该时刻保持对引用关系的清晰认识,遵循合理的内存管理原则。