iOS 通过源码看看alloc以及内存分配
首先从源码(基于779.1)调试追踪一下alloc
的流程。
大致如上图所示
接下来从一道经典面试题开个头
一个NSObject对象占用多少内存
答:系统分配16字节,实际利用8字节
- NSObject 只有一个成员变量
isa
指针 ,arm64
架构后 可以追寻到一个isa_t
类型的联合体(union)结构
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
绝大多数情况下,苹果采用了优化的isa
策略,即isa_t
类型不是class
而是struct
根据它的位域来看 占用64位,即8字节。各位的含义这里就先不说了
在64位里指针占8字节,看起来一个
NSObject
对象 8字节 就行了,而不是16字节,这就需要继续探索了。
回到上面的alloc
流程图
我们主要来看看·cls->instanceSize
(计算开辟内存大小) 和(id)calloc(1, size)
(申请开辟内存,返回地址指针)
先看看cls->instanceSize
相关的源码
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
///都走快速计算 ,在runtime入口函数里,`map_images`内的`realizeClassWithoutSwift` 的`setInstanceSize` 做cache的setFastInstanceSize
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;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
///__builtin_constant_p(x) : 如果x的值在编译时能确定,那么该函数返回值为1.
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
/// 即得到`setFastInstanceSize`里 (word_align(newSize) + FAST_CACHE_ALLOC_DELTA16)
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
/// 减去 FAST_CACHE_ALLOC_DELTA16 得到 word_align(newSize),再进行16字节对齐
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
///16字节对齐的算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
//uint16_t a = ((_flags & ~FAST_CACHE_ALLOC_MASK)|(((word_align(newSize) + FAST_CACHE_ALLOC_DELTA16))&FAST_CACHE_ALLOC_MASK))&FAST_CACHE_ALLOC_MASK;
_flags = newBits;
}
通过debug
发现 ,instanceSize
只会走fastInstanceSize, 而这个我看了一下,在runtime入口函数里,map_images
内的realizeClassWithoutSwift
的setInstanceSize
做了cache
的setFastInstanceSize
。
所以我们接着往下看fastInstanceSize
走else
判断
-
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
相当于获取到得到setFastInstanceSize
里(word_align(newSize) + FAST_CACHE_ALLOC_DELTA16)
,可以在setFastInstanceSize
用_flags & FAST_CACHE_ALLOC_MASK
验证一下。 -
align16(size + extra - FAST_CACHE_ALLOC_DELTA16)
参数减去了FAST_CACHE_ALLOC_DELTA16
,因为在setFastInstanceSize
加了setFastInstanceSize
, 这样得到的就是word_align(newSize)
。
那么看一下word_align
# define WORD_MASK 7UL ( 0000 0000 0000 0111)
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
~WORD_MASK
:1111 1111 1111 1000
(x + WORD_MASK) & ~WORD_MASK
即把(x+7) 低三位清0,结果将是8的倍数,与16字节对齐算法一样。
那么
#define FAST_CACHE_ALLOC_DELTA16 0x0008
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
FAST_CACHE_ALLOC_DELTA16等于8, 因此得到的sizeBits=8的倍数+8,至少也是16。
- 所以
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
获取到的size>=16且是8的倍数,再通过align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
进行16字节对齐 -
(x + size_t(15)) & ~size_t(15);
这里的x
即word_align(newSize)
下面通俗的理解一下
15取反 1111 1111 1111 0000 = 1*2^15 +... 1*2^4
与15取反 相与 前四位为0 即抹掉前4位 只要是大于15的数与它相与必定是16的倍数
x 默认 + 15 ,如果x小于16 那么+15也是大于16了,自然也是返回16
举个例子:
x = 8
(x + size_t(15)) & ~size_t(15)
15 二进制 :0000 0000 0000 1111
~size_t(15) : 15取反 1111 1111 1111 0000
x + size_t(15) = 8+15 = 23 0000 0000 0001 0111
23&~15 : 0000 0000 0001 0000 =1*2^4 = 16
所以cls->instanceSize
得到的size是16的倍数
接下来看看具体开辟内存空间的方法calloc
这部分源码需要去libmalloc
里面看了,可以在mallco.c
里找到实现
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
到这里ptr = zone->calloc(zone, num_items, size);
,单看源码无法继续往下了,因此编译源码来debug
。
-
main
函数calloc
一下void *p = calloc(1, 8);
- 在
ptr = zone->calloc(zone, num_items, size);
打上断点,lldb
输入s
指令下一步,发现进入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);
}
- 又无法跳转,还是断住,
s
,发现进入nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
printf(&total_bytes);
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
printf("1111\n");
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
- 调试继续往下看,进入了
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
调试看到这里的total_bytes
就是传进来的size
(通过这一步calloc_get_size(num_items, size, 0, &total_bytes)
操作) - 而最终
_nano_malloc_check_clear
里面有一个重要方法size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
通过调试发现slot_bytes
即是分配的大小
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{ /// 部分源码
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
printf(&size);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
}
而最终的segregated_size_to_fit
则又是做了一次16字节对齐
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;
}
可以看到当size=0
的时候,size=NANO_REGIME_QUANTA_SIZE
,如下宏,1左移4位 即 0000 0000 0001 0000
即16。
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
size!=0
是进行下面的操作
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;
相当于k = (size+16-1) >> 4; slot_bytes = k <<4;
相当于低四位清0, 和上面align16
效果一样,得到的将是16的倍数。
例子:
size=8
k = (8+16-1) >>4 即23>>4
23: 0000 0000 0001 0111
右移4位 :0000 0000 0000 0001
再左移4位:0000 0000 0001 0000
即低四位清0 得16
由此可见,目前版本中instanceSize
和calloc
都做了内存的16字节对齐的保证。
最后实践来看看sizeof
, class_getInstanceSize
, malloc_size
区别
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8字节
int _age; // 4字节
};// 16字节
/// 结构体内存对齐 大小必须是最大成员大小的倍数
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16 字节
int _no; // 4字节
};// 16字节
@interface Person : NSObject
{
@public
int _age; // 4字节
}
@end
@implementation Person
@end
@interface Student : Person
{
@public
int _no;// 4字节
}
@end
@implementation Student
@end
如上Person和Student
及其结构体本质
先各自打印出结果
NSObject *obj = [[NSObject alloc]init];
NSLog(@"sizeof obj: %zd",sizeof(struct NSObject_IMPL));
NSLog(@"class_getInstanceSize obj: %zd",class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size obj: %zd",malloc_size((__bridge const void *)(obj)));
Person *person = [[Person alloc]init];
person->_age = 4;
Student *student = [[Student alloc]init];
student->_age = 3;
student->_no = 5;
NSLog(@"sizeof person: %zd",sizeof(struct Person_IMPL));
NSLog(@"class_getInstanceSize person: %zd",class_getInstanceSize([Person class]));
NSLog(@"malloc_size person: %zd",malloc_size((__bridge const void *)(person)));
NSLog(@"sizeof student struct: %zd",sizeof(struct Student_IMPL));
NSLog(@"sizeof student: %zd",sizeof(student));
NSLog(@"class_getInstanceSize studetn: %zd",class_getInstanceSize([Student class]));
NSLog(@"malloc_size studetn: %zd",malloc_size((__bridge const void *)(student)));
image.png
1. sizeof
- 是一个运算符,传进来的是类型,用来计算这个类型占多大内存,这个在 编译器编译阶段 就会确定大小。
比如sizeof(student)
,student即指针类型,占8个字节,不管你是传student
或者person
都是8
上面结果有一个sizeof(struct Student_IMPL)
为24 , 按理Student_IMPL
结构体内应该是16+4=20,这是因为结构体内存对齐,大小必须是最大成员大小的倍数,即isa占用的8的倍数。
2. class_getInstanceSize
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());
}
看注释是 成员变量的大小 ,word_align
是8字节对齐的 , 所以class_getInstanceSize
计算出来的大小是8字节对齐的。
3. malloc_size
- 计算对象实际分配的内存大小 即上面分析的
segregated_size_to_fit
,是16字节对齐的。
追踪malloc_size的源码 最终追踪到
static MALLOC_INLINE size_t
__nano_vet_and_size_inner(nanozone_t *nanozone, const void *ptr, boolean_t inner)
{
// Extracts the size of the block in bytes. Checks for a plausible ptr.
nano_blk_addr_t p; // the compiler holds this in a register
nano_meta_admin_t pMeta;
p.addr = (uint64_t)ptr; // Begin the dissection of ptr
if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
return 0;
}
if (nano_common_max_magazines <= p.fields.nano_mag_index) {
return 0;
}
if (!inner && p.fields.nano_offset & NANO_QUANTA_MASK) { // stray low-order bits?
return 0;
}
pMeta = &(nanozone->meta_data[p.fields.nano_mag_index][p.fields.nano_slot]);
if ((void *)(pMeta->slot_bump_addr) <= ptr) {
return 0; // Beyond what's ever been allocated!
}
if (!inner && ((p.fields.nano_offset % pMeta->slot_bytes) != 0)) {
return 0; // Not an exact multiple of the block size for this slot
}
printf("22222");
return pMeta->slot_bytes;
}
返回的是pMeta->slot_bytes
,而这个pMeta->slot_bytes
在开辟空间时的_nano_malloc_check_clear
里 可以看到蛛丝马迹。
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
最终走到segregated_band_grow
里面有一段赋值
pMeta->slot_bytes = (unsigned int)slot_bytes;
所以malloc_size
即是经过segregated_size_to_fit
进行16字节对齐的slot_bytes
。
如有错误,请大佬纠正。