iOS的OC对象的内存对齐

3,912 阅读8分钟

前言

通过上一篇文章iOS的OC对象创建的alloc原理的介绍可以很清楚了对象的创建在底层的过程是怎样的了。并且只简单介绍了对象的开辟内存的空间,这篇文章将会详细介绍一下对象的内存对齐

为了方便对下面的内容介绍,用TestObject作为例子,示例代码如下:

@interface TestObject : NSObject

@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) long height;
@property(nonatomic,copy) NSString *hobby;

@property(nonatomic,assign) int sex;
@property(nonatomic,assign) char char1;
@property(nonatomic,assign) char char2;

@end

1.对象的内存情况

查看对象的内存情况可以在lldb中用xppo这些指令来查看

从中可以看到的0x101a5e050是对象的指针的首地址,每一行的开头部分都是这一行的内存值的开始的排列。而0x0000001300006261这部分的是内存的值,这些内存的值都是从对象的首地址来依次的排列的。

p,是expression - 的别名,p为print的简写,同时可以写为pri,打印某个东西,可以i是变量和表达式;call为调用某个方法,输出变量也是可以的。 po一般用于打印对象,是expression -O — 的别名。 p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名。

从中示例可以看到第一个打印的是一串数字,po出来是有问题,其他的都可以正常地从内存地址打印出来的,这时我们可以换一种方式打印

0x0000001300006261拆分出来打印就可以得到了,其中9798分别是小写字母a和b的ASCII编码。为什么会这样呢?这里面就涉及到了对象的内存优化了,在上一篇文章中,有介绍到内存是以8字节来分配的。其中TestObject中的ageint占4字节,char1char2char分别占1个字节,如果都按8字节来分的话就会造成很大的浪费。至于为什么会这样的,就由下面的内存对齐来介绍了。

2 内存对齐

先说一下内存对齐的原则:

1.数据成员对齐规则:结构体(struct)或者联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。

2.结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储

3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

通过上面的说明是不是觉得不是很明白?下面就举个例子来解释一下

struct JSStruct1{
    char a;
    double b;
    int c;
    short d;
}JSStruct1;

struct JSStruct2{
    double b;
    char a;
    short d;
    int c;
}JSStruct2;

2020-05-02 22:23:48.415188+0800 LGTest[9394:360173] 24====16

下面就是根据这个例子来进行解释。首先需要遵循一个min算法,假设min(m,n),其中m为当前开始的位置,n为大小。根据内存对齐的原则,m是n的整数倍的情况下才开始排,如果不是那么就m的值往上加直到是n的整数倍才可以排。例如上面的例子:JSStruct1中的char a的大小是1,开始的位置是0,那么min(0,1),是可以排的;b的大小是8,开始的位置是1,min(1,8),所以b的m需要到8才可以排,那么b的是需要从8开始排的,那么排b是8,9,10,11,12,13,14,15,即到15将b排完;排完b之后到了c,此时c的开始位置应该为16,大小为4,min(16,4),因为16是4的整数倍,那么c的排位是16,17,18,19,排完c之后的位置是到了19;那么d的开始位置为20,大小为2,min(20,2),因为20是2的整数倍,那么排位d的是20,21,排完d的位置是21。因为整体的内存对齐是8字节对齐的。需要8的倍数所以最终是24,这就是JSStruct1整体的内存大小,那么你可以对JSStruct2来根据上面的解释进行练习一下为什么是16。

但是相对于结构体的内存对齐是按照属性排下来,对象的内存对齐却不是的,因为做了内存由编译器做了优化(由第一部分的内容可以看到)。

3.对象生成的内存与系统开辟的内存关系

还是通过TestObject的例子,然后将两个char类型的char1和char2和一个int类型的sex不实现并且TestObject类中注释掉

        TestObject *test = [TestObject alloc];
        test.name = @"jason";
        test.age = 19;
        test.hobby = @"足球";
        test.height = 180;
//        test.sex = 1;
//        test.char1 = 'a';
//        test.char2 = 'b';
        
NSLog(@"对象生成的内存:%lu,系统开辟的内存:%lu",class_getInstanceSize([test class]),malloc_size((__bridge const void *)(test)));

2020-05-02 22:39:10.191590+0800 LGTest[9577:368352] 对象生成的内存:40,系统开辟的内存:48

由上面的结果知道,为什么对象生成的内存和系统开辟的内存是不一样的呢? 为了搞清楚还是需要用到上一篇文章iOS的OC对象创建的alloc原理里面源码的_class_createInstanceFromZone方法的源码

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    //判断当前class或者superclass是否有.cxx_construct构造方法的实现
    bool hasCxxCtor = cls->hasCxxCtor();
    //判断当前class或者superclass是否有.cxx——destruct析构方法的实现
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    //通过进行内存对齐得到实例大小
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        //初始化实例的isa指针
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

通过断点可以看到

size_t size = cls->instanceSize(extraBytes);

返回的对象内存大小是40,往下走到了calloc方法,但是直接进去是进不去的因为这是libmalloc的源码也是可以直接到苹果的开源库下载源码。因为objc的源码和malloc的源码是分开的,直接按下面的方式来直接进去。

通过源码的一步步跳转到了以下这种情况下,看到ptr = zone->calloc(zone, num_items, size);如果是一直这样点击源码跳转下去就变成了死循环,肯定是有问题的。

为了解决这个问题可以用po命令来

然后再一步一步来搜索出来,再用po命令和断点相结合就可以层层深入进去源码

最终找到在计算开辟系统内存大小的源码的方法在

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 = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	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;
}

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)

从源码中看到,传进来的size是40,SHIFT_NANO_QUANTUM是4,NANO_REGIME_QUANTA_SIZE就是16,那么这里就是16字节对齐,因为传进来的是40,为了能够16字节对齐需要补齐所以得到的就是48。从上面的对象的字节对齐是8字节,为什么系统开辟的内存是16字节呢?因为8字节对齐的参考的是对象里面的属性,而16字节对齐的参考的是整个对象,因为系统开辟的内存如果只是按照对象属性的大小来的话,可能会导致内存溢出的。

4.问题

定义一个类TestJason,里面什么属性都没有,根据上面介绍的内存对齐,得到的对象内存和系统的内存分别是多少呢?

TestJason *test2 = [TestJason alloc];
NSLog(@"%lu===%lu",class_getInstanceSize([test2 class]),malloc_size((__bridge const void *)(test2)));

答案是:8和16

这是为什么呢?有的人会认为是16和16这个答案,因为上一篇文章介绍alloc原理的时候有说过最少分配是16字节。但是通过看class_getInstanceSize的源码,知道里面其实8字节对齐就直接返回了。

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

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
  assert(isRealized());
  return data()->ro->instanceSize;
}

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

5.最后

至此OC对象的内存对齐的介绍就到这里了,后续还会陆续出一些其他的底层知识,欢迎关注。如果觉得内容有出错的,欢迎评论留言。