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

深入学习Objective-C属性的内存管理特性

2023-11-107.9k 阅读

一、Objective-C 属性概述

在 Objective-C 中,属性(Properties)是一种简洁的方式来封装对象的实例变量。通过属性,开发者可以更方便地访问和修改对象的状态,同时也有助于提高代码的可读性和可维护性。属性不仅仅是简单的变量声明,它背后还涉及到很多复杂的机制,其中内存管理特性就是非常重要的一部分。

在声明属性时,通常使用 @property 关键字,例如:

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *myString;
@end

上述代码声明了一个名为 myString 的属性,它的类型是 NSString,并带有 nonatomicstrong 两个特性。接下来,我们将深入探讨这些内存管理相关的特性。

二、内存管理特性之 strong

2.1 strong 关键字的含义

strong 是一种强引用类型。当一个对象被一个 strong 类型的属性所引用时,该对象的引用计数会增加。只要有至少一个 strong 引用指向这个对象,它就不会被释放。

2.2 代码示例

@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";
        // 此时,@"John" 这个字符串对象的引用计数至少为 1
        // 因为 person.name 是 strong 引用指向它
    }
    // 当 autoreleasepool 结束时,person 对象被释放
    // person.name 对 @"John" 的 strong 引用消失
    // @"John" 字符串对象如果没有其他 strong 引用,也会被释放
    return 0;
}

在这个例子中,person.namestrong 类型的属性,它对 @"John" 字符串对象形成了强引用,使该字符串对象在 person 对象存在期间不会被释放。

2.3 strong 的应用场景

strong 通常用于大多数情况下对象之间的正常引用关系。例如,一个视图控制器(ViewController)持有它所管理的视图(View),视图控制器对视图的引用就应该使用 strong。这样可以确保在视图控制器存在期间,视图不会被意外释放,从而保证了界面的正常显示和交互。

三、内存管理特性之 weak

3.1 weak 关键字的含义

weak 是一种弱引用类型。与 strong 不同,weak 引用不会增加对象的引用计数。当对象的最后一个 strong 引用消失,对象被释放时,所有指向该对象的 weak 引用会自动被设置为 nil。这种特性有效地避免了野指针(dangling pointer)的产生。

3.2 代码示例

@interface ParentView : UIView
@end

@implementation ParentView
@end

@interface ChildView : UIView
@property (nonatomic, weak) ParentView *parent;
@end

@implementation ChildView
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ParentView *parentView = [[ParentView alloc] init];
        ChildView *childView = [[ChildView alloc] init];
        childView.parent = parentView;
        // 此时,parentView 有一个 strong 引用(parentView 自身)
        // childView.parent 是 weak 引用,不会增加 parentView 的引用计数
        parentView = nil;
        // 当 parentView 被设置为 nil 时,原来的 ParentView 对象的 strong 引用消失
        // 对象被释放,同时 childView.parent 自动被设置为 nil
        if (childView.parent == nil) {
            NSLog(@"Parent view has been deallocated.");
        }
    }
    return 0;
}

在上述代码中,ChildViewparent 属性是 weak 类型,当 ParentView 对象被释放时,childView.parent 自动变为 nil,避免了野指针问题。

3.3 weak 的应用场景

weak 常用于解决对象之间的循环引用问题。例如,在视图层级关系中,子视图对父视图的引用通常使用 weak。因为父视图已经对所有子视图有 strong 引用,如果子视图也对父视图使用 strong 引用,就会形成循环引用,导致对象无法正常释放。使用 weak 可以打破这种循环,确保内存的正确管理。

四、内存管理特性之 assign

4.1 assign 关键字的含义

assign 用于基本数据类型(如 intfloatdouble 等)和 CGFloatNSInteger 等非对象类型。它不会涉及到对象的引用计数管理,只是简单地赋值。对于对象类型,assign 不建议使用,因为当对象被释放后,指向它的 assign 类型的指针不会自动变为 nil,会导致野指针问题。

4.2 代码示例

@interface MathCalculator : NSObject
@property (nonatomic, assign) int result;
@end

@implementation MathCalculator
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MathCalculator *calculator = [[MathCalculator alloc] init];
        calculator.result = 10 + 5;
        // 这里简单地对 result 进行赋值,result 是基本数据类型 int
    }
    return 0;
}

在这个例子中,resultint 类型,使用 assign 进行属性声明,符合其使用场景。

4.3 assign 的应用场景

assign 主要用于基本数据类型,因为基本数据类型本身不涉及对象的内存管理,不需要引用计数操作。它简单直接地进行值的存储和读取,效率较高。

五、内存管理特性之 copy

5.1 copy 关键字的含义

copy 关键字用于创建对象的副本。当使用 copy 声明属性时,在赋值操作时会创建被赋值对象的一个副本,并将副本赋给属性。对于不可变对象(如 NSStringNSArrayNSDictionary 等),copy 操作实际上返回的是对象本身(因为不可变对象不需要创建新的副本),而对于可变对象(如 NSMutableStringNSMutableArrayNSMutableDictionary 等),copy 会创建一个新的不可变副本。

5.2 代码示例

@interface Document : NSObject
@property (nonatomic, copy) NSString *content;
@end

@implementation Document
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableString *mutableContent = [NSMutableString stringWithString:@"Initial content"];
        Document *document = [[Document alloc] init];
        document.content = mutableContent;
        // 这里 document.content 会得到 mutableContent 的一个不可变副本
        [mutableContent appendString:@" - modified"];
        // 即使 mutableContent 被修改,document.content 仍然保持不变
        NSLog(@"Document content: %@", document.content);
        // 输出: Document content: Initial content
    }
    return 0;
}

在上述代码中,Documentcontent 属性使用 copy,当 mutableContent 被修改时,document.content 不受影响,因为它是 mutableContent 的副本。

5.3 copy 的应用场景

copy 常用于确保属性的值不受外部可变对象修改的影响。例如,在一个表示用户信息的类中,如果有一个存储用户名称的属性,为了防止外部代码通过修改原始的可变字符串来改变用户名称,就可以使用 copy 来存储用户名称。这样,即使外部的字符串对象被修改,类内部存储的用户名称仍然保持不变。

六、循环引用问题及解决

6.1 循环引用的产生

循环引用是指两个或多个对象之间相互持有强引用,导致对象无法被释放,从而造成内存泄漏。例如,假设有两个类 ClassAClassB

@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;
        // 此时,a 和 b 相互持有 strong 引用,形成循环引用
        // 即使 a 和 b 超出作用域,它们也不会被释放
    }
    return 0;
}

在这个例子中,ab 有一个 strong 引用,ba 也有一个 strong 引用,形成了循环引用,导致 ab 在超出作用域时无法被释放。

6.2 解决循环引用的方法

  1. 使用 weak 或 unowned:如前文所述,在可能形成循环引用的地方,将其中一个强引用改为 weakunowned。例如,将 ClassBclassA 属性改为 weak
@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end

这样,abstrong 引用,而 baweak 引用,不会形成循环引用,当 ab 超出作用域时,它们可以正常被释放。

  1. 使用 block 时注意循环引用:在使用 block 时也容易出现循环引用问题。例如:
@interface MyViewController : UIViewController
@property (nonatomic, strong) NSString *titleText;
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.titleText = @"New title";
        }
    });
}
@end

在上述代码中,使用 __weak 声明一个弱引用 weakSelf,然后在 block 内部通过 __strong 重新获取强引用 strongSelf,这样既避免了循环引用,又确保了在 block 执行期间 self 不会被释放。

七、autoreleasepool 与属性内存管理

7.1 autoreleasepool 的作用

autoreleasepool 是 Objective-C 内存管理中的一个重要概念。它的主要作用是延迟对象的释放。当一个对象发送 autorelease 消息时,它不会立即被释放,而是被放入最近的 autoreleasepool 中。当 autoreleasepool 被销毁时,它会向池中的所有对象发送 release 消息,如果对象的引用计数变为 0,则对象被释放。

7.2 属性与 autoreleasepool 的关系

在属性的内存管理中,autoreleasepool 也会产生影响。例如,当通过方法返回一个属性值时,如果这个属性指向的对象是通过 autorelease 方式创建的,那么这个对象的生命周期就与 autoreleasepool 相关。

@interface DataFetcher : NSObject
@property (nonatomic, strong) NSArray *dataArray;
- (NSArray *)fetchData;
@end

@implementation DataFetcher
- (NSArray *)fetchData {
    NSMutableArray *tempArray = [[NSMutableArray alloc] init];
    // 假设这里填充 tempArray 的数据
    self.dataArray = tempArray;
    return self.dataArray;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DataFetcher *fetcher = [[DataFetcher alloc] init];
        NSArray *data = [fetcher fetchData];
        // data 指向的对象是通过 autorelease 方式创建的(因为 tempArray 是在方法内部创建并赋值给属性)
        // 当 autoreleasepool 结束时,data 指向的对象可能会被释放(如果没有其他 strong 引用)
    }
    return 0;
}

在这个例子中,fetchData 方法返回的 dataArray 中的对象是通过 autorelease 方式创建的,其生命周期与 autoreleasepool 相关。如果在 autoreleasepool 结束前没有其他 strong 引用指向这个对象,它就会被释放。

7.3 合理使用 autoreleasepool 优化内存

在一些情况下,合理地创建和使用 autoreleasepool 可以优化内存。例如,在一个循环中创建大量临时对象时,如果不及时释放这些对象,可能会导致内存峰值过高。这时可以在循环内部创建一个 autoreleasepool,使临时对象在每次循环结束时及时释放。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (int i = 0; i < 10000; i++) {
            @autoreleasepool {
                NSString *tempString = [NSString stringWithFormat:@"Number %d", i];
                // 这里 tempString 是 autorelease 对象,在内部 autoreleasepool 结束时会被释放
            }
        }
    }
    return 0;
}

通过这种方式,可以有效地控制内存峰值,提高程序的性能和稳定性。

八、属性内存管理在 ARC 和 MRC 下的差异

8.1 ARC(自动引用计数)简介

ARC 是从 iOS 5.0 开始引入的一种自动内存管理机制。在 ARC 环境下,编译器会自动插入适当的内存管理代码,如 retainreleaseautorelease,开发者无需手动编写这些代码,大大减轻了内存管理的负担,同时也减少了因手动管理不当而导致的内存泄漏和悬空指针等问题。

8.2 MRC(手动引用计数)简介

MRC 是在 ARC 之前使用的手动内存管理方式。开发者需要手动调用 retain 来增加对象的引用计数,调用 release 来减少引用计数,以及使用 autorelease 来延迟对象的释放。这种方式对开发者的要求较高,容易出现内存管理错误。

8.3 属性内存管理在 ARC 和 MRC 下的差异

  1. 声明方式:在 ARC 和 MRC 下,属性的声明方式基本相同,但在 MRC 下,需要更加注意属性的内存管理特性与手动引用计数操作的配合。例如,在 MRC 下,使用 strong 类似的概念时,需要手动调用 retain 方法来增加引用计数。
  2. 内存管理代码:在 ARC 下,编译器自动处理对象的引用计数变化,开发者无需手动编写 retainreleaseautorelease 代码。而在 MRC 下,对于 strong 类型的属性,在赋值时需要手动调用 retain,在属性被释放时需要手动调用 release。例如:
// MRC 下的代码
@interface MyClass : NSObject
@property (nonatomic, strong) NSString *myString;
@end

@implementation MyClass
- (void)setMyString:(NSString *)newString {
    if (_myString != newString) {
        [_myString release];
        _myString = [newString retain];
    }
}
- (void)dealloc {
    [_myString release];
    [super dealloc];
}
@end

而在 ARC 下,编译器会自动生成类似的内存管理代码,开发者只需要关注业务逻辑即可。 3. 循环引用处理:在处理循环引用问题上,无论是 ARC 还是 MRC,都需要开发者手动打破循环引用。但在 MRC 下,由于手动管理引用计数,处理不当更容易导致内存泄漏。而在 ARC 下,使用 weakunowned 来解决循环引用更加直观和方便。

九、总结与最佳实践

  1. 理解内存管理特性:深入理解 strongweakassigncopy 等内存管理特性的含义和应用场景是正确管理内存的基础。根据对象之间的关系和业务需求,选择合适的特性来声明属性。
  2. 避免循环引用:时刻警惕循环引用问题,特别是在对象之间存在相互引用关系时。使用 weakunowned 来打破循环引用,确保对象能够正常释放。
  3. 合理使用 autoreleasepool:在创建大量临时对象的情况下,合理使用 autoreleasepool 可以有效控制内存峰值,提高程序性能。
  4. 关注 ARC 和 MRC 的差异:如果项目使用 MRC,要严格按照手动引用计数的规则来管理内存;如果使用 ARC,虽然编译器自动处理了大部分内存管理代码,但仍然需要理解其原理,以便更好地处理复杂的内存管理场景。

通过深入学习和实践这些内容,开发者可以更加熟练地掌握 Objective-C 属性的内存管理特性,编写出高效、稳定且内存安全的应用程序。