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

Objective-C运行时机制中的内存布局与对象生命周期管理

2023-02-072.5k 阅读

Objective-C运行时机制中的内存布局

类的内存布局

在Objective-C中,类是对象的抽象模板,它定义了对象的属性和行为。从内存布局的角度来看,类本身在内存中也有特定的结构。类结构体objc_class是Objective-C运行时中描述类的核心数据结构。在早期的运行时源码中,objc_class定义如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

在现代的64位运行时环境下,objc_class的定义有所变化,以适应64位系统的特性和优化需求。但核心的组成部分依然类似,包括指向元类的isa指针,指向父类的指针(如果有父类),类名,实例变量列表,方法列表,缓存以及协议列表等。

  1. isa指针:每个对象和类都有一个isa指针,对象的isa指针指向它的类,而类的isa指针指向它的元类。元类是用来存储类方法的地方。通过isa指针,运行时系统能够快速定位到对象所属的类以及类方法的定义位置。例如:
@interface MyClass : NSObject
+ (void)classMethod;
@end

@implementation MyClass
+ (void)classMethod {
    NSLog(@"This is a class method");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class myClass = [MyClass class];
        Class metaClass = object_getClass(myClass);
        IMP imp = class_getMethodImplementation(metaClass, @selector(classMethod));
        ((void (*)(id, SEL))imp)(myClass, @selector(classMethod));
    }
    return 0;
}

在这段代码中,我们通过object_getClass获取MyClass的元类,然后通过class_getMethodImplementation获取类方法classMethod的实现,并调用它。

  1. super_class指针:指向该类的父类。如果一个类没有父类(如NSObject),则该指针为nil。通过这个指针,运行时系统能够实现继承机制,当在当前类中找不到某个方法时,会沿着super_class指针在父类中查找。
@interface ParentClass : NSObject
- (void)parentMethod;
@end

@implementation ParentClass
- (void)parentMethod {
    NSLog(@"This is a parent method");
}
@end

@interface ChildClass : ParentClass
@end

@implementation ChildClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ChildClass *child = [[ChildClass alloc] init];
        [child parentMethod];
    }
    return 0;
}

在这个例子中,ChildClass继承自ParentClassChildClass本身没有实现parentMethod,但通过super_class指针,运行时能在ParentClass中找到该方法并调用。

  1. 实例变量列表(ivars):存储了该类实例对象的成员变量信息。每个实例变量在列表中有一个对应的objc_ivar结构体,记录了变量的名称、类型和偏移量等信息。偏移量用于在实例对象内存中定位该变量的位置。例如:
@interface Person : NSObject {
    NSString *name;
    int age;
}
@end

@implementation Person
@end

在这个Person类中,nameage就是实例变量。运行时系统会根据objc_ivar_list中的信息为每个实例对象分配内存空间,并确定每个变量在内存中的位置。

  1. 方法列表(methodLists):是一个指向objc_method_list的指针数组。每个objc_method_list包含了一组相关的方法,如实例方法或类方法。objc_method结构体包含了方法的选择器(SEL)、方法的实现(IMP)以及方法的类型编码(用于描述方法的参数和返回值类型)。例如:
@interface MyClass : NSObject
- (void)instanceMethod;
@end

@implementation MyClass
- (void)instanceMethod {
    NSLog(@"This is an instance method");
}
@end

这里instanceMethod会被添加到MyClass的实例方法列表中。运行时系统在接收到对象的消息时,会在方法列表中查找与消息对应的选择器,进而找到方法的实现并执行。

  1. 缓存(cache):为了提高方法查找的效率,运行时系统为每个类维护了一个方法缓存。当一个方法被调用时,首先会在缓存中查找,如果找到则直接调用,避免了在整个方法列表中进行线性查找。缓存以哈希表的形式存储,键为方法选择器(SEL),值为方法的实现(IMP)。
@interface MyClass : NSObject
- (void)aMethod;
@end

@implementation MyClass
- (void)aMethod {
    NSLog(@"This is a method");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        [obj aMethod];
        // 第二次调用时,可能从缓存中直接获取方法实现
        [obj aMethod];
    }
    return 0;
}

在这个例子中,第一次调用aMethod时,运行时会在方法列表中查找并执行,同时将方法的选择器和实现添加到缓存中。第二次调用时,如果缓存命中,则能更快地执行方法。

  1. 协议列表(protocols):存储了该类所遵循的协议。协议定义了一组方法的声明,但不提供实现。一个类遵循某个协议,意味着它承诺实现协议中的方法。运行时系统通过协议列表来检查对象是否能够响应协议中的方法。例如:
@protocol MyProtocol <NSObject>
- (void)protocolMethod;
@end

@interface MyClass : NSObject <MyProtocol>
@end

@implementation MyClass
- (void)protocolMethod {
    NSLog(@"This is a protocol method");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        if ([obj conformsToProtocol:@protocol(MyProtocol)]) {
            [(id<MyProtocol>)obj protocolMethod];
        }
    }
    return 0;
}

在这段代码中,MyClass遵循MyProtocol协议,并实现了protocolMethod。运行时通过conformsToProtocol方法检查对象是否遵循协议,并根据协议列表中的信息调用相应的方法。

对象的内存布局

  1. 实例对象的内存布局:实例对象是类的具体实例,它在内存中的布局与类的定义密切相关。实例对象的内存首先包含一个isa指针,用于指向它所属的类。紧接着是按照类中定义顺序排列的实例变量。例如,对于前面定义的Person类:
@interface Person : NSObject {
    NSString *name;
    int age;
}
@end

@implementation Person
@end

一个Person类的实例对象在内存中的布局大致如下:isa指针(通常为8字节,64位系统),然后是name指针(8字节,64位系统),接着是age变量(4字节,假设为32位整数)。实例对象的内存大小可以通过class_getInstanceSize函数获取。例如:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface Person : NSObject {
    NSString *name;
    int age;
}
@end

@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        size_t size = class_getInstanceSize([Person class]);
        NSLog(@"Instance size: %zu", size);
    }
    return 0;
}

在这个例子中,class_getInstanceSize返回的大小包括isa指针和实例变量的大小,并且会根据内存对齐的规则进行调整。

  1. 对象的内存对齐:为了提高内存访问效率,计算机系统通常要求数据在内存中的存储地址是特定值的倍数,这就是内存对齐。在Objective-C中,实例对象的内存布局也遵循内存对齐原则。例如,在64位系统中,isa指针是8字节对齐的,其他实例变量也会根据其类型的对齐要求进行对齐。假设我们有一个类:
@interface DataStruct : NSObject {
    char c;
    int i;
    double d;
}
@end

@implementation DataStruct
@end

char类型通常为1字节,int类型通常为4字节,double类型为8字节。按照内存对齐原则,DataStruct实例对象的内存布局中,c后面会填充3个字节,使其与int的4字节对齐边界匹配,i占用4字节,d占用8字节。这样整个实例对象的大小为1 + 3 + 4 + 8 = 16字节(加上isa指针8字节,总共24字节)。通过class_getInstanceSize获取的大小就是经过内存对齐后的大小。

Objective-C运行时机制中的对象生命周期管理

对象的创建

  1. alloc方法:在Objective-C中,创建对象的第一步通常是调用类的alloc方法。alloc方法负责为对象分配内存空间。当调用alloc时,运行时系统会根据类的信息计算出实例对象所需的内存大小(包括isa指针和实例变量的大小,并考虑内存对齐),然后从堆中分配一块相应大小的内存。例如:
@interface MyObject : NSObject
@end

@implementation MyObject
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [MyObject alloc];
        NSLog(@"Object allocated at %p", obj);
    }
    return 0;
}

在这个例子中,[MyObject alloc]MyObject类的实例对象分配了内存,并返回一个指向该内存地址的指针。此时,对象的实例变量处于未初始化状态。

  1. init方法alloc只是分配了内存,并没有对对象进行初始化。通常在alloc之后会调用init方法来初始化对象的实例变量。init方法是一个实例方法,它负责设置对象的初始状态。例如,对于一个Person类:
@interface Person : NSObject {
    NSString *name;
    int age;
}
- (instancetype)initWithName:(NSString *)aName age:(int)anAge;
@end

@implementation Person
- (instancetype)initWithName:(NSString *)aName age:(int)anAge {
    self = [super init];
    if (self) {
        name = [aName copy];
        age = anAge;
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] initWithName:@"John" age:30];
        NSLog(@"Name: %@, Age: %d", person->name, person->age);
    }
    return 0;
}

initWithName:age:方法中,首先调用[super init]来初始化父类部分(如果有父类),然后初始化自身的实例变量。这样,通过alloc分配内存和init初始化,一个可用的对象就创建完成了。

  1. 便捷构造方法:为了方便对象的创建,很多类提供了便捷构造方法。这些方法通常是类方法,在内部会调用allocinit方法,并返回一个已经初始化好的对象。例如,NSString类的stringWithFormat:方法:
NSString *str = [NSString stringWithFormat:@"Hello, %d", 10];

在这个例子中,stringWithFormat:方法内部调用了allocinit方法来创建并初始化一个NSString对象,返回一个格式化后的字符串。

对象的引用计数与内存管理

  1. 引用计数原理:Objective-C使用引用计数(Reference Counting)来管理对象的生命周期。每个对象都有一个引用计数,记录了当前有多少个变量引用了该对象。当对象的引用计数为0时,意味着没有任何变量指向该对象,此时运行时系统会自动释放该对象所占用的内存。例如:
MyObject *obj1 = [[MyObject alloc] init];
MyObject *obj2 = obj1;
// obj1和obj2都指向同一个对象,对象的引用计数为2

obj1 = nil;
// obj1不再指向对象,对象的引用计数减1,此时为1

obj2 = nil;
// obj2也不再指向对象,对象的引用计数减为0,对象被释放
  1. 手动引用计数(MRC):在手动引用计数环境下,开发者需要手动管理对象的引用计数。主要通过retainreleaseautorelease方法来操作。
    • retain方法:使对象的引用计数加1,表示增加对该对象的引用。例如:
MyObject *obj = [[MyObject alloc] init];
[obj retain];
// 对象的引用计数从1变为2
- `release`方法:使对象的引用计数减1,表示减少对该对象的引用。当引用计数减为0时,对象会被释放。例如:
MyObject *obj = [[MyObject alloc] init];
[obj release];
// 对象的引用计数从1变为0,对象被释放
- `autorelease`方法:将对象放入自动释放池(`NSAutoreleasePool`)中,在自动释放池被销毁时,池中的对象会收到`release`消息。例如:
@autoreleasepool {
    MyObject *obj = [[[MyObject alloc] init] autorelease];
    // 对象的引用计数为1,并且被放入自动释放池
}
// 自动释放池被销毁,对象收到release消息,引用计数减为0,对象被释放
  1. 自动引用计数(ARC):为了简化内存管理,iOS 5.0引入了自动引用计数(ARC)。在ARC环境下,编译器会自动在适当的位置插入retainreleaseautorelease代码,开发者无需手动管理对象的引用计数。例如:
MyObject *obj = [[MyObject alloc] init];
// 编译器会自动在适当位置插入内存管理代码,无需手动调用retain、release等方法

ARC大大减少了因手动内存管理不当导致的内存泄漏和悬空指针等问题,但开发者仍然需要理解对象的生命周期和内存管理的基本原理,以便正确处理对象之间的引用关系,避免循环引用等问题。例如,当两个对象相互强引用时,会导致循环引用,对象无法被释放。例如:

@interface ClassA : NSObject {
    ClassB *objectB;
}
@end

@interface ClassB : NSObject {
    ClassA *objectA;
}
@end

@implementation ClassA
@end

@implementation ClassB
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        a->objectB = b;
        b->objectA = a;
        // a和b相互强引用,形成循环引用
    }
    return 0;
}

在这种情况下,需要通过使用弱引用(__weak)或不安全的未拥有引用(__unsafe_unretained)来打破循环引用。例如,将ClassA中的objectB声明为__weak类型:

@interface ClassA : NSObject {
    __weak ClassB *objectB;
}
@end

这样,ab的引用不会增加b的引用计数,从而打破了循环引用,使对象能够在适当的时候被释放。

对象的销毁

  1. dealloc方法:当对象的引用计数变为0时,运行时系统会调用对象的dealloc方法。dealloc方法用于释放对象所占用的资源,如释放分配的内存、关闭文件句柄等。在dealloc方法中,开发者需要手动释放对象持有的其他对象的引用,因为ARC环境下,对象在dealloc方法调用之前,编译器会自动释放其强引用的对象,但对于一些特殊情况(如使用CFRetain等Core Foundation函数手动管理的对象),需要在dealloc中手动释放。例如:
@interface MyObject : NSObject {
    NSString *name;
}
@end

@implementation MyObject
- (void)dealloc {
    [name release];
    [super dealloc];
}
@end

在MRC环境下,需要手动调用[name release]来释放name对象的引用。在ARC环境下,编译器会自动处理这部分,但如果name是通过CFRetain等手动管理的,仍需手动释放。

  1. 对象销毁的过程:当对象的引用计数变为0时,运行时系统首先调用对象的dealloc方法。在dealloc方法中,对象会释放自身持有的资源。然后,运行时系统会回收对象所占用的内存空间,将其返回给堆,以便重新分配给其他对象使用。例如,对于一个包含文件句柄的对象:
#import <Foundation/Foundation.h>
#import <fcntl.h>
#import <unistd.h>

@interface FileObject : NSObject {
    int fileDescriptor;
}
- (instancetype)initWithFilePath:(NSString *)path;
@end

@implementation FileObject
- (instancetype)initWithFilePath:(NSString *)path {
    self = [super init];
    if (self) {
        const char *cPath = [path fileSystemRepresentation];
        fileDescriptor = open(cPath, O_RDONLY);
    }
    return self;
}

- (void)dealloc {
    if (fileDescriptor != -1) {
        close(fileDescriptor);
    }
    [super dealloc];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FileObject *fileObj = [[FileObject alloc] initWithFilePath:@"/etc/passwd"];
        // 使用fileObj进行文件操作
    }
    return 0;
}

在这个例子中,FileObjectinitWithFilePath:方法中打开一个文件,获取文件描述符fileDescriptor。在dealloc方法中,关闭文件描述符,释放资源。当fileObj的引用计数变为0时,运行时系统会调用dealloc方法,然后回收对象的内存空间。

综上所述,Objective-C运行时机制中的内存布局和对象生命周期管理是其核心特性。深入理解这些机制对于编写高效、稳定的Objective-C代码至关重要,无论是在手动引用计数环境下,还是在自动引用计数环境下,开发者都需要掌握内存管理的基本原则和技巧,以避免内存泄漏和其他内存相关的问题。同时,了解类和对象的内存布局有助于优化代码性能,提高内存使用效率。