从【class_getInstanceSize方法】探究iOS的内存分配策略

先抛问题

各位看官请看下面两种case的输出分别是多少:

case1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface LLYModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
LLYModel *objc = [LLYModel alloc];
objc.name = @"lly";
objc.nickName = @"123";
objc.age = 18;
objc.height = 180;
NSLog(@"%@ - %lu - %lu - %lu",objc,sizeof(objc),class_getInstanceSize([LLYModel class]),malloc_size((__bridge const void *)(objc)));
case2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface LLYModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) long weight;
@end
LLYModel *objc = [LLYModel alloc];
objc.name = @"lly";
objc.nickName = @"123";
objc.age = 18;
objc.height = 180;
objc.weight = 180;
NSLog(@"%@ - %lu - %lu - %lu",objc,sizeof(objc),class_getInstanceSize([LLYModel class]),malloc_size((__bridge const void *)(objc)));

可以先分析一下打印情况,我这里直接上结果:

1
2
case1:<LLYModel: 0x1006cf490> - 8 - 40 - 48
case2:<LLYModel: 0x1006cf490> - 8 - 48 - 48

这就有点意思了,class_getInstanceSize获取到的大小并不完全和对象的真实大小完全一致,通过对相关源码的探究,最终发现了原因,这里先上结论:

class_getInstanceSize获取的大小是根据8字节对齐计算,而内存的实际分配策略是根据16字节对齐计算

源码分析

我们首先来看看runtime中class_getInstanceSize的实现,通过调用栈的追踪,最终定位到下面几个比较重要的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Class's ivar size rounded up to a pointer-size boundary.
// 这里返回类的实例变量的大小 因为只有实例变量是存放在类对象中的。
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// May be unaligned depending on class's ivars.
// 未对齐的实例变量的大小
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
// 64位下8字节对齐 32位下4字节对齐
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}

通过以上几个函数的调用,就可以验证上面的结论class_getInstanceSize是以8字节对齐的
在上面的函数中,我们还发现了一个很熟悉的身影,就是下面这个结构:

1
class_ro_t *ro()

我们知道,这里面的数据都是只读数据,是在程序编译期就决定了的,我们常用的实例变量就存放在这个结构体中,这也是为什么类在初始化后不能再动态添加实例变量的原因。

然后我们再来看看类的创建过程,还是从源码入手,不过内存创建的函数调用栈比较复杂,我画个简单的流程图来表示下先:

在runtime库中,这个调用堆栈跟到最后两个标黄部分就进不去了,不过我们还是通过一些细节找到了这两个函数的出处,并最终定位到了负责内存分配过程中size计算的函数,该部分代码位于libsystem_malloc库中,具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
//最小16字节
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//先加上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;
}
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
#define NANO_QUANTA_MASK (NANO_REGIME_QUANTA_SIZE - 1)
#define NANO_SIZE_CLASSES (NANO_MAX_SIZE/NANO_REGIME_QUANTA_SIZE)

通过以上源码也证明了上面的结论内存的实际分配策略是根据16字节对齐计算

补充部分

因为iOS中的类本质上都是结构体,所以他们的内存大小应该也需要满足c语言中结构体对齐的原则,这里简单介绍下c语言中如何进行字节对齐:

内部对齐

即结构体内部变量的起始地址需要是变量自身大小的倍数。具体对齐逻辑参考下图:

外部对齐

即结构体的整体大小需要和当前运行环境的字对齐。

这里还有一个知识点需要提及,为了更高效的利用内存,xcode编译器会对实例变量进行重排序,这个在我上面的LLYModel中并未体现,感兴趣的同学可以自行实验

小结

以上就是这次对iOS类分配内存大小探索的全部内容,首先是发现问题,然后通过源码的分析找到问题产生的原因,中间的探索过程一度中断,不过最后还是通过一些小的线索定位到问题的所在,在源码分析过程中,我们应该做到抓住关键问题,忽略干扰条件,多一些细心和耐心。