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

Objective-C属性修饰符(strong、weak、copy)详解

2022-08-016.3k 阅读

一、Objective-C 属性修饰符概述

在Objective-C 编程中,属性(properties)是一种用于封装对象数据的机制。属性修饰符则用于定义属性的行为,比如内存管理方式、访问控制等。其中,strongweakcopy 是非常重要且常用的内存管理相关的修饰符,它们决定了对象之间的引用关系以及对象何时被释放,深刻理解它们对于写出健壮、高效且内存安全的代码至关重要。

二、strong 修饰符

2.1 strong 的含义

strong 修饰符表示一种强引用关系。当一个对象被一个 strong 类型的属性引用时,这个对象的引用计数会增加。只要至少有一个 strong 引用指向该对象,这个对象就不会被释放。也就是说,strong 引用会“持有”对象,确保对象在需要时不会被销毁。

2.2 strong 的内存管理机制

在ARC(自动引用计数)环境下,strong 修饰符的内存管理是由编译器自动处理的。当一个 strong 引用指向一个新的对象时,对象的引用计数增加;当 strong 引用被释放(比如属性被赋值为 nil 或者包含该属性的对象被销毁)时,对象的引用计数减少。当对象的引用计数降为 0 时,对象的内存就会被自动释放。

下面通过一段简单的代码示例来展示 strong 修饰符的工作原理:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.name = @"John";
        
        NSLog(@"Person's name: %@", person.name);
    }
    return 0;
}

在上述代码中,Person 类有一个 strong 修饰的 name 属性。当我们创建一个 Person 对象并为 name 属性赋值为 @"John" 时,@"John" 这个字符串对象的引用计数增加。在 @autoreleasepool 块结束时,person 对象被销毁,其 name 属性的 strong 引用也被释放,@"John" 字符串对象的引用计数减少。由于此时没有其他 strong 引用指向 @"John",它的引用计数降为 0 并被释放(在ARC环境下自动完成)。

2.3 strong 的适用场景

strong 修饰符适用于大多数需要持有对象的情况。比如,当一个对象包含另一个对象,并且希望被包含的对象的生命周期与包含它的对象相关联时,就应该使用 strong。例如,一个 Car 类包含一个 Engine 类的实例,Car 对象持有 Engine 对象,只要 Car 对象存在,Engine 对象就应该存在,这时就可以使用 strong 修饰 Engine 属性。

#import <Foundation/Foundation.h>

@interface Engine : NSObject

@end

@implementation Engine

@end

@interface Car : NSObject

@property (nonatomic, strong) Engine *engine;

@end

@implementation Car

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *car = [[Car alloc] init];
        car.engine = [[Engine alloc] init];
        
        NSLog(@"Car has an engine.");
    }
    return 0;
}

在这个例子中,Car 对象通过 strong 引用持有 Engine 对象,确保在 Car 对象的生命周期内 Engine 对象不会被释放。

2.4 strong 可能导致的问题 - 循环引用

虽然 strong 修饰符在大多数情况下很有用,但如果使用不当,会导致循环引用的问题。循环引用是指两个或多个对象之间相互持有 strong 引用,使得它们的引用计数永远不会降为 0,从而导致内存泄漏。

以下是一个简单的循环引用示例:

#import <Foundation/Foundation.h>

@interface ClassA : NSObject

@property (nonatomic, strong) ClassB *classB;

@end

@implementation ClassA

@end

@interface ClassB : NSObject

@property (nonatomic, strong) ClassA *classA;

@end

@implementation ClassB

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        
        a.classB = b;
        b.classA = a;
    }
    return 0;
}

在上述代码中,ClassAClassB 相互持有对方的 strong 引用。当 @autoreleasepool 块结束时,ab 变量超出作用域,它们对 ClassAClassB 对象的 strong 引用被释放。但是,由于 ClassA 对象持有 ClassB 对象的 strong 引用,ClassB 对象也持有 ClassA 对象的 strong 引用,这两个对象的引用计数都不会降为 0,从而导致内存泄漏。

三、weak 修饰符

3.1 weak 的含义

weak 修饰符表示一种弱引用关系。与 strong 不同,weak 引用不会增加对象的引用计数。当对象的所有 strong 引用都被释放后,对象会被销毁,此时所有指向该对象的 weak 引用会自动被设置为 nil,这就是所谓的“弱引用归零”特性。

3.2 weak 的内存管理机制

weak 修饰符在内存管理方面依赖于运行时系统。运行时会维护一个全局的弱引用表,记录所有的 weak 引用及其指向的对象。当对象的引用计数变为 0 并即将被释放时,运行时会遍历弱引用表,将所有指向该对象的 weak 引用设置为 nil

以下是一个展示 weak 修饰符工作原理的代码示例:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __weak Person *weakPerson;
        
        {
            Person *strongPerson = [[Person alloc] init];
            strongPerson.name = @"Jane";
            weakPerson = strongPerson;
            
            NSLog(@"Weak person's name: %@", weakPerson.name);
        }
        
        NSLog(@"Weak person after strong person is deallocated: %@", weakPerson);
    }
    return 0;
}

在上述代码中,我们首先定义了一个 __weak 类型的 weakPerson 变量。在一个内部块中,我们创建了一个 strongPerson 对象,并将 weakPerson 指向 strongPerson。此时,weakPerson 是一个弱引用,不会增加 strongPerson 的引用计数。当内部块结束时,strongPerson 超出作用域,其 strong 引用被释放,strongPerson 对象被销毁。由于 weakPerson 是弱引用,它会自动被设置为 nil,所以最后打印 weakPerson 时,输出为 nil

3.3 weak 的适用场景

weak 修饰符常用于解决循环引用的问题。例如,在视图控制器(UIViewController)和视图(UIView)之间的关系中,视图控制器通常通过 strong 引用持有视图,而视图不需要持有视图控制器,否则会导致循环引用。这时,视图可以使用 weak 引用指向视图控制器。

#import <UIKit/UIKit.h>

@interface CustomView : UIView

@property (nonatomic, weak) UIViewController *viewController;

@end

@implementation CustomView

@end

@interface ViewController : UIViewController

@property (nonatomic, strong) CustomView *customView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.customView = [[CustomView alloc] initWithFrame:self.view.bounds];
    self.customView.viewController = self;
    [self.view addSubview:self.customView];
}

@end

在上述代码中,ViewController 通过 strong 引用持有 CustomView,而 CustomView 通过 weak 引用指向 ViewController,避免了循环引用。

另外,weak 修饰符还适用于当一个对象的生命周期不应该影响另一个对象的生命周期,但是又需要在某些情况下引用它的场景。比如,一个通知中心(NSNotificationCenter)的观察者可能希望观察某个对象的通知,但不希望持有该对象,以免影响其正常的生命周期,这时就可以使用 weak 引用。

3.4 weak 的局限性

虽然 weak 修饰符在解决循环引用等问题上非常有用,但它也有一些局限性。由于 weak 引用不会增加对象的引用计数,所以如果对象的唯一引用是 weak 引用,那么对象可能会在不经意间被销毁。此外,在多线程环境下,由于 weak 引用的归零操作是在运行时异步进行的,可能会出现短暂的“悬垂指针”问题,即 weak 引用还未被设置为 nil 时,对象已经被销毁,这需要开发者在多线程编程中特别注意。

四、copy 修饰符

4.1 copy 的含义

copy 修饰符用于创建对象的副本。当一个属性被声明为 copy 时,在赋值操作时,会对赋值的对象进行复制操作,而不是简单地增加引用计数。这意味着,赋值后,属性持有一个新的对象副本,而不是原始对象的引用。

4.2 copy 的内存管理机制

copy 操作涉及到对象的复制,具体的复制行为取决于对象遵循的协议。对于遵循 NSCopying 协议的对象,会调用 copyWithZone: 方法进行复制;对于遵循 NSMutableCopying 协议的对象,会调用 mutableCopyWithZone: 方法进行可变复制。

NSString 为例,NSString 是不可变的,遵循 NSCopying 协议。当使用 copy 修饰的 NSString 属性进行赋值时,会创建一个新的 NSString 对象副本。

#import <Foundation/Foundation.h>

@interface Document : NSObject

@property (nonatomic, copy) NSString *title;

@end

@implementation Document

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableString *mutableTitle = [NSMutableString stringWithString:@"Original Title"];
        
        Document *document = [[Document alloc] init];
        document.title = mutableTitle;
        
        NSLog(@"Document title: %@", document.title);
        
        [mutableTitle setString:@"Modified Title"];
        
        NSLog(@"Document title after modifying mutable string: %@", document.title);
    }
    return 0;
}

在上述代码中,我们创建了一个 NSMutableString 对象 mutableTitle,然后将其赋值给 Document 对象的 copy 修饰的 title 属性。此时,title 属性持有一个 NSString 对象副本。当我们修改 mutableTitle 时,document.title 不会受到影响,因为它是一个独立的副本。

4.3 copy 的适用场景

copy 修饰符主要用于防止对象被意外修改的场景。比如,在一个类中,如果希望某个属性的值不被外部修改(即使外部传递进来的是一个可变对象),就可以使用 copy 修饰符。常见的应用场景包括处理字符串、数组、字典等对象。

对于可变数组 NSMutableArray,如果使用 strong 修饰,外部对原始可变数组的修改会影响到类内部持有该数组的属性。而使用 copy 修饰,类内部持有一个不可变数组 NSArray 的副本,外部对原始可变数组的修改不会影响到类内部。

#import <Foundation/Foundation.h>

@interface DataContainer : NSObject

@property (nonatomic, copy) NSArray *items;

@end

@implementation DataContainer

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *mutableItems = [NSMutableArray arrayWithObjects:@"Item1", @"Item2", nil];
        
        DataContainer *container = [[DataContainer alloc] init];
        container.items = mutableItems;
        
        NSLog(@"Container items: %@", container.items);
        
        [mutableItems addObject:@"Item3"];
        
        NSLog(@"Container items after modifying mutable array: %@", container.items);
    }
    return 0;
}

在这个例子中,DataContaineritems 属性使用 copy 修饰,所以即使外部修改了 mutableItemscontainer.items 仍然保持不变。

4.4 copy 需要注意的问题

使用 copy 修饰符时,需要确保被复制的对象遵循相应的复制协议(NSCopyingNSMutableCopying)。如果对象没有正确实现这些协议,调用 copy 操作可能会导致运行时错误。另外,由于 copy 操作会创建新的对象,可能会带来一定的性能开销,特别是在处理大量数据时,需要谨慎使用。

五、strongweakcopy 的对比

  1. 内存管理方面
    • strong 增加对象的引用计数,持有对象,直到所有 strong 引用都被释放对象才会被销毁。
    • weak 不增加对象的引用计数,对象销毁时,所有 weak 引用自动归零。
    • copy 创建对象副本,新副本有自己独立的引用计数。
  2. 适用场景方面
    • strong 适用于对象之间有强关联,希望对象生命周期相互关联的场景。
    • weak 用于解决循环引用问题,以及对象之间不需要强关联,一个对象的生命周期不影响另一个对象生命周期的场景。
    • copy 用于防止对象被意外修改,希望持有一个独立副本的场景。
  3. 性能方面
    • strongweak 主要涉及引用计数的操作,性能开销相对较小。
    • copy 由于涉及对象的复制操作,特别是对于复杂对象,性能开销可能较大。

在实际编程中,需要根据具体的业务需求和对象之间的关系,准确选择合适的属性修饰符,以确保代码的正确性、稳定性和性能。通过合理运用 strongweakcopy 修饰符,可以编写出高质量的Objective - C 代码,避免常见的内存管理问题和程序错误。