NSObject 对象的内存布局

2020-04-25  本文已影响0人  6ffd6634d577

前言

Objective-C语言是一门高级语言,底层是由C/C++语言实现。要想从本质上了解Objective-C对象的底层数据结构和内存布局,就需要一步步揭开那最神秘的面纱。

Objective-C对象经过编译链接运行后,所经历的过程如下所示:


image.png

在后面的讲解中,主要将Objective-C对象一步步转为最底层的实现。

将Objective-C语言转换为C/C++语言

在终端执行下面的命令,可以将Objective-C对象转换成C/C++语言:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

举例说明:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

执行上述命令后,得到的结果如下:

 struct NSObject_IMPL {
     Class isa;
 };
 
 int main(int argc, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
 
         NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
     }
    return 0;
}

通过上述编译后的代码可以看出,NSObject对象的底层数据结构是结构体。

struct NSObject_IMPL {
    Class isa;
};

如何获取NSObject对象的内存大小?

获取NSObject对象的内存大小,需要用到以下几个函数:

  • class_getInstanceSize
  • malloc_size
  • sizeOf
    NSObject *obj = [[NSObject alloc] init];
    
    NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
    NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
    NSLog(@"sizeOf = %zd", sizeof(obj));

控制台打印如下:

class_getInstanceSize = 8
malloc_size = 16
sizeOf = 8

获取结果居然不一样,那是为什么呢?那就继续探究一下源码实现吧!

1、class_getInstanceSize

这个是一个runtime提供的API,用于获取类实例对象所占用的内存大小,返回所占用的字节数。

在苹果开源网站,找到对应的objc4-779.1.zip压缩包。看一下源码实现,在objc-class.mm文件到找到了该方法的实现,如下所示:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

Class's ivar size rounded up to a pointer-size boundary

翻译一下,返回实例对象中成员变量内存大小。说白了,class_getInstanceSize就是获取实例对象中成员变量内存大小。

仔细想一下,实例对象在创建的时候,系统应该就会分配对应的内存空间,那在对象初始化的过程中,是否有对应的内存分配呢?

2、alloc

我们都知道初始化一个OC对象是有两个步骤:

  1. 给对象分配一个内存空间
  2. 初始化该对象

当我们 alloc 的时候系统会分配内存空间(地址)给OC对象,当 init 的时候实现了对象的初始化工作。就完成了一个对象的创建过程。

当执行alloc的时候,系统会自动调用分配内存地址的方法:

对象的创建离不开alloc方法,对象创建的过程中可能存在分配内存空间的方法,一起看下源码。

NSObject.mm类中找到alloc以及allocFromZone方法的实现:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

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

找到时机调用的核心方法是:_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_createInstanceFromZone

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 = 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);
}

在调用calloc或者malloc_zone_calloc函数是需要传入size参数,可以发现size变量来源于下面的代码:

    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;
    }

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

CF requires all objects be at least 16 bytes.

CoreFoundation 框架要求所有对象至少分配16个字节。

当实例对象不足16个字节,系统分配给16个字节,属于系统的硬性规定。

仔细看,会发现alignedInstanceSize函数不就是class_getInstanceSize函数的内部实现。

3、malloc_size

这个函数主要获取 系统实际分配的内存大小,具体的底层实现也可以在源码libmalloc找到

4、sizeOf

值得注意的一点是,sizeof是操作符,不是函数,它的作用对象是数据类型,主要作用于编译时。

因此,它作用于变量时,也是对 其类型 进行操作。得到的结果是该数据 类型 占用空间大小,即size_t类型。

5、应用

通过上面的学习,我们可以很好回答下面的这个经典的问题了:

一个NSObject对象占用多少内存?

在64位架构下, 系统分配了16个字节给NSObject对象(通过malloc_size函数获得);
但NSObject对象内部只使用了8个字节的空间(可以通过class_getInstanceSize函数获得)。

内存对齐

1、内存对齐是什么?

内存对齐 是一种在计算机内存中 排列数据(表现为变量的地址)、访问数据(表现为CPU读取数据)的一种方式。

它包含了两种相互独立又相互关联的部分:基本数据对齐结构体数据对齐

在iOS开发过程中,编译器会自动的进行字节对齐的处理,并且在64位架构下,是以8字节进行内存对齐的。

2、内存对齐的原则

内存对齐应该是编译器的管辖范围,编译器为程序中的每个数据单元安排在适当的位置上,方便计算机快速高效的进行读取数据。

每个平台的编译器都有自己的对齐系数和相应的对齐规则。在iOS中的64位架构下,对齐系数就是8个字节。

注意: 内存对齐有实际占用的内存对齐,也有系统分配内存对齐,iOS中的64位架构下,系统分配对齐系数是16个字节,比如一个 NSObject 对象实际占用8个字节,但是系统分配16个字节

例如:代码申请4个字节的空间,但是因为内存对齐,系统实际分配了16个字节

    void *p = malloc(4);
    NSLog(@"%zd", malloc_size(p));

    [7436:1197123] 16

2.1 数据成员对齐

结构体或者共用体中的成员变量中,首个成员变量放在偏移量为0的位置上,后面的成员变量的对齐偏移量是取指定对齐系数和本身该成员变量所占用大小中的较小值,即 min(对齐系数,成员变量的内存大小 )

2.2 数据整体对齐

在结构体或者共用体中的成员变量完成自身的对齐之后,整个结构体或者共用体也需要进行字节对齐处理,一般为 min(对齐系数,最大成员变量的内存大小 )的整数倍。

结合上述原则1、2,可以推断出下面的常用原则,以结构体为例:

  • 结构体变量的首地址是其最长基本类型成员的整数倍;
  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;
  • 结构体的总大小为结构体最大基本类型成员变量大小的整数倍;
  • 结构体中的成员变量都是分配在连续的内存空间中。

在熟悉上述对齐原则基础上,默认在64位架构下,举个例子:

struct object {
    int a; // 4
    NSString *b; // 8
    int c; // 4
    char d; // 1
};

结构体中最大的成员变量占用8个字节,根据上面的对齐原则,最终获得的对齐系数是min(最大成员变量大小8个字节, 对齐系数8个字节) = 8。

不考虑内存对齐的情况下,实际占用4 + 8 + 4 + 1 = 17个字节,考虑字节对齐的情况下,分配24个字节。

例2:

struct object {
    int a; // 4
    char b; // 1
    int c; // 4
};

根据上面结构体,可以得出需要对齐的字节数为min(对齐系数, 最大成员变量的内存大小) = 4个字节。对齐后的内存分配表如下所示:


image.png

3、内存对齐的原因

为了减少CPU访问内存的次数,提高计算机性能,一些计算机硬件平台要求存储在内存中的变量按自然边界对齐。

3.1 性能上的提升
3.2 跨平台

4、内存对齐的注意事项

4.1 内存分配

在结构体中,声明成员变量的顺序不一致,也会导致最终分配内存大小的不同。

struct object {
    int a; // 4
    NSString *b; // 8
    int c; // 4
};

对齐情况下,系统分配24个字节,具体分配如下:


image.png

调换一下成员变量的声明顺序:

struct object {
    int a; // 4
    int c; // 4
    NSString *b; // 8
};

这种情况下,系统分配16个字节,具体分配如下:


image.png

通过上面对比可以看出,在日常开发中,设计结构的时候,合理调换成员变量的顺序,可以很好地节省内存空间。

OC对象的内存布局

情景一:带有一个成员变量的对象占用内存的大小

@interface Animal : NSObject
{
    @public
    int _age;
}
@end

    Animal *animal = [[Animal alloc] init];
    NSLog(@"Animal -- class_getInstanceSize = %zd", class_getInstanceSize([animal class]));
    NSLog(@"Animal -- malloc_size = %zd", malloc_size((__bridge const void *)(animal)));
    NSLog(@"Animal -- sizeOf = %zd", sizeof(animal));

2020-04-25 13:34:44.353148+0800 ClangDemo[7571:1234217] Animal -- class_getInstanceSize = 16
2020-04-25 13:34:44.353241+0800 ClangDemo[7571:1234217] Animal -- malloc_size = 16
2020-04-25 13:34:44.353264+0800 ClangDemo[7571:1234217] Animal -- sizeOf = 8

情景二:不同成员变量的对象占用内存的大小
在情景一的基础上,在Animal对象再添加一个成员变量_weight,如下所示:

@interface Animal : NSObject
{
    @public
    int _age;
    int _weight;
}
@end

2020-04-25 13:42:41.791946+0800 ClangDemo[7575:1236418] Animal -- class_getInstanceSize = 16
2020-04-25 13:42:41.792034+0800 ClangDemo[7575:1236418] Animal -- malloc_size = 16
2020-04-25 13:42:41.792058+0800 ClangDemo[7575:1236418] Animal -- sizeOf = 8

情景三:继续添加不同类型的成员变量
添加整型成员变量

@interface Animal : NSObject
{
    @public
    int _age;
    int _weight;
    int _height;
}
@end

2020-04-25 13:44:27.894359+0800 ClangDemo[7577:1237260] Animal -- class_getInstanceSize = 24
2020-04-25 13:44:27.894433+0800 ClangDemo[7577:1237260] Animal -- malloc_size = 32
2020-04-25 13:44:27.894453+0800 ClangDemo[7577:1237260] Animal -- sizeOf = 8

添加字符串型成员变量

NSString *_name;

2020-04-25 13:46:12.214979+0800 ClangDemo[7581:1238086] Animal -- class_getInstanceSize = 32
2020-04-25 13:46:12.215062+0800 ClangDemo[7581:1238086] Animal -- malloc_size = 32
2020-04-25 13:46:12.215085+0800 ClangDemo[7581:1238086] Animal -- sizeOf = 8

情景四:调换成员变量声明顺序
情况一:整型变量中掺杂字符串变量

    int _age;
     NSString *_name;
     int _weight;
     NSString *_nick;
     int _height;

2020-04-25 13:47:56.280581+0800 ClangDemo[7584:1238907] Animal -- class_getInstanceSize = 48
2020-04-25 13:47:56.280654+0800 ClangDemo[7584:1238907] Animal -- malloc_size = 48
2020-04-25 13:47:56.280675+0800 ClangDemo[7584:1238907] Animal -- sizeOf = 8

情况二:调换一下声明成员变量的顺序

    int _age;
     int _weight;
     int _height;
     NSString *_name;
     NSString *_nick;

2020-04-25 13:49:21.888761+0800 ClangDemo[7588:1239645] Animal -- class_getInstanceSize = 40
2020-04-25 13:49:21.888838+0800 ClangDemo[7588:1239645] Animal -- malloc_size = 48
2020-04-25 13:49:21.888860+0800 ClangDemo[7588:1239645] Animal -- sizeOf = 8

情景五:继承体系下的内存分配

@interface Animal : NSObject
{
    @public
    int _age;
     int _weight;
}
@end

@interface Dog : Animal
{
    @public
    int _height;
}

2020-04-25 13:53:47.886732+0800 ClangDemo[7592:1240968] Dog -- class_getInstanceSize = 24
2020-04-25 13:53:47.886810+0800 ClangDemo[7592:1240968] Dog -- malloc_size = 32
2020-04-25 13:53:47.886831+0800 ClangDemo[7592:1240968] Dog -- sizeOf = 8

参考文章

关于NSObject对象的内存布局,看我就够了!

上一篇下一篇

猜你喜欢

热点阅读