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

深入理解Objective-C对象的创建与销毁

2023-03-102.6k 阅读

Objective-C 对象创建的基础

类与对象的概念

在 Objective-C 中,类(class)是一种抽象的数据类型,它定义了一组对象所共有的属性(成员变量)和行为(方法)。而对象(object)则是类的实例,是在程序运行时根据类的定义所创建的具体实体。例如,我们可以定义一个 Person 类,包含姓名、年龄等属性,以及吃饭、睡觉等行为。然后创建多个 Person 类的对象,每个对象都有自己独立的姓名和年龄值。

简单对象创建示例

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello, my name is %@, and I'm %ld years old.", self.name, (long)self.age);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.name = @"John";
        person.age = 30;
        [person sayHello];
    }
    return 0;
}

在上述代码中,我们首先定义了一个 Person 类,它有两个属性 nameage,以及一个方法 sayHello。在 main 函数中,通过 [[Person alloc] init] 创建了一个 Person 对象,并为其属性赋值,然后调用 sayHello 方法。

深入剖析对象创建过程

alloc 方法

alloc 方法是用于分配内存来创建对象的基础方法。当我们调用 alloc 时,它会为对象分配足够的内存空间,包括对象的实例变量以及一些额外的元数据。这些元数据用于存储对象所属的类信息、引用计数等。

#import <objc/runtime.h>

@interface Animal : NSObject
@property (nonatomic, strong) NSString *species;
@end

@implementation Animal
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [Animal alloc];
        NSLog(@"Class of animal: %@", object_getClass(animal));
    }
    return 0;
}

在这个例子中,我们调用 [Animal alloc] 创建了一个 Animal 对象,并通过 object_getClass 函数获取对象的类信息。可以看到,alloc 方法返回了一个已分配内存的对象,但其属性还未初始化。

init 方法

init 方法用于初始化对象的属性。在 alloc 分配内存后,通常紧接着调用 init 方法。init 方法的主要职责是为对象的实例变量设置初始值。

@interface Book : NSObject
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *author;
- (instancetype)initWithTitle:(NSString *)title author:(NSString *)author;
@end

@implementation Book
- (instancetype)initWithTitle:(NSString *)title author:(NSString *)author {
    self = [super init];
    if (self) {
        self.title = title;
        self.author = author;
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Book *book = [[Book alloc] initWithTitle:@"Objective - C Programming" author:@"John Doe"];
        NSLog(@"Book: %@ by %@", book.title, book.author);
    }
    return 0;
}

在上述 Book 类的实现中,我们定义了一个自定义的初始化方法 initWithTitle:author:。在这个方法中,首先调用 [super init],这是为了确保父类的初始化工作得以完成。如果父类的初始化成功(即 self 不为 nil),则为 Book 对象的属性赋值。

便捷构造方法

除了使用 allocinit 方法创建对象外,Objective - C 还允许我们定义便捷构造方法(convenience constructor)。这些方法将 allocinit 操作封装在一起,使对象创建更加简洁。

@interface Car : NSObject
@property (nonatomic, strong) NSString *brand;
@property (nonatomic, assign) NSInteger year;
+ (instancetype)carWithBrand:(NSString *)brand year:(NSInteger)year;
@end

@implementation Car
+ (instancetype)carWithBrand:(NSString *)brand year:(NSInteger)year {
    return [[self alloc] initWithBrand:brand year:year];
}
- (instancetype)initWithBrand:(NSString *)brand year:(NSInteger)year {
    self = [super init];
    if (self) {
        self.brand = brand;
        self.year = year;
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *car = [Car carWithBrand:@"Toyota" year:2020];
        NSLog(@"Car: %@ %ld", car.brand, (long)car.year);
    }
    return 0;
}

Car 类中,我们定义了一个便捷构造方法 carWithBrand:year:。这个类方法内部调用了 alloc 和自定义的初始化方法 initWithBrand:year:,外部使用时更加方便。

自动释放池与对象的生存周期

自动释放池的概念

自动释放池(autorelease pool)是 Objective - C 内存管理中的一个重要概念。当一个对象发送 autorelease 消息时,它并不会立即被释放,而是被放入到最近的自动释放池中。当自动释放池被销毁时,池中的所有对象都会收到 release 消息,如果对象的引用计数降为 0,则会被释放。

自动释放池的使用场景

在一些循环中创建大量临时对象时,如果不使用自动释放池,可能会导致内存峰值过高。例如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (NSInteger i = 0; i < 1000000; i++) {
            NSString *tempString = [NSString stringWithFormat:@"Number: %ld", (long)i];
            // 处理 tempString
        }
    }
    return 0;
}

在这个例子中,stringWithFormat: 方法返回的 NSString 对象会自动发送 autorelease 消息。由于在 @autoreleasepool 块内,当循环结束,自动释放池销毁时,这些临时的 NSString 对象会被释放,避免了内存占用持续上升。

手动创建自动释放池

除了系统默认的自动释放池,我们也可以手动创建自动释放池。例如在一个长时间运行的任务中,我们可以在任务的关键节点手动创建自动释放池来及时释放临时对象。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Start main autorelease pool");
        for (NSInteger i = 0; i < 10; i++) {
            @autoreleasepool {
                NSString *tempString = [NSString stringWithFormat:@"Inner loop: %ld", (long)i];
                NSLog(@"%@", tempString);
            }
        }
        NSLog(@"End main autorelease pool");
    }
    return 0;
}

在上述代码中,我们在主自动释放池内又创建了多个内部自动释放池,每个内部自动释放池负责管理其内部创建的临时对象,从而更好地控制内存。

对象销毁与内存管理

引用计数原理

Objective - C 使用引用计数(reference counting)来管理对象的内存。每个对象都有一个引用计数,当对象被创建时,引用计数初始化为 1。每当有新的引用指向该对象时,引用计数加 1;当一个引用不再指向该对象时,引用计数减 1。当引用计数降为 0 时,对象会被释放,其占用的内存也会被回收。

手动内存管理方法

在手动引用计数(MRC)模式下,我们需要手动调用 retainreleaseautorelease 方法来管理对象的引用计数。

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

@implementation Dog
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog1 = [[Dog alloc] init];
        dog1.name = @"Buddy";
        Dog *dog2 = dog1;
        [dog1 retain];
        NSLog(@"Reference count of dog1: %lu", (unsigned long)[dog1 retainCount]);
        [dog1 release];
        [dog1 release];
        // dog1 现在应该被释放了
        // 如果再次访问 dog1 会导致程序崩溃
    }
    return 0;
}

在上述代码中,我们通过 retain 方法增加对象的引用计数,通过 release 方法减少引用计数。需要注意的是,在 MRC 下,过度释放对象(引用计数小于 0)会导致程序崩溃。

自动引用计数(ARC)

为了简化内存管理,Objective - C 引入了自动引用计数(ARC)。在 ARC 模式下,编译器会自动在适当的位置插入 retainreleaseautorelease 代码。

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

@implementation Cat
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat1 = [[Cat alloc] init];
        cat1.name = @"Whiskers";
        Cat *cat2 = cat1;
        // 不需要手动调用 retain、release 等方法
        // ARC 会自动管理引用计数
    }
    return 0;
}

在 ARC 模式下,我们无需手动管理对象的引用计数,大大降低了内存管理的复杂性和出错的可能性。但仍需要注意一些内存管理的潜在问题,如循环引用。

解决循环引用问题

循环引用是指两个或多个对象相互持有对方的强引用,导致对象的引用计数永远不会降为 0,从而造成内存泄漏。例如:

@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end

@interface Child : NSObject
@property (nonatomic, strong) Parent *parent;
@end

@implementation Parent
@end

@implementation Child
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Parent *parent = [[Parent alloc] init];
        Child *child = [[Child alloc] init];
        parent.child = child;
        child.parent = parent;
        // parent 和 child 形成了循环引用
    }
    return 0;
}

为了解决这个问题,我们可以将其中一个引用改为弱引用(weak reference)。

@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end

@interface Child : NSObject
@property (nonatomic, weak) Parent *parent;
@end

@implementation Parent
@end

@implementation Child
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Parent *parent = [[Parent alloc] init];
        Child *child = [[Child alloc] init];
        parent.child = child;
        child.parent = parent;
        // 现在不会有循环引用问题了
    }
    return 0;
}

在上述代码中,Child 类的 parent 属性改为了弱引用,这样当 Parent 对象释放时,Child 对象的 parent 属性会自动变为 nil,避免了循环引用。

对象销毁时的清理工作

dealloc 方法

当对象的引用计数降为 0 时,对象会被释放,此时会调用 dealloc 方法。dealloc 方法用于执行对象销毁时的清理工作,如释放资源、关闭文件等。

#import <Foundation/Foundation.h>

@interface FileHandler : NSObject
@property (nonatomic, strong) NSString *fileName;
- (void)openFile;
- (void)closeFile;
@end

@implementation FileHandler
- (void)openFile {
    NSLog(@"Opening file: %@", self.fileName);
}
- (void)closeFile {
    NSLog(@"Closing file: %@", self.fileName);
}
- (void)dealloc {
    [self closeFile];
    NSLog(@"FileHandler object is being deallocated");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FileHandler *fileHandler = [[FileHandler alloc] init];
        fileHandler.fileName = @"example.txt";
        [fileHandler openFile];
    }
    return 0;
}

在上述 FileHandler 类中,我们在 dealloc 方法中调用了 closeFile 方法,确保在对象销毁时文件被正确关闭。

注意事项

dealloc 方法中,需要注意避免再次访问已释放的对象或引起其他内存管理问题。同时,在 ARC 模式下,不需要手动调用 [super dealloc],编译器会自动处理。

通过深入理解 Objective - C 对象的创建与销毁过程,我们能够更好地掌握内存管理,编写出高效、稳定的程序。无论是在手动引用计数还是自动引用计数环境下,合理地创建、使用和销毁对象都是保证程序性能和稳定性的关键。同时,对于对象销毁时的清理工作也不容忽视,确保资源的正确释放和程序的正常结束。