OC对象内存占用及优化

结构体内存对齐原理

前言:我们都知道,在iOS开发中,我们写的oc代码,底层都是用c++来实现的,而oc对象本质就是结构体指针,那么结构体占用内存的计算方法是什么呢,有没有什么规则呢,下面我们就来研究一下。

首先,我们看下面两个结构体,并且打印两个结构体占用的内存大小,看看结果如何。

struct Struct1 {
    double a;   // 8字节
    char b;     // 1字节
    int c;      // 4字节
    short d;    // 2字节
} struct1;

struct Struct2 {
    double a;   // 8字节
    int b;      // 4字节
    char c;     // 1字节
    short d;    // 2字节
} struct2;
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSLog(@"struct1 size : %lu \n struct2 size : %lu", sizeof(struct1), sizeof(struct2));
}

我们看到,两个结构体成员类型都是一样的,只是顺序不一样,他们占用内存是不是相同呢?看结果:


image

这结果真是让我们大吃一斤!那为什么顺序不同结果就不一样呢?我们看一下结构体内存对齐原则:

  1. 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
    一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要
    从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,
    结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存
    储。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
    其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b
    里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  3. 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬。

看完了对齐原理,我们来验证下为什么刚才的结果是不一样的。

struct Struct1 {
    double a;   // 8字节  [0...7]
    char b;     // 1字节  [8]
    int c;      // 4字节  (9,10,11,[12...15]
    short d;    // 2字节  [16,17]
} struct1;  // 8字节内存对齐   18 -> 24

struct Struct2 {
    double a;   // 8字节  [0...7]
    int b;      // 4字节  [8...11]
    char c;     // 1字节  [12]
    short d;    // 2字节  (13,[14,15]
} struct2;  // 8字节内存对齐  16 -> 16

按照刚才的原理,我们看到确实是这样。接下来我们加大难度:

struct Struct3 {
    double a;   // 8字节  [0...7]
    int b;      // 4字节  [8...11]
    char c;     // 1字节  [12]
    short d;    // 2字节  (13,[14,15]
    int e;      // 4字节  [16...19]
    struct Struct1 s1;  // 24字节  (20,21,22,23,[24...47]
}struct3; // 8字节内存对齐  48 -> 48

如果有结构体嵌套,根据上面的规则,我们计算struct3内存大小应该是48字节,我们打印下验证结果:

image

我们再看一种情况

struct Struct4 {
    char a;     // 1字节  [0]
    short b;    // 2字节  [2,3]
    double c;   // 8字节  [8...15]
    int d;      // 4字节  [16...19]
} struct4;  // 8字节内存对齐   20 -> 24

struct Struct5 {
    int a;              // 4字节  [0...3]
    int b;              // 4字节  [4...7]
    struct Struct4 s4;  // 24字节 [8...31]
    short c;            // 1字节  [32]
}struct5; // 8字节内存对齐  33 -> 40

struct Struct6 {
    int a1;             // 4字节  [0...3]
    int b1;             // 4字节  [4...7]
    char a;             // 1字节  [8]
    short b;            // 2字节  [10,11]
    double c;           // 8字节  [16...23]
    int d;              // 4字节  [24...27]
    short e;            // 1字节  [28]
}struct6; // 8字节内存对齐  28 -> 32
image

C++结构体是可以继承的,那么struct5struct6却不一样,因为在继承的时候,可以理解成把父结构体这个小组织继承过来,他里面的内存分配形式不变,就算里面有多余的没有用到的内存,子结构体也没有权限去往里面写数据,所以他们的内存占用不同。

既然结构体继承是这样的,那么我们试一下OC中的类呢。

image

OC中类本质就是结构体指针,那SJFather占16字节(isa->8字节,a->1字节,16字节对齐),SJSon继承SJFather,如果按上面结构体情况,是不是先把SJFather的16字节继承过来且没权限修改,再加上一个b->1字节,16字节对齐后占32字节。但是我们看到打印出来16字节,也就是在底层,SJSon直接把SJFather成员变量放在自己的结构体中,并没有结构体嵌套,所以SJSon占用的内存:isa->8 + a->1 + b->1 = 10,16字节对齐后16字节,这里需要注意下。

OC对象内存大小

下面我们来研究下对象的内存大小。

@interface SJPerson : NSObject

@property (nonatomic, copy) NSString *name;  // 8
@property (nonatomic, copy) NSString *nickName;  // 8
@property (nonatomic, assign) int age;  // 4
@property (nonatomic, assign) long height;  // 8

@end
SJPerson *sj = [[SJPerson alloc] init];    
NSLog(@"%@ - %lu - %lu - %lu", sj, sizeof(sj), class_getInstanceSize([SJPerson class]), malloc_size((__bridge const void *)(sj)));
image

指针8字节,根据上面结构体内存,我们可算出成员变量内存对齐后占用28 -> 32字节,加上isa指针8字节,共40字节,SJPerson这个类占用40字节就够了,为什么malloc_size打印出来是48呢,我们研究下。
找到malloc源码,看下calloc流程有哪些。

  1. _malloc_zone_calloc
void *
calloc(size_t num_items, size_t size)
{
    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
  1. 我们根据返回值ptr,找到关键信息zone->calloc
    _malloc_zone_calloc

但是我们点calloc进去

void    *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */

什么信息都看不到,我们看源码calloc有好多calloc = xxx赋值的地方,有赋值的地方就有存储值的地方。
我们可以在zone->calloc打个断点,当执行到这行代码时,在控制台po zone->calloc,就会发现输出default_zone_calloc,我们在全局搜索。
或者用汇编,也可以看到走到default_zone_calloc方法。

  1. default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}

返回值同样看不到任何信息,我们打断点故技重施,会输出nano_calloc

  1. nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;
        /// 返回null不用看,我们肯定要找成功返回
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
                /// 重要信息
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
        /// 当total_bytes大于256,执行下面代码,需要再验证下
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}
  1. _nano_malloc_check_clear
    _nano_malloc_check_clear

    找到最关键的代码,segregated_next_block就是死循环查找合适内存空间。
  2. segregated_next_block


    segregated_next_block

总结下calloc流程图如下:


calloc流程.jpg

calloc流程基本走完了,但是我们最关心的问题,申请空间申请多大呢?我们再回到第5步中,slot_bytes这个字段即开辟内存空间大小。再网上看这个值咋么获取的

size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        /// size = 16
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    /// (size + 15) >> 4 << 4,即k为大于size的最小的16字节对齐数据
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

至此,也就是能解释为什么class_getInstanceSize是40的时候,malloc_size是48了,因为要16字节对齐。那为什么要以16字节对齐呢,oc中成员变量最多的占8字节,或者说为什么不以32字节对齐或者其他。因为如果以8字节对齐,不同对象内存空间是连续挨在一起的,访问时有可能会发生错误,也就是野指针访问,如果扩大到16,内存连续的可能性会降低,一个NSObject对象只有一个isa指针,占8字节,空8字节,发生访问错误的几率就会降低,而且随便加一个成员变量,内存就会大于8,如果以8字节对齐,计算量会变大。为什么不用更大32呢?32的话可能会浪费很多内存,所以综上考虑,iOS对象内存空间用16字节对齐。

OC对象内存优化

SJPerson

打印看下sj的内存分配

内存分配

可以看出系统自动帮我们做了内存分配优化,而且我们写的属性的顺序与内存位置顺序无关。

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

推荐阅读更多精彩内容