前言
前文讲到,alloc是oc中创建对象并分配内存的函数,其源码最终会调用calloc函数。
本文将继续探索如何确定对象创建所需内存大小与内存分配的原理。
对象创建所需内存大小
调试分析
示例代码
@interface EmptyFoo : NSObject
@end
@interface PropertyFoo : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end
@interface MemberFoo : NSObject
{
NSString *name;
int age;
}
@end
@interface FuncFoo : NSObject
- (void)test;
+ (void)test2;
@end
int main() {
EmptyFoo *o = [EmptyFoo alloc];
NSLog(@"objectSize:%lu mallocSize:%lu", class_getInstanceSize(EmptyFoo.class), malloc_size((__bridge void *)o));
PropertyFoo *o1 = [PropertyFoo alloc];
NSLog(@"objectSize:%lu mallocSize:%lu", class_getInstanceSize(PropertyFoo.class), malloc_size((__bridge void *)o1));
MemberFoo *o2 = [MemberFoo alloc];
NSLog(@"objectSize:%lu mallocSize:%lu", class_getInstanceSize(MemberFoo.class), malloc_size((__bridge void *)o2));
FuncFoo *o3 = [FuncFoo alloc];
NSLog(@"objectSize:%lu mallocSize:%lu", class_getInstanceSize(FuncFoo.class), malloc_size((__bridge void *)o3));
}
运行结果:
EmptyFoo objectSize:8 mallocSize:16
PropertyFoo objectSize:24 mallocSize:32
MemberFoo objectSize:24 mallocSize:32
FuncFoo objectSize:8 mallocSize:16
关于class_getInstanceSize和malloc_size的区别可见:「类与对象」如何准确获取对象的内存大小?
通过本地调试,我们简单确认了以下几点:
- 成员变量和属性都会影响对象的大小,类方法和实例方法不会。
- 空类(即
NSObject)的对象本身就有8字节的大小(ISA)。 - 对象本身大小和内存分配给对象的大小不一致,前者是8字节对齐,后者是16字节对齐。
这里对象本身大小和分配内存大小如何理解:如下图所示,一个空NSObject对象在内存中占用16字节大小,但对象本身只使用了8字节大小。
源码分析
回顾一下OC中对象创建的底层源码如下,可发现申请内存的大小是由cls->instanceSize计算所得。
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
...
size_t size;
// !!计算创建对象需要的大小 会进行字节对齐!!
size = cls->instanceSize(extraBytes);
...
id obj = (id)calloc(1, size);
...
return obj;
}
cls->instanceSize()
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
// 从缓存中获取实例大小
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;
}
当缓存中有实例大小时走fastInstanceSize,可以注意到返回值执行了align16,说明进行了16字节对齐。
size_t fastInstanceSize(size_t extra) const
{
...
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
没有缓存时通过alignedInstanceSize()计算实例大小,64位机下会进行8字节对齐。
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
#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
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
综上,有两个问题需要关注
-
走缓存和直接计算所需的实例大小时不一样的,前者16字节对齐,后者8字节对齐,这里是不是有什么问题?
回顾上文,计算出的实例大小是用于
id obj = (id)calloc(1, size);,实际上calloc函数在申请内存大小时本身是16字节对齐的,所以走缓存或直接计算所得的size不一致也不会对内存分配造成影响。这里后文会有更详细的讲解。 -
为什么第一次运行
cache.hasFastInstanceSize()也为true,即走有缓存的逻辑?这个问题可通过下图回答,
_read_images可以理解为在程序最开始初始化阶段执行。
内存分配原理
调试分析
上文讲到,内存分配最终会调用calloc函数,且calloc函数申请的内存会进行16字节对齐。
示例代码
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p)); // 运行结果输出48
源码分析
calloc
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) {
...
void *ptr;
ptr = zone->calloc(zone, num_items, size);
return ptr;
}
到这里会发现zone->calloc()无法继续直接跳转,我们通过断点进入实际执行的函数。
// 此函数为_malloc_zone_calloc中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);
}
// 此函数即default_zone_calloc中zone->calloc()执行的函数
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
...
if (total_bytes <= NANO_MAX_SIZE) {
// !!重点函数!!
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
}
}
...
}
最终我们来到了底层的关键函数_nano_malloc_check_clear,其中我们主要关注计算实际分配内存大小的函数segregated_size_to_fit,具体分配内存的代码讲解感兴趣的可见:iOS 高级之美(六)—— malloc分析
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);
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));
if (ptr) {
...
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
// 初始化内存为0
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
segregated_size_to_fit
由源码可见,对传入的size进行了16字节对齐。
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
...
// 16字节对齐(+15 右移4位 左移4位)
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
...
return slot_bytes;
}
综上,内存分配的calloc函数为什么进行了16字节对齐已经水落石出。
至于为什么OC对calloc要进行16字节对齐,网上能找到的说法主要是为了有容错空间,因为一个ISA是8字节,如果实例分配按8字节对齐,只要内存地址偏移一点就容易访问到别的对象上。16字节对齐兼顾了内存利用率和容错率。
我自己理解可能也与兼容Swift有关,Swift的对象基础大小就是16字节。