NSObject 对象的内存布局

前言

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对象的内存布局,看我就够了!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容

  • 最全的iOS面试题及答案 iOS面试小贴士 ———————————————回答好下面的足够了-----------...
    zweic阅读 2,568评论 0 73
  • 面试题 一个 NSObject 对象占用多少内存? 系统分配了16个字节给NSObject对象(通过malloc_...
    叫我小黑阅读 526评论 0 0
  • Objective-C编程语言是C语言的超集,在C语言的基础上加入了面向对象的内容。OC可以和C/C++混合使用,...
    Henry_Dev阅读 5,039评论 13 31
  • 山中喜鹊筑巢 山下我拉着 你的柳条走 池塘里的鱼团锦簇 在一只猫午后的梦中
    老晁阅读 107评论 0 1
  • 道尽人间悲欢离合 演绎经典和浪漫 文/罗汉 七月九日晚,雨后的铜城,凉风习习,天降祥瑞。百年...
    红尘罗汉阅读 1,207评论 8 21