NSObject对象的本质

2,113 阅读4分钟

NSObject的内存本质

NSObject的定义

在Xcode中进入查看NSObject的定义如下

@interface NSObject  {
    Class isa;
}

NSObject的底层实现

​ 在终端进入对应文件夹目录下使用以下代码可以生成对应文件的c++代码(以main.m为例)

  • xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
  • 如果需要链接其他框架,使用-framework参数。比如-framework UIKit

在main.cpp文件中可以找到以下结构体

struct NSObject_IMPL {
	Class isa;
};

​ 这个结构体就是NSObject的底层实现;由此可知一个NSObject对象本质上就只有一个isa指针,而指针在arm64架构上是占8个字节的。所以NSObject类的实例大小是8个字节。

​ OC的runtime函数同样提供了一个class_getInstanceSize()函数(该函数返回的是内存对齐后的大小)去获取对应实例的大小,但是这个并不是实例所分配到的内存大小。要想获取实例分配到的内存大小需要使用malloc_size()(注意传的参数需要进行桥接)函数。

​ 代码验证如下:

NSObject *objc = [[NSObject alloc] init];
// 实例对象实际需要的内存
NSLog(@"---%zd---",class_getInstanceSize([objc class]));
// 获得objc所指向的内存大小(实际分配内存)
NSLog(@"---%zd---",malloc_size((__bridge const void *)(objc)));

输出结果分别为8和16。(如果想要查看这两个函数的具体实现可以去opensource.apple.com/tarballs/ob…上下载对应的源码查看)

分配内存一般都是调用alloc函数,在OC中实际上是调用了allocWithZone函数,在源码NSObject.mm文件中找到函数的实现:

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

继续在objc-runtime--new.mm文件中找到_objc_rootAllocWithZone函数的实现如下:

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

继续找_class_createInstancesFromZone函数的实现如下:

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    //size是分配内存的大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

在objc-runtime--new.h文件中找到cls的instanceSize方法:

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

由上可知当实例对象所需的内存至少是16个字节

一个NSObject对象只有一个指针所以它占16个字节

通过LLDB指令窥探NSObject内存

常用的LLDB指令如下:

  • print、p:打印
  • po:打印对象
读取内存
  • memory read/数量格式字节数 内存地址
  • x/数量格式字节数 内存地址
  • x/3xw 0x10010(表示打印三串数据每串数据都是16进制占4个字节)
格式

x是16进制,f是浮点,d是10进制

字节大小

b:byte 1字节,h:half word 2字节

w:word 4字节,g:giant word 8字节

修改内存的中的值
  • memory write 内存地址 数值

  • memory write 0x000010 10

image-20200324203132534

由图可知一个NSObject对象它占16个字节

Student的本质

将如下代码转换为c++代码后

@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}

@end

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

​ 找到Student的定义如下:

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _no;
	int _age;
};

之前已经知道一个 NSObject_IMPL 结构体内部只有一个isa指针(arm64下占8个字节),两个int占8个字节,加起来刚好是16个字节。

运行以下代码:

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

struct NSObject_IMPL {
    Class isa;
};

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};


@interface Student : NSObject
{
    @public
    int _no;
    int _age;
}

@end

@implementation Student


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu->_no = 4;
        stu->_age = 5;
        
        struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
        NSLog(@"no is %d,age is %d",stuImpl->_no,stuImpl->_age);
        
        NSLog(@"%zd",class_getInstanceSize([Student class]));
        NSLog(@"%zd",malloc_size((__bridge const void *)stu));
    }
    return 0;
}

在stuImpl处下断点,找到内存地址后使用Xcode自带的View Memory功能输入地址后

graph LR
Debug --> B[Debug Workflow]--> C[View Memory]
image-20200325105249799

可以找到 _no 和 _age 的值如上图。输入LLDB指令后重新查看内存:

image-20200325105318577

放开断点后打印如下:

image-20200325105445825

实际上Student对象的内存分配就类似下图:

image-20200325000044574

更复杂的继承结构

如下代码运行后:

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


//struct NSObject_IMPL {
//    Class isa;
//};
//
//struct Person_IMPL {
//    struct NSObject_IMPL NSObject_IVARS;
//    int _age;
//};
//
//
//struct Student_IMPL {
//    struct Person_IMPL Person_IVARS;
//    int _no;
//};


@interface Person : NSObject
{
    int _age;
}

@end

@implementation Person


@end

@interface Student : Person
{
    int _no;
}

@end

@implementation Student


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Student *stu = [[Student alloc] init];
        NSLog(@"stu - %zd",class_getInstanceSize([Student class]));
        NSLog(@"stu - %zd",malloc_size((__bridge const void *)stu));
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd",class_getInstanceSize([Person class]));
        NSLog(@"person - %zd",malloc_size((__bridge const void *)person));
        
    }
    return 0;
}

输出结果是:

image-20200325132028356

说明当分配给Person的内存大小能够装的下Student的成员变量时会使用Person的内存。

给Student增加一个NSInteger修饰的_score后打印结果变为:

image-20200325202452898

将 _score 放在 _no后运行后打印结果为:

image-20200325203928852

这是因为内存对齐:结构体的大小必须是最大成员变量的倍数(此时Student结构体中最大的成员变量是Person_IMPL占16字节),所以stu对齐后返回的内存大小是32。