经过上一篇文章iOS的OC对象建立的alloc原理的介绍能够很清楚了对象的建立在底层的过程是怎样的了。而且只简单介绍了对象的开辟内存的空间,这篇文章将会详细介绍一下对象的内存对齐。算法
为了方便对下面的内容介绍,用TestObject做为例子,示例代码以下:express
@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
复制代码
查看对象的内存状况能够在lldb中用x
、p
和po
这些指令来查看数组
0x101a5e050
是对象的指针的首地址,每一行的开头部分都是这一行的内存值的开始的排列。而
0x0000001300006261
这部分的是内存的值,这些内存的值都是从对象的首地址来依次的排列的。
p,是expression - 的别名,p为print的简写,同时能够写为pri,打印某个东西,能够i是变量和表达式;call为调用某个方法,输出变量也是能够的。 po通常用于打印对象,是expression -O — 的别名。 p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名。 bash
从中示例能够看到第一个打印的是一串数字,po出来是有问题,其余的均可以正常地从内存地址打印出来的,这时咱们能够换一种方式打印post
0x0000001300006261
拆分出来打印就能够获得了,其中
97
和
98
分别是小写字母a和b的ASCII编码。为何会这样呢?这里面就涉及到了对象的内存优化了,在上一篇文章中,有介绍到内存是以8字节来分配的。其中TestObject中的
age
是
int
占4字节,
char1
和
char2
是
char
分别占1个字节,若是都按8字节来分的话就会形成很大的浪费。至于为何会这样的,就由下面的
内存对齐来介绍了。
先说一下内存对齐的原则:优化
1.数据成员对齐规则:结构体(struct)或者联合体(union)的数据成员,第一个数据成员放在offset为0的地方,之后每一个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,好比说是数组,结构体等)的整数倍开始(好比int为4字节,则要从4的整数倍地址开始存储)。ui
2.结构体做为成员:若是一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储编码
3.收尾工做:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。atom
经过上面的说明是否是以为不是很明白?下面就举个例子来解释一下spa
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。
可是相对于结构体的内存对齐是按照属性排下来,对象的内存对齐却不是的,由于作了内存由编译器作了优化(由第一部分的内容能够看到)。
仍是经过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字节对齐的参考的是整个对象,由于系统开辟的内存若是只是按照对象属性的大小来的话,可能会致使内存溢出的。
定义一个类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());
}
复制代码
至此OC对象的内存对齐的介绍就到这里了,后续还会陆续出一些其余的底层知识,欢迎关注。若是以为内容有出错的,欢迎评论留言。